DevelopersPlugin Development Guide

Plugin Development Guide

Build powerful extensions for Lokus with our comprehensive plugin system. This guide takes you from zero to publishing your first plugin.

What You’ll Build

By following this guide, you’ll learn to:

  • Set up a complete plugin development environment
  • Create plugins using the Plugin SDK
  • Use CLI tools for rapid development
  • Work with editor, UI, and data provider APIs
  • Test and debug plugins effectively
  • Publish plugins to the registry
  • Follow best practices and security guidelines

Prerequisites

Before starting, ensure you have:

  • Node.js 18+ and npm/yarn installed
  • TypeScript knowledge (JavaScript works too)
  • Lokus installed on your system
  • Code editor (VS Code recommended)
  • Git for version control

Optional but helpful:

  • Familiarity with React (for UI plugins)
  • Understanding of TipTap/ProseMirror (for editor plugins)
  • Basic knowledge of async/await patterns

Quick Start

The fastest way to get started:

# Install the Plugin CLI globally
npm install -g lokus-plugin-cli
 
# Create your first plugin (Quick Mode)
lokus-plugin create my-first-plugin -y
 
# Navigate to plugin directory
cd my-first-plugin
 
# Link for development
lokus-plugin link
 
# Start development with hot reload
lokus-plugin dev

This creates a fully configured plugin project with TypeScript, testing, and hot reload support.

Development Environment Setup

Installing Dependencies

# Install plugin SDK
npm install @lokus/plugin-sdk
 
# Install development tools
npm install --save-dev typescript @types/node eslint prettier jest

Install these extensions for the best experience:

{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "ms-vscode.vscode-typescript-next",
    "orta.vscode-jest"
  ]
}

Configure workspace settings (.vscode/settings.json):

{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "typescript.tsdk": "node_modules/typescript/lib"
}

TypeScript Configuration

Create or use generated tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "moduleResolution": "node",
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "test"]
}

Understanding Plugin Structure

A typical plugin project looks like this:

my-plugin/
├── src/
│   ├── index.ts              # Plugin entry point
│   ├── commands.ts           # Command implementations
│   ├── ui/                   # UI components
│   │   ├── Panel.tsx
│   │   └── styles.css
│   └── utils/                # Utilities
│       └── helpers.ts
├── assets/
│   ├── icon.png              # Plugin icon (128x128)
│   └── logo.svg              # Logo assets
├── test/
│   ├── index.test.ts         # Unit tests
│   └── integration.test.ts   # Integration tests
├── package.json              # npm configuration
├── plugin.json               # Plugin manifest
├── tsconfig.json             # TypeScript config
├── .eslintrc.json           # ESLint config
├── .prettierrc              # Prettier config
├── README.md                # Documentation
└── .gitignore              # Git ignore rules

Creating Your First Plugin

Let’s build a complete plugin step by step.

Step 1: Create the Plugin Manifest

Create plugin.json:

{
  "id": "mycompany.hello-plugin",
  "version": "1.0.0",
  "name": "Hello Plugin",
  "displayName": "Hello World Plugin for Lokus",
  "description": "A simple plugin that demonstrates core functionality",
  "author": {
    "name": "Your Name",
    "email": "you@example.com"
  },
  "license": "MIT",
  "lokusVersion": "^1.0.0",
  "categories": ["Editor"],
  "keywords": ["hello", "example", "tutorial"],
  "icon": "assets/icon.png",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "activationEvents": [
    "onStartup"
  ],
  "permissions": [
    "editor:read",
    "editor:write",
    "ui:create",
    "commands:register",
    "storage:read",
    "storage:write"
  ],
  "contributes": {
    "commands": [
      {
        "command": "helloPlugin.sayHello",
        "title": "Say Hello",
        "category": "Hello Plugin",
        "icon": "$(heart)"
      },
      {
        "command": "helloPlugin.insertGreeting",
        "title": "Insert Greeting",
        "category": "Hello Plugin"
      }
    ],
    "keybindings": [
      {
        "command": "helloPlugin.insertGreeting",
        "key": "ctrl+shift+g",
        "mac": "cmd+shift+g",
        "when": "editorTextFocus"
      }
    ],
    "configuration": {
      "title": "Hello Plugin",
      "properties": {
        "helloPlugin.greeting": {
          "type": "string",
          "default": "Hello",
          "description": "Greeting message to use"
        },
        "helloPlugin.showTimestamp": {
          "type": "boolean",
          "default": true,
          "description": "Include timestamp in greetings"
        }
      }
    }
  }
}

