Pure plugin definition library for MAJK - outputs plugin definitions, not HTTP servers
npm install @majkapp/plugin-kitFluent builder framework for creating robust MAJK plugins
Build type-safe, production-ready MAJK plugins with excellent developer experience, comprehensive validation, and clear error messages.
โจ Fluent API - Chainable builder pattern with full TypeScript support
๐ก๏ธ Type Safety - Compile-time checks for routes, IDs, and descriptions
โ
Build-Time Validation - Catches errors before runtime
๐ Clear Error Messages - Actionable suggestions when things go wrong
๐ Auto HTTP Server - Built-in routing, CORS, error handling
โ๏ธ React & HTML Screens - Support for both SPA and simple HTML UIs
๐ง Tool Management - Declare tools with schema validation
๐พ Storage Integration - Direct access to plugin storage
๐ก Event Bus - Subscribe to system events
๐ RPC Callbacks - Pass callbacks across plugin boundaries with automatic serialization
๐งน Auto Cleanup - Managed lifecycle with automatic resource cleanup
โค๏ธ Health Checks - Built-in health monitoring
``bash`
npm install @majk/plugin-kit
`typescript
import { definePlugin } from '@majk/plugin-kit';
export default definePlugin('my-plugin', 'My Plugin', '1.0.0')
.pluginRoot(__dirname) // REQUIRED: Must be first call
.ui({
appDir: 'dist',
base: '/plugin-screens/my-plugin', // NO trailing slash
history: 'hash'
})
.topbar('/plugin-screens/my-plugin/dashboard', {
icon: '๐'
})
.screenReact({
id: 'dashboard',
name: 'Dashboard',
description: 'Main dashboard for my plugin. Shows key metrics and actions.',
route: '/plugin-screens/my-plugin/dashboard',
reactPath: '/'
})
.apiRoute({
method: 'GET',
path: '/api/data',
name: 'Get Data',
description: 'Retrieves plugin data. Returns formatted response with metadata.',
handler: async (req, res, { majk, storage }) => {
const data = await storage.get('data') || [];
return { data, count: data.length };
}
})
.tool('global', {
name: 'myTool',
description: 'Does something useful. Processes input and returns results.',
inputSchema: {
type: 'object',
properties: {
param: { type: 'string' }
},
required: ['param']
}
}, async (input, { logger }) => {
logger.info(Tool called with: ${input.param});
return { success: true, result: input.param.toUpperCase() };
})
.build();
`
Every plugin starts with definePlugin(id, name, version) followed immediately by .pluginRoot(__dirname):
`typescript`
definePlugin('system-explorer', 'System Explorer', '1.0.0')
.pluginRoot(__dirname) // REQUIRED: Must be first call
Rules:
- id must be unique and URL-safe (kebab-case recommended)name
- is the display nameversion
- follows semver.pluginRoot(__dirname)
- is REQUIRED - Must be the first call after definePlugin
#### React Screens
For React SPAs, configure UI:
`typescript
.ui({
appDir: 'dist', // Where your built React app is
base: '/plugin-screens/my-plugin', // Base path (NO trailing slash)
history: 'hash' // REQUIRED: Use 'hash' for iframe routing
})
.screenReact({
id: 'dashboard',
name: 'Dashboard',
description: 'Main dashboard view. Shows metrics and controls.', // 2-3 sentences
route: '/plugin-screens/my-plugin/dashboard', // Must start with /plugin-screens/{id}/
reactPath: '/' // Path within your React app
})
`
The React app receives:
- window.__MAJK_BASE_URL__ - Host base URLwindow.__MAJK_IFRAME_BASE__
- - Plugin base pathwindow.__MAJK_PLUGIN_ID__
- - Your plugin ID
#### HTML Screens
For simple HTML pages:
`typescript`
.screenHtml({
id: 'about',
name: 'About',
description: 'Information about the plugin. Shows version and author.',
route: '/plugin-screens/my-plugin/about',
html: '...' // OR htmlFile: 'about.html'
})
Define REST endpoints:
`typescript
.apiRoute({
method: 'POST',
path: '/api/tasks/:id/complete',
name: 'Complete Task',
description: 'Marks a task as complete. Updates task status and triggers notifications.',
handler: async (req, res, { majk, storage, logger }) => {
const { id } = req.params; // Path parameters
const { note } = req.body; // Request body
const status = req.query.get('status'); // Query params
logger.info(Completing task ${id});
// Access MAJK APIs
const todos = await majk.todos.list();
// Use plugin storage
await storage.set(task:${id}, { completed: true });
return { success: true, taskId: id };
}
})
`
Available Methods: GET, POST, PUT, PATCH, DELETE
Context Provided:
- majk - Full MAJK API interfacestorage
- - Plugin-scoped key-value storagelogger
- - Scoped logger (debug, info, warn, error)http
- - HTTP configuration (port, baseUrl, secret)
Tools are functions that agents can invoke:
`typescript
.tool(
'conversation', // Scope: 'global' | 'conversation' | 'teammate' | 'project'
{
name: 'analyzeSentiment',
description: 'Analyzes text sentiment. Returns positive, negative, or neutral classification.',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string' }
},
required: ['text']
}
},
async (input, { majk, logger }) => {
logger.info('Analyzing sentiment');
// Your implementation
const sentiment = analyzeSentiment(input.text);
return {
success: true,
data: { sentiment, confidence: 0.95 }
};
}
)
`
Tool Scopes:
- global - Available everywhereconversation
- - Scoped to conversationsteammate
- - Scoped to teammatesproject
- - Scoped to projects
Declare entities your plugin provides:
`typescript
.entity('teammate', [
{
id: 'bot-assistant',
name: 'Bot Assistant',
role: 'bot',
capabilities: ['analysis', 'reporting']
}
])
.entity('mcpServer', [
{
id: 'custom-server',
name: 'Custom MCP Server',
transport: { type: 'stdio', command: 'node', args: ['server.js'] }
}
])
`
Supported Entity Types:
- mcpServer - MCP serversteammate
- - Team members/botsconversation
- - Conversationstodo
- - Tasksproject
- - Projectsagent
- - AI agents
#### Config Wizard
Show a wizard on first run:
`typescript`
.configWizard({
path: '/setup',
title: 'Initial Setup',
width: 600,
height: 400,
description: 'Configure plugin settings. Set up API keys and preferences.',
shouldShow: async (ctx) => {
const config = await ctx.storage.get('config');
return !config; // Show if no config exists
}
})
#### Settings Screen
Ongoing settings management:
`typescript`
.settings({
path: '/settings',
title: 'Plugin Settings',
description: 'Manage plugin configuration. Adjust behavior and display options.'
})
#### onReady
Called after server starts, before onLoad completes:
`typescriptConversation event: ${event.type}
.onReady(async (ctx, cleanup) => {
// Subscribe to events
const sub = ctx.majk.eventBus.conversations().subscribe((event) => {
ctx.logger.info();
});
cleanup(() => sub.unsubscribe());
// Set up timers
const timer = setInterval(() => {
ctx.logger.debug('Periodic check');
}, 60000);
cleanup(() => clearInterval(timer));
// Any other setup
await loadData(ctx.storage);
})
`
Cleanup Registration:
All cleanup functions are automatically called on onUnload().
#### Health Checks
Define custom health monitoring:
`typescript
.health(async ({ majk, storage, logger }) => {
try {
// Check dependencies
await majk.conversations.list();
await storage.get('health-check');
return {
healthy: true,
details: { api: 'ok', storage: 'ok' }
};
} catch (error) {
logger.error(Health check failed: ${error.message});`
return {
healthy: false,
details: { error: error.message }
};
}
})
Provided to all handlers and hooks:
`typescript
interface PluginContext {
pluginId: string; // Your plugin ID
pluginRoot: string; // Plugin directory path
dataDir: string; // Plugin data directory
app: {
version: string; // MAJK version
name: string; // App name
appDataDir: string; // App data directory
};
http: {
port: number; // Assigned HTTP port
secret: string; // Security secret
baseUrl: string; // Base URL for iframe
};
majk: MajkInterface; // Full MAJK API
storage: PluginStorage; // Key-value storage
logger: PluginLogger; // Scoped logger
timers?: ScopedTimers; // Managed timers
ipc?: ScopedIpcRegistry; // Electron IPC
}
`
The main MAJK API:
`typescript`
interface MajkInterface {
ai: AIAPI; // AI providers and LLMs
conversations: ConversationAPI;
todos: TodoAPI;
projects: ProjectAPI;
teammates: TeammateAPI;
mcpServers: MCPServerAPI;
knowledge: KnowledgeAPI;
tasks: TaskAPI;
eventBus: EventBusAPI;
auth: AuthAPI;
secrets: SecretsAPI;
plugins: PluginManagementAPI;
}
Access AI providers and language models:
`typescript
// Get the default LLM
const llm = ctx.majk.ai.getDefaultLLM();
// Send a prompt
const result = await llm.prompt({
messages: [
{ role: 'system', content: 'You are a helpful assistant' },
{ role: 'user', content: 'What is 2+2?' }
],
temperature: 0.7,
maxTokens: 100
});
console.log(result.content); // "4"
// Stream responses
const stream = llm.promptStream({
messages: [{ role: 'user', content: 'Tell me a story' }]
});
for await (const chunk of stream) {
if (chunk.type === 'content_delta') {
process.stdout.write(chunk.content);
}
}
// List available providers
const providers = ctx.majk.ai.listProviders();
console.log(Available: ${providers.map(p => p.name).join(', ')});
// Get specific provider
const bedrock = ctx.majk.ai.getProvider('bedrock');
if (bedrock) {
const claude = bedrock.getLLM('anthropic.claude-3-5-sonnet-20241022-v2:0');
// Use Claude...
}
// Query by capability
const imageProviders = ctx.majk.ai.getProvidersWithCapability('imageGeneration');
if (imageProviders.length > 0) {
const image = await imageProviders[0].generateImage({
prompt: 'A beautiful sunset over mountains'
});
}
`
Key Features:
- Provider-agnostic: Works with OpenAI, Anthropic, Bedrock, local models, etc.
- Streaming support: Real-time response streaming
- Function calling: LLM can invoke functions
- Structured output: JSON schema enforcement
- Advanced capabilities: Image generation, embeddings, transcription
- Capability discovery: Query providers by features
Use Cases:
- Add AI features to your plugin
- Create AI-powered tools
- Build custom AI workflows
- Integrate multiple AI providers
- Generate content, analyze data, summarize text
Generate authenticated URLs for opening plugin UIs in external browsers or creating cross-plugin links:
`typescript
// Simple convenience method - get URL for another plugin's screen
const url = await ctx.majk.plugins.getExternalUrl(
'@analytics/dashboard', // Plugin ID (package name format)
'main', // Screen ID (optional)
'#/overview' // Hash fragment (optional)
);
// Opens: http://localhost:9000/plugins/analytics/dashboard/ui/main?token=abc123#/overview
await ctx.shell?.openExternal(url);
// Advanced: Full control over URL generation
const customUrl = await ctx.majk.plugins.getPluginScreenUrl('@myorg/plugin', {
screenId: 'settings', // Specific screen
theme: 'dark', // Theme preference
hash: '#/advanced', // Client-side route
queryParams: { // Custom query parameters
view: 'compact',
filter: 'active'
}
});
// Direct URL with custom path (when you know the exact endpoint)
const apiUrl = await ctx.majk.plugins.getPluginExternalUrl('plugin-id', {
path: '/api/export', // Custom path within plugin
queryParams: { format: 'json' }
});
`
API Methods:
- getExternalUrl(pluginId, screenId?, hash?) - Simple convenience method for getting screen URLs
- getPluginScreenUrl(pluginId, options) - Screen-aware URL generation with full options
- getPluginExternalUrl(pluginId, options) - Low-level URL generation for custom paths
URL Format:
``
http://localhost:{port}/plugins/{org}/{plugin}/ui/{screen}?token={token}&theme={theme}#{hash}
Security:
- URLs include one-time authentication tokens (60s TTL)
- After expiration, users are redirected to login if required
- Tokens are automatically validated by the plugin server
Use Cases:
- Open plugin screens in external browser windows
- Create shareable links to plugin functionality
- Cross-plugin navigation and deep linking
- Programmatic browser-based testing
- Integration with external tools and workflows
Example - Cross-Plugin Integration:
`typescript
.apiRoute({
method: 'POST',
path: '/api/generate-report',
name: 'Generate Report',
description: 'Generates analytics report. Creates report and returns link to view.',
handler: async (req, res, { majk }) => {
// Generate report data
const reportId = await generateReport(req.body);
// Create link to analytics plugin's viewer
const viewerUrl = await majk.plugins.getExternalUrl(
'@analytics/viewer',
'report',
#/report/${reportId}
);
return {
success: true,
reportId,
viewUrl: viewerUrl // User can click to view in analytics plugin
};
}
})
`
Simple key-value storage scoped to your plugin:
`typescript`
interface PluginStorage {
get
set
delete(key: string): Promise
clear(): Promise
keys(): Promise
}
Example:
`typescript
// Save data
await storage.set('user-preferences', {
theme: 'dark',
notifications: true
});
// Load data
const prefs = await storage.get
// List all keys
const keys = await storage.keys();
// Delete specific key
await storage.delete('old-data');
// Clear everything
await storage.clear();
`
Subscribe to system events:
`typescriptEvent: ${event.type}
// Listen to conversation events
const sub = majk.eventBus.conversations().subscribe((event) => {
console.log(, event.entity);
});
// Unsubscribe
sub.unsubscribe();
// Specific event types
majk.eventBus.conversations().created().subscribe(...);
majk.eventBus.conversations().updated().subscribe(...);
majk.eventBus.conversations().deleted().subscribe(...);
// Custom channels
majk.eventBus.channel('my-events').subscribe(...);
`
The kit validates at build time:
โ
Route Prefixes - Screen routes must match plugin ID
โ
File Existence - React dist and HTML files must exist
โ
Uniqueness - No duplicate routes, tools, or API endpoints
โ
Dependencies - UI must be configured for React screens
โ
Descriptions - Must be 2-3 sentences ending with period
Example Error:
``
โ Plugin Build Failed: React screen route must start with "/plugin-screens/my-plugin/"
๐ก Suggestion: Change route from "/screens/dashboard" to "/plugin-screens/my-plugin/dashboard"
๐ Context: {
"screen": "dashboard",
"route": "/screens/dashboard"
}
All API route errors are automatically caught and logged:
`typescript
.apiRoute({
method: 'POST',
path: '/api/process',
name: 'Process Data',
description: 'Processes input data. Validates and transforms the payload.',
handler: async (req, res, { logger }) => {
// Errors are automatically caught and returned as 500 responses
throw new Error('Processing failed');
// Returns:
// {
// "error": "Processing failed",
// "route": "Process Data",
// "path": "/api/process"
// }
}
})
`
Logs show:
``
โ POST /api/process - Error: Processing failed
[stack trace]
`typescript
// โ Don't use in-memory state
let cache = {};
// โ
Use storage
await ctx.storage.set('cache', data);
`
`typescript
.onReady(async (ctx, cleanup) => {
// โ Don't forget to cleanup
const timer = setInterval(...);
// โ
Register cleanup
const timer = setInterval(...);
cleanup(() => clearInterval(timer));
// โ
Event subscriptions
const sub = ctx.majk.eventBus.conversations().subscribe(...);
cleanup(() => sub.unsubscribe());
})
`
`typescript
.apiRoute({
method: 'POST',
path: '/api/create',
name: 'Create Item',
description: 'Creates a new item. Validates input before processing.',
handler: async (req, res) => {
// โ
Validate input
if (!req.body?.name) {
res.status(400).json({ error: 'Name is required' });
return;
}
// Process...
}
})
`
`typescript
// โ Basic logging
logger.info('User action');
// โ
Structured logging
logger.info('User action', { userId, action: 'create', resourceId });
`
`typescriptTool failed: ${error.message}
.tool('global', spec, async (input, { logger }) => {
try {
const result = await processData(input);
return { success: true, data: result };
} catch (error) {
logger.error();`
return {
success: false,
error: error.message,
code: 'PROCESSING_ERROR'
};
}
})
See example.ts for a comprehensive example showing:
- React and HTML screens
- API routes with parameters
- Tools in different scopes
- Entity declarations
- Config wizard
- Event subscriptions
- Storage usage
- Health checks
Plugins can communicate with each other using RPC services and pass callbacks across plugin boundaries:
`typescriptProcessed: ${path}
// Plugin A: Register a service
.onLoad(async (ctx) => {
await ctx.rpc.registerService('fileProcessor', {
async processFile(path: string, onProgress: (percent: number) => void): Promise
await onProgress(0);
await onProgress(50);
await onProgress(100);
return ;`
}
});
})
`typescript
// Plugin B: Use the service with callbacks
.onLoad(async (ctx) => {
const processor = await ctx.rpc.createProxy<{
processFile(path: string, onProgress: (p: number) => void): Promise
}>('fileProcessor');
// Pass callback - it just works!
const result = await processor.processFile('/file.txt', (progress) => {
console.log(Progress: ${progress}%);`
});
})
For long-lived callbacks (subscriptions, event listeners), use createCallback():
`typescript
// Auto-cleanup after 10 calls
const callback = await ctx.rpc.createCallback!(
(event) => console.log('Event:', event),
{ maxCalls: 10 }
);
await eventService.subscribe(callback);
// Auto-cleanup after 5 seconds
const callback = await ctx.rpc.createCallback!(
(data) => console.log('Data:', data),
{ timeout: 5000 }
);
await dataStream.subscribe(callback);
// Manual cleanup
const callback = await ctx.rpc.createCallback!((msg) => console.log(msg));
// ... later
ctx.rpc.cleanupCallback!(callback);
`
See docs/RPC_CALLBACKS.md for comprehensive documentation, patterns, and best practices.
Full TypeScript support with:
`typescript
import {
definePlugin,
FluentBuilder,
PluginContext,
RequestLike,
ResponseLike
} from '@majk/plugin-kit';
// Type-safe plugin ID
const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0');
// ^ Enforces route prefixes
// Type-safe routes
.screenReact({
route: '/plugin-screens/my-plugin/dashboard'
// ^^^^^^^^^ Must match plugin ID
})
`
After running npm run build, check ui/src/generated/generation.log for schema optimization warnings. The generator analyzes your function input schemas and provides actionable suggestions for improving hook performance (e.g., flattening nested structures, reducing large objects).
``
โ Plugin Build Failed: React app not built: /path/to/ui/dist/index.html does not exist
๐ก Suggestion: Run "npm run build" in your UI directory to build the React app
Fix: Build your React app before building the plugin.
``
โ Plugin Build Failed: Duplicate API route: POST /api/data
Fix: Each route (method + path) must be unique.
``
โ Plugin Build Failed: Description for "My Screen" must be 2-3 sentences, found 1 sentences
๐ก Suggestion: Rewrite the description to have 2-3 clear sentences.
Fix: Write 2-3 complete sentences ending with periods.
```
โ Plugin Build Failed: Duplicate tool name: "analyze"
Fix: Each tool name must be unique within the plugin.
MIT