Getting Started

This guide will walk you through creating your first Lokus plugin from scratch. By the end, you’ll have a working plugin that adds custom commands and interacts with the editor.

Prerequisites

Before you begin, ensure you have:

  • Node.js 18+ installed
  • npm package manager
  • Lokus installed on your system
  • Basic knowledge of TypeScript
  • A code editor (VS Code recommended)

Quick Start

The fastest way to create a new plugin is using the Lokus Plugin CLI:

# Install the CLI globally (one-time setup)
npm install -g @lokus/plugin-cli
 
# Create a new plugin with interactive prompts
lokus-plugin create my-first-plugin
 
# Or use quick mode with defaults
lokus-plugin create my-first-plugin --skip-prompts --template basic-typescript
 
# Navigate to the plugin directory
cd my-first-plugin
 
# Install dependencies
npm install
 
# Build the plugin
npm run build
 
# Link for development (creates symlink in ~/.lokus/plugins)
lokus-plugin link
 
# Validate the plugin
lokus-plugin validate

Note: The CLI supports both @lokus/plugin-cli and lokus-plugin-cli package names for compatibility.

Available Templates

When creating a plugin, you can choose from these templates:

TemplateDescription
basic-typescriptSimple TypeScript plugin with commands
basic-pluginMinimal plugin structure
react-ui-panelPlugin with React-based UI panel
ui-extension-pluginPlugin with toolbar and status bar items
language-support-pluginPlugin for language/syntax support

Use with: npx lokus-plugin create my-plugin --template react-ui-panel

Project Structure

Here’s what gets generated:

my-first-plugin/
├── src/
│   └── index.ts          # Plugin entry point
├── dist/                  # Compiled output (after build)
│   └── index.js
├── test/
│   ├── index.test.ts     # Unit tests
│   └── setup.ts          # Test setup
├── package.json          # npm configuration
├── plugin.json           # Plugin manifest
├── tsconfig.json         # TypeScript configuration
├── esbuild.config.js     # Build configuration
└── README.md

Understanding the Manifest

The plugin.json file defines your plugin’s metadata and capabilities:

{
  "manifest": "2.0",
  "id": "my-first-plugin",
  "name": "my-first-plugin",
  "displayName": "My First Plugin",
  "version": "0.1.0",
  "description": "A Lokus plugin",
  "author": "Your Name",
  "license": "MIT",
  "main": "./dist/index.js",
  "lokusVersion": ">=1.0.0",
  "permissions": [
    "editor:read",
    "editor:write",
    "commands:register",
    "ui:notifications"
  ],
  "activationEvents": ["onStartup"]
}

Key Manifest Fields

FieldRequiredDescription
idYesUnique identifier (lowercase, hyphens)
nameYesPlugin name
versionYesSemantic version (e.g., “1.0.0”)
mainYesPath to compiled entry point
permissionsNoRequired API permissions
activationEventsNoWhen to activate the plugin

Creating Your First Plugin

Step 1: Define the Plugin Class

Edit src/index.ts:

import { PluginContext } from 'lokus-plugin-sdk';
 
export default class MyFirstPlugin {
  private context: PluginContext;
  private logger: PluginContext['logger'];
 
  constructor(context: PluginContext) {
    this.context = context;
    this.logger = context.logger;
    this.logger.info('MyFirstPlugin initialized');
  }
 
  async activate(activationContext: any): Promise<void> {
    this.logger.info('Activating MyFirstPlugin...');
 
    try {
      // Register a command
      activationContext.commands.registerCommand(
        'myFirstPlugin.sayHello',
        {
          name: 'Say Hello',
          description: 'Shows a hello notification',
          callback: () => {
            this.context.ui.showInformationMessage('Hello from MyFirstPlugin!');
            this.logger.info('Hello command executed');
          }
        }
      );
 
      this.logger.info('MyFirstPlugin activated successfully');
    } catch (error) {
      this.logger.error('Failed to activate plugin:', error);
      throw error;
    }
  }
 
