npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@majkapp/plugin-kit

v3.7.12

Published

Pure plugin definition library for MAJK - outputs plugin definitions, not HTTP servers

Readme

@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

npm install @majk/plugin-kit

Quick Start

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

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:

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

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

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

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

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

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

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

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

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

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:

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:

// 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:

// 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:

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

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:

// 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:

// 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:

.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

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

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

2. Register Cleanups

.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

.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

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

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

5. Handle Errors Gracefully

.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

// 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

// 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():

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

TypeScript

Full TypeScript support with:

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