Step 2: Create the Plugin Entry Point

Create src/index.ts:

import { definePlugin } from '@lokus/plugin-sdk';
 
export default definePlugin({
  activate(context) {
    // Log activation
    context.log('info', 'Hello Plugin is activating...');
 
    // Register commands
    context.subscriptions.push(
      context.api.commands.register({
        id: 'helloPlugin.sayHello',
        title: 'Say Hello',
        handler: async () => {
          const greeting = await context.api.storage.get('greeting', 'Hello');
          context.api.ui.showNotification({
            message: `${greeting} from Hello Plugin!`,
            type: 'success'
          });
        }
      })
    );
 
    // Create UI Panel
    context.subscriptions.push(
      context.api.ui.registerPanel({
        id: 'helloPlugin.panel',
        title: 'Hello Panel',
        type: 'webview',
        location: 'sidebar',
        icon: 'heart',
        html: `
          <div style="padding: 20px;">
            <h2>Hello Plugin</h2>
            <button onclick="window.parent.postMessage({ command: 'sayHello' }, '*')">
              Say Hello
            </button>
          </div>
        `
      })
    );
  },
 
  deactivate() {
    console.log('Hello Plugin deactivated');
  }
});

Step 3: Configure package.json

Create or update package.json:

{
  "name": "hello-plugin",
  "version": "1.0.0",
  "description": "A simple Hello World plugin for Lokus",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "dev": "lokus-plugin dev",
    "build": "tsc",
    "watch": "tsc --watch",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "lint": "eslint src --ext .ts",
    "lint:fix": "eslint src --ext .ts --fix",
    "format": "prettier --write 'src/**/*.ts'",
    "validate": "lokus-plugin validate",
    "package": "npm run build && lokus-plugin package",
    "clean": "rm -rf dist"
  },
  "dependencies": {
    "@lokus/plugin-sdk": "^1.0.0"
  },
  "devDependencies": {
    "@types/node": "^18.0.0",
    "@types/jest": "^29.0.0",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.0.0",
    "jest": "^29.0.0",
    "prettier": "^3.0.0",
    "ts-jest": "^29.0.0",
    "typescript": "^5.0.0"
  },
  "lokus": {
    "manifest": "./plugin.json"
  }
}

Step 4: Build and Test

# Install dependencies
npm install
 
# Build the plugin
npm run build
 
# Start development mode with hot reload
npm run dev

Open Lokus and test your plugin:

  1. Open command palette (Ctrl/Cmd + Shift + P)
  2. Type “Say Hello” and execute
  3. Press Ctrl/Cmd + Shift + G to insert greeting
  4. Open sidebar to see your panel

Using the Plugin SDK

The Plugin SDK provides everything you need to build plugins.

Using definePlugin

The definePlugin helper provides type inference and a cleaner API:

import { definePlugin } from '@lokus/plugin-sdk'
 
export default definePlugin({
  async activate(context) {
    // Access API through context.api
    // Subscriptions should be pushed to context.subscriptions
    
    context.log('info', 'Plugin activated');
    
    context.subscriptions.push(
      context.api.commands.register({
        id: 'my.command',
        title: 'My Command',
        handler: () => { /* ... */ }
      })
    );
  }
});

Benefits of BasePlugin:

  • Automatic subscription management
  • Built-in logging methods
  • Error handling utilities
  • Settings management helpers
  • Event listener management

Available APIs

The SDK provides comprehensive APIs:

// Access through context.api or this.api (in BasePlugin)
 
// Commands API
api.commands.register({ id, title, handler })
api.commands.execute(commandId, ...args)
 
// Editor API
api.editor.getActiveEditor()
api.editor.insertContent(content)
api.editor.getContent()
api.editor.setContent(content)
api.editor.registerExtension(extension)
 