  async deactivate(): Promise<void> {
    this.logger.info('Deactivating MyFirstPlugin...');
    // Cleanup resources here
    this.logger.info('MyFirstPlugin deactivated');
  }
}

Step 2: Understanding the Plugin Context

When your plugin is instantiated, it receives a PluginContext with access to all 14 Lokus APIs:

interface PluginContext {
  // Plugin metadata
  pluginId: string;
  pluginPath: string;
 
  // Logging
  logger: {
    info(message: string, ...args: any[]): void;
    warn(message: string, ...args: any[]): void;
    error(message: string, ...args: any[]): void;
    debug(message: string, ...args: any[]): void;
  };
 
  // Core APIs (always available)
  editor: EditorAPI;        // 95+ methods for TipTap editor
  ui: UIAPI;                // Panels, dialogs, notifications
  commands: CommandsAPI;     // Command registration
  workspace: WorkspaceAPI;   // File and editor access
  config: ConfigurationAPI;  // Plugin settings
 
  // Language & Features
  languages: LanguagesAPI;   // Completion, hover, etc.
 
  // Terminal & Tasks
  terminal: TerminalAPI;     // Terminal management
  tasks: TaskAPI;            // Task execution
 
  // Debugging & Themes
  debug: DebugAPI;           // Debug sessions
  themes: ThemeAPI;          // Theme management
 
  // Storage & Files
  storage: StorageAPI;       // Key-value storage
  fs: FilesystemAPI;         // File operations
 
  // Network & System
  network: NetworkAPI;       // HTTP requests (requires permission)
  clipboard: ClipboardAPI;   // Clipboard access
}

See API Reference for complete documentation of all APIs.

Step 3: Using the Activation Context

The activate method receives an activation context with command registration:

async activate(activationContext: any): Promise<void> {
  // Register commands
  activationContext.commands.registerCommand(
    'commandId',
    {
      name: 'Display Name',
      description: 'What this command does',
      callback: () => {
        // Command implementation
      }
    }
  );
}

Step 4: Build and Test

# Build the plugin
npm run build
 
# Run tests
npm test
 
# Validate manifest
npx lokus-plugin validate

Development Workflow

# Creates symlink in ~/.lokus/plugins/
npx lokus-plugin link

2. Rebuild on Changes

# Watch mode (if configured)
npm run dev
 
# Or manual rebuild
npm run build

3. Restart Lokus

After rebuilding, restart Lokus to load the updated plugin.

4. Check Logs

Open Lokus Developer Tools (View → Developer → Toggle Developer Tools) to see plugin logs.

Working with the Editor API

The EditorAPI provides 95+ methods for deep TipTap editor integration:

async activate(activationContext: any): Promise<void> {
  const editor = this.context.editor;
 
  // Add a slash command
  await editor.addSlashCommand({
    name: 'my-command',
    description: 'Insert custom content',
    icon: '🎉',
    execute: async () => {
      await editor.insertNode('paragraph', {}, 'Hello World!');
    }
  });
 
  // Add a toolbar button
  editor.addToolbarItem({
    id: 'my-toolbar-button',
    title: 'My Button',
    icon: 'star',
    handler: () => {
      this.context.ui.showInformationMessage('Toolbar button clicked!');
    }
  });
 
  // Register custom TipTap extension
  await editor.registerExtension({
    name: 'customNode',
    type: 'node',
    schema: {
      attrs: { data: { default: null } },
      content: 'inline*',
      group: 'block',
      parseDOM: [{ tag: 'div.custom' }],
      toDOM: (node) => ['div', { class: 'custom' }, 0]
    }
  });
 
  // Get current selection
  const selection = await editor.getSelection();
  if (selection) {
    this.context.logger.info('Selection:', selection);
  }
}

Key Editor Methods

