npm package: @majkapp/plugin-kit
Description: Pure plugin definition library for MAJK - outputs plugin definitions, not HTTP servers
Author: Majk Contributors
Latest version: 3.7.13

# @majk/plugin-kit

**Fluent builder framework for creating robust MAJK plugins**

Build type-safe, production-ready MAJK plugins with excellent developer experience, comprehensive validation, and clear error messages.

## Features

✨ **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

## Installation

```bash
npm install @majk/plugin-kit
```

## Quick Start

```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();
```

## Core Concepts

### Plugin Definition

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 name
- `version` follows semver
- **`.pluginRoot(__dirname)` is REQUIRED** - Must be the first call after `definePlugin`

### Screens

#### 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 URL
- `window.__MAJK_IFRAME_BASE__` - Plugin base path
- `window.__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: '<html>...'  // OR htmlFile: 'about.html'
})
```

### API Routes

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 interface
- `storage` - Plugin-scoped key-value storage
- `logger` - Scoped logger (debug, info, warn, error)
- `http` - HTTP configuration (port, baseUrl, secret)

### Tools

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 everywhere
- `conversation` - Scoped to conversations
- `teammate` - Scoped to teammates
- `project` - Scoped to projects

### Entities

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 servers
- `teammate` - Team members/bots
- `conversation` - Conversations
- `todo` - Tasks
- `project` - Projects
- `agent` - AI agents

### Config Wizard & Settings

#### 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.'
})
```

### Lifecycle Hooks

#### onReady

Called after server starts, before `onLoad` completes:

```typescript
.onReady(async (ctx, cleanup) => {
  // Subscribe to events
  const sub = ctx.majk.eventBus.conversations().subscribe((event) => {
    ctx.logger.info(`Conversation event: ${event.type}`);
  });
  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 }
    };
  }
})
```

## API Reference

### PluginContext

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
}
```

### MajkInterface

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;
}
```

### AI API

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

### Plugin Management API

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
    };
  }
})
```

### PluginStorage

Simple key-value storage scoped to your plugin:

```typescript
interface PluginStorage {
  get<T>(key: string): Promise<T | undefined>;
  set<T>(key: string, value: T): Promise<void>;
  delete(key: string): Promise<void>;
  clear(): Promise<void>;
  keys(): Promise<string[]>;
}
```

**Example:**

```typescript
// Save data
await storage.set('user-preferences', {
  theme: 'dark',
  notifications: true
});

// Load data
const prefs = await storage.get<Preferences>('user-preferences');

// List all keys
const keys = await storage.keys();

// Delete specific key
await storage.delete('old-data');

// Clear everything
await storage.clear();
```

### EventBus

Subscribe to system events:

```typescript
// Listen to conversation events
const sub = majk.eventBus.conversations().subscribe((event) => {
  console.log(`Event: ${event.type}`, 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(...);
```

## Validation & Error Handling

### Build-Time Validation

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"
}
```

### Runtime Error Handling

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]
```

## Best Practices

### 1. Use Storage for State

```typescript
// ❌ Don't use in-memory state
let cache = {};

// ✅ Use storage
await ctx.storage.set('cache', data);
```

### 2. Register Cleanups

```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());
})
```

### 3. Validate Input

```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...
  }
})
```

### 4. Use Structured Logging

```typescript
// ❌ Basic logging
logger.info('User action');

// ✅ Structured logging
logger.info('User action', { userId, action: 'create', resourceId });
```

### 5. Handle Errors Gracefully

```typescript
.tool('global', spec, async (input, { logger }) => {
  try {
    const result = await processData(input);
    return { success: true, data: result };
  } catch (error) {
    logger.error(`Tool failed: ${error.message}`);
    return {
      success: false,
      error: error.message,
      code: 'PROCESSING_ERROR'
    };
  }
})
```

## Examples

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

## RPC & Callbacks

Plugins can communicate with each other using RPC services and pass callbacks across plugin boundaries:

### Basic RPC Service

```typescript
// Plugin A: Register a service
.onLoad(async (ctx) => {
  await ctx.rpc.registerService('fileProcessor', {
    async processFile(path: string, onProgress: (percent: number) => void): Promise<string> {
      await onProgress(0);
      await onProgress(50);
      await onProgress(100);
      return `Processed: ${path}`;
    }
  });
})
```

### Consuming with Callbacks

```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<string>
  }>('fileProcessor');

  // Pass callback - it just works!
  const result = await processor.processFile('/file.txt', (progress) => {
    console.log(`Progress: ${progress}%`);
  });
})
```

### Explicit Callbacks with Cleanup

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](docs/RPC_CALLBACKS.md) for comprehensive documentation, patterns, and best practices.

## TypeScript

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
})
```

## Troubleshooting

### Client Generation Warnings

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).

### "React app not built"

```
❌ 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.

### "Duplicate API route"

```
❌ Plugin Build Failed: Duplicate API route: POST /api/data
```

**Fix:** Each route (method + path) must be unique.

### "Description must be 2-3 sentences"

```
❌ 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.

### "Tool names must be unique"

```
❌ Plugin Build Failed: Duplicate tool name: "analyze"
```

**Fix:** Each tool name must be unique within the plugin.

## License

MIT