// UI API
api.ui.showNotification({ message, type })
api.ui.showDialog({ title, message, buttons })
api.ui.registerPanel({ id, title, html })
api.ui.createStatusBarItem({ id, text, command })
 
// Workspace API
api.workspace.getWorkspacePath()
api.workspace.findFiles(pattern)
api.workspace.openFile(path)
 
// Filesystem API
api.fs.readFile(path)
api.fs.writeFile(path, content)
api.fs.exists(path)
api.fs.watch(path, handler)
 
// Storage API
api.storage.get(key, defaultValue)
api.storage.set(key, value)
api.storage.delete(key)
api.storage.clear()
 
// Configuration API
api.config.get(key, defaultValue)
api.config.set(key, value)
api.config.has(key)
 
// Network API (requires permission)
api.network.fetch(url, options)
 
// Events API
api.events.on(event, handler)
api.events.off(event, handler)
api.events.emit(event, data)

Permission System

Declare required permissions in plugin.json:

{
  "permissions": [
    "editor:read",
    "editor:write",
    "filesystem:read",
    "filesystem:write",
    "network:fetch",
    "ui:create",
    "commands:register",
    "storage:read",
    "storage:write"
  ]
}

Available Permissions:

  • editor:read - Read editor content
  • editor:write - Modify editor content
  • editor:create - Create new editors
  • filesystem:read - Read files
  • filesystem:write - Write files
  • filesystem:delete - Delete files
  • filesystem:watch - Watch file changes
  • network:fetch - Make HTTP requests
  • network:websocket - WebSocket connections
  • workspace:read - Read workspace data
  • workspace:write - Modify workspace
  • ui:create - Create UI elements
  • ui:modify - Modify UI
  • ui:notifications - Show notifications
  • commands:register - Register commands
  • commands:execute - Execute commands
  • storage:read - Read plugin storage
  • storage:write - Write plugin storage
  • storage:secrets - Access secret storage
  • shell:execute - Execute shell commands
  • clipboard:read - Read clipboard
  • clipboard:write - Write clipboard

Plugin Types

Basic Plugin

Simple command-based plugin:

export default class BasicPlugin extends BasePlugin {
  async activate(context: PluginContext) {
    await this.initialize(context)
 
    this.registerCommand({
      id: 'basicPlugin.hello',
      title: 'Hello',
      handler: () => {
        this.api?.ui.showNotification({
          message: 'Hello!',
          type: 'info'
        })
      }
    })
  }
}

Editor Plugin

Custom editor extensions:

import { Node } from '@tiptap/core'
 
export default class EditorPlugin extends BasePlugin {
  async activate(context: PluginContext) {
    await this.initialize(context)
 
    // Create custom node
    const CustomNode = Node.create({
      name: 'customNode',
      group: 'block',
      content: 'inline*',
 
      parseHTML() {
        return [{ tag: 'div.custom' }]
      },
 
      renderHTML({ HTMLAttributes }) {
        return ['div', { class: 'custom', ...HTMLAttributes }, 0]
      },
 
      addCommands() {
        return {
          insertCustomNode: () => ({ commands }) => {
            return commands.insertContent({
              type: this.name,
              content: [{ type: 'text', text: 'Custom content' }]
            })
          }
        }
      }
    })
 
    // Register extension
    this.api?.editor.registerExtension(CustomNode)
 
    // Register command
    this.registerCommand({
      id: 'editorPlugin.insertCustomNode',
      title: 'Insert Custom Node',
      handler: async () => {
        const editor = await this.api?.editor.getActiveEditor()
        editor?.commands.insertCustomNode()
      }
    })
  }
}

UI Plugin

Custom panels and webviews:

export default class UIPlugin extends BasePlugin {
  private panel?: WebviewPanel
 
  async activate(context: PluginContext) {
    await this.initialize(context)
 
    // Register panel with React component
    this.panel = this.registerPanel({
      id: 'uiPlugin.panel',
      title: 'My Panel',
      type: 'react',
      location: 'sidebar',
      icon: 'star',
      component: MyPanelComponent
    })
 
    // Listen for messages from panel
    this.panel.onDidReceiveMessage(message => {
      this.log('info', 'Received from panel:', message)
    })
 
    // Send messages to panel
    this.panel.postMessage({
      command: 'update',
      data: { value: 'hello' }
    })
  }
}