The EditorAPI includes comprehensive methods for:

  • Extensions: registerExtension(), registerNode(), registerMark()
  • Slash Commands: addSlashCommand(), removeSlashCommand()
  • UI Elements: addToolbarItem(), addContextMenuItem(), addKeyboardShortcut()
  • Content Manipulation: insertNode(), replaceSelection(), getText(), getSelection()
  • Node Views: registerNodeView() for custom rendering
  • Input Rules: registerInputRule() for automatic transformations
  • Format Detection: registerFormat() for language support

See EditorAPI Reference for complete documentation of all 95+ methods.

Writing Tests

Create test/index.test.ts:

import { createMockPluginContext, createMockActivationContext } from 'lokus-plugin-sdk/testing';
import MyFirstPlugin from '../src/index';
 
describe('MyFirstPlugin', () => {
  let plugin: MyFirstPlugin;
  let mockContext: any;
  let mockActivationContext: any;
 
  beforeEach(() => {
    mockContext = createMockPluginContext();
    mockActivationContext = createMockActivationContext();
    plugin = new MyFirstPlugin(mockContext);
  });
 
  afterEach(async () => {
    await plugin.deactivate();
  });
 
  it('should initialize', () => {
    expect(plugin).toBeDefined();
    expect(mockContext.logger.info).toHaveBeenCalledWith('MyFirstPlugin initialized');
  });
 
  it('should activate and register commands', async () => {
    await plugin.activate(mockActivationContext);
 
    expect(mockActivationContext.commands.registerCommand).toHaveBeenCalledWith(
      'myFirstPlugin.sayHello',
      expect.any(Object)
    );
  });
 
  it('should execute hello command', async () => {
    await plugin.activate(mockActivationContext);
 
    // Get the callback that was registered
    const [, options] = mockActivationContext.commands.registerCommand.mock.calls[0];
    options.callback();
 
    expect(mockContext.ui.showInformationMessage).toHaveBeenCalledWith(
      'Hello from MyFirstPlugin!'
    );
  });
});

Run tests:

npm test

Packaging and Publishing

Package Your Plugin

# Validate first
npx lokus-plugin validate
 
# Build
npm run build
 
# Package (creates .zip file)
npx lokus-plugin package

Publish to Registry

# Login (first time only)
npx lokus-plugin login
 
# Publish
npx lokus-plugin publish

See Publishing Guide for more details.

Troubleshooting

Plugin Won’t Load

  1. Check the manifest:

    npx lokus-plugin validate
  2. Verify the build output exists:

    ls dist/index.js
  3. Check symlink:

    ls -la ~/.lokus/plugins/

Commands Not Appearing

  1. Ensure command is registered in activate() method
  2. Check browser console for errors
  3. Verify permissions includes commands:register

Build Errors

  1. TypeScript errors:

    npx tsc --noEmit
  2. Missing dependencies:

    npm install

Explore More APIs

Now that you have a basic plugin, explore the other powerful APIs:

Terminal & Task Integration

// Create a terminal
const terminal = this.context.terminal.createTerminal({
  name: 'Build',
  cwd: this.context.workspace?.rootPath
});
terminal.sendText('npm run build');

See TerminalAPI and TaskAPI.

UI Components

// Create a status bar item
const statusBar = this.context.ui.createStatusBarItem('left', 100);
statusBar.text = 'My Plugin Active';
statusBar.show();
 
// Show progress indicator
await this.context.ui.withProgress(
  { title: 'Processing...' },
  async (progress) => {
    progress.report({ message: 'Step 1' });
    await doWork();
  }
);

See UIAPI for dialogs, panels, tree views, and more.

Language Features

// Add completion provider
this.context.languages.registerCompletionProvider('markdown', {
  async provideCompletionItems(document, position) {
    return [
      { label: 'todo', insertText: '- [ ] ', detail: 'Todo item' }
    ];
  }
});

See LanguagesAPI for hover, formatting, and code actions.

Next Steps

Core Documentation

Learn by Example

Publishing