Data Provider Plugin

Integrate external data sources:

export default class DataProviderPlugin extends BasePlugin {
  async activate(context: PluginContext) {
    await this.initialize(context)
 
    // Register data provider
    this.api?.data.registerProvider({
      id: 'myDataProvider',
      name: 'My Data Provider',
 
      async query(params: QueryParams) {
        // Fetch data from external source
        const response = await fetch(
          `https://api.example.com/data?query=${params.query}`
        )
        return await response.json()
      },
 
      async create(item: DataItem) {
        // Create new item
        await fetch('https://api.example.com/data', {
          method: 'POST',
          body: JSON.stringify(item)
        })
      },
 
      async update(id: string, item: Partial<DataItem>) {
        // Update existing item
        await fetch(`https://api.example.com/data/${id}`, {
          method: 'PUT',
          body: JSON.stringify(item)
        })
      },
 
      async delete(id: string) {
        // Delete item
        await fetch(`https://api.example.com/data/${id}`, {
          method: 'DELETE'
        })
      }
    })
  }
}

MCP Plugin

Integrate Model Context Protocol:

export default class MCPPlugin extends BasePlugin {
  async activate(context: PluginContext) {
    await this.initialize(context)
 
    // Register MCP tools
    this.api?.mcp.registerTool({
      name: 'analyze_text',
      description: 'Analyze text content',
      inputSchema: {
        type: 'object',
        properties: {
          text: {
            type: 'string',
            description: 'Text to analyze'
          }
        }
      },
      handler: async (params) => {
        // Analyze text and return results
        return {
          wordCount: params.text.split(' ').length,
          charCount: params.text.length
        }
      }
    })
 
    // Register MCP resources
    this.api?.mcp.registerResource({
      uri: 'lokus://notes/recent',
      name: 'Recent Notes',
      description: 'List of recently edited notes',
      mimeType: 'application/json',
      handler: async () => {
        const files = await this.api?.workspace.findFiles('**/*.md')
        return JSON.stringify(files?.slice(0, 10))
      }
    })
  }
}

Theme Plugin

Custom themes:

export default class ThemePlugin extends BasePlugin {
  async activate(context: PluginContext) {
    await this.initialize(context)
 
    // Register theme
    this.api?.theme.register({
      id: 'myTheme',
      name: 'My Custom Theme',
      type: 'dark',
      colors: {
        'editor.background': '#1e1e1e',
        'editor.foreground': '#d4d4d4',
        'editor.lineHighlight': '#2d2d2d',
        'editor.selection': '#264f78',
        'ui.primary': '#007acc',
        'ui.secondary': '#3c3c3c',
        'text.primary': '#ffffff',
        'text.secondary': '#9d9d9d'
      },
      fonts: {
        'editor.fontFamily': 'JetBrains Mono, monospace',
        'editor.fontSize': '14px',
        'ui.fontFamily': 'Inter, sans-serif'
      }
    })
  }
}

Testing Plugins

Unit Testing

Create test/index.test.ts:

import { describe, it, expect, beforeEach } from '@jest/globals'
import { createMockContext } from '@lokus/plugin-sdk/testing'
import HelloPlugin from '../src/index'
 
describe('HelloPlugin', () => {
  let plugin: HelloPlugin
  let context: ReturnType<typeof createMockContext>
 
  beforeEach(() => {
    context = createMockContext()
    plugin = new HelloPlugin()
  })
 
  it('should activate successfully', async () => {
    await plugin.activate(context)
    expect(context.subscriptions.length).toBeGreaterThan(0)
  })
 
  it('should register commands', async () => {
    await plugin.activate(context)
 
    const commands = context.api.commands.getAll()
    expect(commands).toContain('helloPlugin.sayHello')
    expect(commands).toContain('helloPlugin.insertGreeting')
  })
 
  it('should say hello', async () => {
    await plugin.activate(context)
 
    await context.api.commands.execute('helloPlugin.sayHello')
 
    const notifications = context.api.ui.getNotifications()
    expect(notifications.length).toBe(1)
    expect(notifications[0].message).toContain('Hello')
  })
 
  it('should insert greeting into editor', async () => {
    await plugin.activate(context)
 
    const editor = context.api.editor.getActiveEditor()
    await context.api.commands.execute('helloPlugin.insertGreeting')
 
    const content = await editor.getContent()
    expect(content).toContain('Hello')
  })
 
  it('should load settings', async () => {
    await plugin.activate(context)
 
    await context.api.config.set('helloPlugin.greeting', 'Hi')
    await plugin.activate(context)
 
    const greeting = await context.api.config.get('helloPlugin.greeting')
    expect(greeting).toBe('Hi')
  })
 
  it('should deactivate cleanly', async () => {
    await plugin.activate(context)
    await plugin.deactivate()
 
    expect(context.subscriptions.every(s => s.disposed)).toBe(true)
  })
})

Integration Testing

Create test/integration.test.ts:

import { describe, it, expect } from '@jest/globals'
import { createTestEnvironment } from '@lokus/plugin-sdk/testing'
 
describe('HelloPlugin Integration', () => {
  it('should work end-to-end', async () => {
    const env = await createTestEnvironment()
 
    // Load plugin
    await env.loadPlugin('./dist/index.js')
 
    // Execute command
    await env.executeCommand('helloPlugin.sayHello')
 
    // Verify notification
    const notifications = env.getNotifications()
    expect(notifications[0].message).toContain('Hello')
 
    // Clean up
    await env.dispose()
  })
})

Running Tests

# Run all tests
npm test
 
# Watch mode
npm run test:watch
 
# With coverage
npm run test:coverage
 
# Specific test file
npm test -- index.test.ts

Assets and Styles

Using Static Assets

Plugins can load images, fonts, and other static assets from their directory. Use context.assetUri to resolve paths:

export default definePlugin({
  activate(context) {
    // Construct asset URL
    const iconUrl = `${context.assetUri}/assets/icon.png`;
    
    context.api.ui.registerPanel({
      // ...
      html: `<img src="${iconUrl}" />`
    });
  }
});

Styling

Lokus automatically loads style.css, styles.css, or index.css from your plugin root.

/* style.css */
.my-plugin-container {
  padding: 20px;
  background: var(--background-secondary);
  color: var(--text-primary);
}

Debugging Plugins

Enable Debug Mode

Add to plugin.json:

{
  "dev": {
    "debug": true,
    "sourceMaps": true,
    "verboseLogging": true,
    "hotReload": true
  }
}

Using Console Logging

export default class MyPlugin extends BasePlugin {
  async activate(context: PluginContext) {
    // Use built-in logging (with BasePlugin)
    this.log('debug', 'Detailed debug information')
    this.log('info', 'General information')
    this.log('warn', 'Warning message')
    this.log('error', 'Error occurred', error)
 
    // Or use console directly
    console.log('[MyPlugin] Debug:', data)
    console.error('[MyPlugin] Error:', error)
  }
}

Using Breakpoints

  1. Start development mode:

    lokus-plugin dev --debug
  2. Open Lokus Developer Tools:

    • View → Developer → Toggle Developer Tools
    • Or press Ctrl/Cmd + Shift + I
  3. Navigate to Sources tab

  4. Find your plugin code in the sources tree

  5. Set breakpoints by clicking line numbers

  6. Trigger plugin functionality to hit breakpoints

Performance Profiling

export default class MyPlugin extends BasePlugin {
  async activate(context: PluginContext) {
    await this.initialize(context)
 
    // Profile a function
    this.profileAsync('loadData', async () => {
      await this.loadData()
    })
 
    // Manual timing
    const start = performance.now()
    await this.heavyOperation()
    const end = performance.now()
    this.log('info', `Operation took ${end - start}ms`)
  }
}

Debug Configuration (VS Code)

Create .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Plugin",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/node_modules/@lokus/plugin-cli/bin/lokus-plugin",
      "args": ["dev", "--debug"],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal",
      "sourceMaps": true,
      "outFiles": ["${workspaceFolder}/dist/**/*.js"]
    }
  ]
}

Best Practices

1. Use TypeScript

// ✅ Good - Type safety
interface PluginSettings {
  enabled: boolean
  maxItems: number
}
 
async function loadSettings(): Promise<PluginSettings> {
  return {
    enabled: await this.getSetting('enabled', true),
    maxItems: await this.getSetting('maxItems', 10)
  }
}
 
// ❌ Avoid - No type safety
async function loadSettings() {
  return {
    enabled: await this.getSetting('enabled'),
    maxItems: await this.getSetting('maxItems')
  }
}

2. Handle Errors Gracefully

// ✅ Good - Comprehensive error handling
async insertContent(content: string): Promise<void> {
  try {
    const editor = await this.api?.editor.getActiveEditor()
 
    if (!editor) {
      throw new Error('No active editor')
    }
 
    await editor.insertContent(content)
    this.log('info', 'Content inserted successfully')
 
  } catch (error) {
    this.handleError(error as Error, 'Failed to insert content')
 
    this.api?.ui.showNotification({
      message: 'Failed to insert content. Please try again.',
      type: 'error'
    })
  }
}
 
// ❌ Avoid - Unhandled errors
async insertContent(content: string) {
  const editor = await this.api.editor.getActiveEditor()
  await editor.insertContent(content)
}

3. Clean Up Resources

// ✅ Good - Proper cleanup
export default class MyPlugin extends BasePlugin {
  private timers: NodeJS.Timeout[] = []
  private watchers: Disposable[] = []
 
  async activate(context: PluginContext) {
    await this.initialize(context)
 
    // Create timer
    const timer = setInterval(() => this.check(), 60000)
    this.timers.push(timer)
 
    // Create watcher
    const watcher = await this.api?.fs.watch('/path', () => {})
    if (watcher) this.watchers.push(watcher)
  }
 
  async deactivate() {
    // Clean up timers
    this.timers.forEach(timer => clearInterval(timer))
    this.timers = []
 
    // Dispose watchers
    this.watchers.forEach(watcher => watcher.dispose())
    this.watchers = []
  }
}
 
// ❌ Avoid - Resource leaks
export default class MyPlugin extends BasePlugin {
  async activate(context: PluginContext) {
    setInterval(() => this.check(), 60000) // Never cleaned up
    await this.api.fs.watch('/path', () => {}) // Never disposed
  }
}

4. Use Async/Await Properly

// ✅ Good - Proper async handling
async function loadAllData(): Promise<Data[]> {
  // Parallel execution
  const [users, posts, comments] = await Promise.all([
    this.fetchUsers(),
    this.fetchPosts(),
    this.fetchComments()
  ])
 
  return this.combineData(users, posts, comments)
}
 
// ❌ Avoid - Sequential when parallel is possible
async function loadAllData() {
  const users = await this.fetchUsers()
  const posts = await this.fetchPosts()
  const comments = await this.fetchComments()
 
  return this.combineData(users, posts, comments)
}

5. Optimize Performance

// ✅ Good - Debounced, lazy loading
export default class MyPlugin extends BasePlugin {
  private updateDebounced = this.debounce(
    () => this.updateUI(),
    300
  )
 
  async activate(context: PluginContext) {
    await this.initialize(context)
 
    // Lazy load heavy dependencies
    this.onEvent('editor:focus', async () => {
      if (!this.heavyModule) {
        this.heavyModule = await import('./heavy-module')
      }
    })
 
    // Debounce frequent updates
    this.onEvent('editor:change', () => {
      this.updateDebounced()
    })
  }
}
 
// ❌ Avoid - Load everything upfront
export default class MyPlugin extends BasePlugin {
  async activate(context: PluginContext) {
    // Loads large library immediately
    const heavy = await import('./heavy-module')
 
    // Updates on every keystroke
    this.onEvent('editor:change', () => {
      this.updateUI()
    })
  }
}

6. Document Your Code

/**
 * Analyzes text content and returns statistics
 *
 * @param text - The text to analyze
 * @param options - Analysis options
 * @returns Analysis results including word count, reading time, etc.
 * @throws {Error} If text is empty or invalid
 *
 * @example
 * ```typescript
 * const stats = await analyzeText('Hello world', {
 *   includeReadingTime: true
 * })
 * console.log(stats.wordCount) // 2
 * ```
 */
async analyzeText(
  text: string,
  options: AnalysisOptions = {}
): Promise<AnalysisResult> {
  if (!text || text.trim().length === 0) {
    throw new Error('Text cannot be empty')
  }
 
  // Implementation...
}

7. Version Your Plugin Properly

Follow semantic versioning:

  • MAJOR (1.0.0 → 2.0.0): Breaking changes
  • MINOR (1.0.0 → 1.1.0): New features, backward compatible
  • PATCH (1.0.0 → 1.0.1): Bug fixes, backward compatible
{
  "version": "1.2.3",
  "changelog": {
    "1.2.3": "Fixed issue with panel not loading",
    "1.2.0": "Added new command for bulk operations",
    "1.1.0": "Added configuration options",
    "1.0.0": "Initial release"
  }
}

Security Best Practices

1. Validate Input

// ✅ Good - Input validation
async function processUserInput(input: string): Promise<void> {
  // Validate input
  if (!input || typeof input !== 'string') {
    throw new Error('Invalid input')
  }
 
  // Sanitize input
  const sanitized = input.trim().slice(0, 1000)
 
  // Validate path if it's a file path
  if (this.isFilePath(input)) {
    if (!this.isValidPath(sanitized)) {
      throw new Error('Invalid file path')
    }
  }
 
  await this.process(sanitized)
}
 
// ❌ Avoid - No validation
async function processUserInput(input: any) {
  await this.process(input)
}

2. Use Permissions Correctly

// ✅ Good - Check permissions
async function writeToFile(path: string, content: string): Promise<void> {
  // Check if we have permission
  if (!this.hasPermission('filesystem:write')) {
    throw new Error('No permission to write files')
  }
 
  // Validate path
  if (!this.isValidPath(path)) {
    throw new Error('Invalid file path')
  }
 
  await this.api?.fs.writeFile(path, content)
}
 
// ❌ Avoid - No permission check
async function writeToFile(path: string, content: string) {
  await this.api.fs.writeFile(path, content)
}

3. Sanitize HTML

// ✅ Good - Sanitize HTML
function createPanelHTML(userContent: string): string {
  // Escape HTML entities
  const escaped = userContent
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;')
 
  return `<div>${escaped}</div>`
}
 
// ❌ Avoid - XSS vulnerability
function createPanelHTML(userContent: string) {
  return `<div>${userContent}</div>`
}

4. Secure API Keys

// ✅ Good - Use secret storage
async function getAPIKey(): Promise<string> {
  // Store in secure storage
  const key = await this.api?.storage.getSecret('apiKey')
 
  if (!key) {
    throw new Error('API key not configured')
  }
 
  return key
}
 
async function setAPIKey(key: string): Promise<void> {
  // Validate key format
  if (!this.isValidAPIKey(key)) {
    throw new Error('Invalid API key format')
  }
 
  // Store securely
  await this.api?.storage.setSecret('apiKey', key)
}
 
// ❌ Avoid - Plain text storage
async function getAPIKey() {
  return await this.api.storage.get('apiKey')
}

5. Rate Limit API Calls

// ✅ Good - Rate limiting
export default class MyPlugin extends BasePlugin {
  private rateLimiter = this.createRateLimiter({
    maxRequests: 10,
    windowMs: 60000 // 1 minute
  })
 
  async fetchData(url: string): Promise<any> {
    // Check rate limit
    if (!this.rateLimiter.tryAcquire()) {
      throw new Error('Rate limit exceeded. Please try again later.')
    }
 
    return await this.api?.network.fetch(url)
  }
}
 
// ❌ Avoid - No rate limiting
async fetchData(url: string) {
  return await this.api.network.fetch(url)
}

Publishing Your Plugin

1. Prepare for Publishing

# Validate plugin
npm run validate
 
# Run tests
npm test
 
# Build for production
npm run build
 
# Package plugin
npm run package

2. Create Documentation

Create comprehensive README.md:

# My Plugin
 
Brief description of what your plugin does.
 
## Features
 
- Feature 1
- Feature 2
- Feature 3
 
## Installation
 
1. Open Lokus
2. Go to Settings → Plugins
3. Search for "My Plugin"
4. Click Install
 
## Usage
 
### Basic Usage
 
Description and examples...
 
### Advanced Features
 
More complex usage patterns...
 
## Configuration
 
Available settings:
 
- `myPlugin.setting1` - Description
- `myPlugin.setting2` - Description
 
## Commands
 
- `myPlugin.command1` - Description
- `myPlugin.command2` - Description
 
## Keyboard Shortcuts
 
- `Ctrl+Shift+X` - Command 1
- `Ctrl+Shift+Y` - Command 2
 
## Troubleshooting
 
Common issues and solutions...
 
## Contributing
 
How to contribute to the plugin...
 
## License
 
MIT

3. Publish to Registry

# Login to registry
lokus-plugin login --token YOUR_TOKEN
 
# Publish plugin
lokus-plugin publish
 
# Publish beta version
lokus-plugin publish --tag beta

4. Version Management

Update version in plugin.json:

{
  "version": "1.1.0",
  "changelog": {
    "1.1.0": {
      "date": "2025-11-12",
      "changes": [
        "Added new feature X",
        "Fixed bug Y",
        "Improved performance of Z"
      ]
    }
  }
}

5. Continuous Integration

Create .github/workflows/publish.yml:

name: Publish Plugin
 
on:
  release:
    types: [published]
 
jobs:
  publish:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v3
 
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
 
      - name: Install dependencies
        run: npm install
 
      - name: Run tests
        run: npm test
 
      - name: Build
        run: npm run build
 
      - name: Validate
        run: npm run validate
 
      - name: Publish
        run: npm run publish
        env:
          LOKUS_PLUGIN_TOKEN: ${{ secrets.LOKUS_PLUGIN_TOKEN }}

Common Patterns

Singleton Pattern

export default class SingletonPlugin extends BasePlugin {
  private static instance: SingletonPlugin
 
  static getInstance(): SingletonPlugin {
    return SingletonPlugin.instance
  }
 
  async activate(context: PluginContext) {
    await this.initialize(context)
    SingletonPlugin.instance = this
  }
}

Event Bus Pattern

export default class EventBusPlugin extends BasePlugin {
  private eventBus = new EventEmitter()
 
  async activate(context: PluginContext) {
    await this.initialize(context)
 
    // Subscribe to events
    this.eventBus.on('custom:event', this.handleEvent)
 
    // Emit events
    this.eventBus.emit('custom:event', { data: 'value' })
  }
}

Command Pattern

interface Command {
  execute(): Promise<void>
  undo(): Promise<void>
}
 
class InsertTextCommand implements Command {
  constructor(
    private editor: Editor,
    private text: string
  ) {}
 
  async execute() {
    await this.editor.insertContent(this.text)
  }
 
  async undo() {
    // Undo logic
  }
}
 
export default class CommandPlugin extends BasePlugin {
  private commandHistory: Command[] = []
 
  async executeCommand(command: Command) {
    await command.execute()
    this.commandHistory.push(command)
  }
 
  async undo() {
    const command = this.commandHistory.pop()
    if (command) {
      await command.undo()
    }
  }
}

Troubleshooting

Plugin Won’t Activate

Check:

  • Manifest is valid JSON
  • All required fields are present
  • Permissions are declared
  • No syntax errors in code
  • Dependencies are installed

Solution:

lokus-plugin validate
lokus-plugin dev --verbose

Hot Reload Not Working

Check:

  • Dev mode is enabled
  • File watcher is running
  • No compilation errors

Solution:

lokus-plugin dev --clean

Build Errors

Check:

  • TypeScript configuration is correct
  • All imports are valid
  • Types are properly defined

Solution:

npm run clean
npm install
npm run build

Performance Issues

Check:

  • Not loading heavy resources on startup
  • Debouncing frequent operations
  • Cleaning up resources properly

Solution:

  • Use lazy loading
  • Implement debouncing
  • Profile with performance tools

Next Steps

Now that you know how to build plugins, explore:

Resources

Support

Need help? Here’s how to get support:

Happy plugin development!