Skip to content

Creating Plugins

This guide walks through creating a Lokus plugin from scratch, covering the SDK, project structure, manifest format, lifecycle hooks, permissions, and testing.

  • Node.js 16+
  • npm 8+
  • TypeScript 4.5+ (recommended)

Install the Lokus plugin SDK and CLI:

Terminal window
npm install lokus-plugin-sdk
npm install -g lokus-plugin-cli

The CLI generates a ready-to-build project:

Terminal window
lokus-plugin create my-plugin

This creates a directory with the following structure:

my-plugin/
plugin.json # Plugin manifest
src/
index.js # Entry point (activate/deactivate)
dist/ # Build output
package.json
tsconfig.json # If using TypeScript

For TypeScript projects, the CLI scaffolds .ts source files with full type definitions from the SDK.

Every plugin needs a plugin.json at its root. This file defines metadata, permissions, activation triggers, and contribution points.

In Tauri desktop mode, only plugin.json is read. The manifest.json fallback is only available in browser mode (via PluginManager). For maximum compatibility, always include plugin.json.

{
"id": "my-word-count",
"name": "Word Count",
"version": "1.0.0",
"description": "Displays word count in the status bar",
"main": "./dist/index.js",
"lokusVersion": "^1.0.0",
"author": "Your Name",
"license": "ISC",
"permissions": ["editor:read", "ui:create"],
"activationEvents": ["onStartup"],
"categories": ["Editor"]
}

The v2 format adds manifest, engines, publisher, and capabilities fields. Use "manifest": "2.0" and replace lokusVersion with engines.lokus:

{
"manifest": "2.0",
"id": "my-word-count",
"name": "Word Count",
"displayName": "Word Count Plugin",
"version": "1.0.0",
"publisher": "your-name",
"main": "./dist/index.js",
"engines": { "lokus": "^1.0.0" },
"permissions": ["editor:read", "ui:create"],
"activationEvents": ["onStartup"],
"categories": ["Editor"]
}
FieldTypeDescription
idstringUnique identifier. Lowercase, letters, numbers, hyphens. Cannot start with lokus.
namestringHuman-readable name
versionstringSemver format (1.0.0)
mainstringPath to compiled entry point (.js or .mjs)
lokusVersionstringCompatible Lokus version range (v1), or use engines.lokus (v2)

Control when your plugin loads:

{
"activationEvents": [
"onStartup",
"onStartupFinished",
"onCommand:myPlugin.hello",
"onLanguage:markdown",
"onView:myPlugin.sidebar",
"workspaceContains:**/*.custom",
"onFileSystem:myScheme",
"onUri",
"onDebug:node",
"onSearch:myProvider",
"onWebviewPanel:myPlugin.preview",
"onCustomEditor:myPlugin.editor",
"onAuthenticationRequest:myPlugin.auth",
"*"
]
}
EventTrigger
onStartupApp launches (use sparingly — impacts startup)
onStartupFinishedApp has finished launching
*Activate for all events (wildcard)
onCommand:idA specific command is executed
onLanguage:langA file with that language ID opens
onView:viewIdA specific view is opened
workspaceContains:patternWorkspace has files matching the glob
onFileSystem:schemeA file system scheme is accessed
onUriA URI is handled by the plugin
onDebug:typeA debug session of the given type starts
onSearch:providerA search provider is requested
onWebviewPanel:viewTypeA webview panel is opened
onCustomEditor:viewTypeA custom editor is opened
onAuthenticationRequest:idAn authentication request is made

Declare what your plugin adds to Lokus in the contributes section:

{
"contributes": {
"commands": [
{
"command": "myPlugin.countWords",
"title": "Count Words",
"category": "Word Count"
}
],
"keybindings": [
{
"command": "myPlugin.countWords",
"key": "ctrl+shift+w",
"mac": "cmd+shift+w"
}
],
"configuration": {
"title": "Word Count",
"properties": {
"wordCount.enabled": {
"type": "boolean",
"default": true,
"description": "Enable word counting"
}
}
},
"customEditors": [],
"webviews": []
}
}

Your main file must export a class with activate() and optionally deactivate() methods:

export default class WordCountPlugin {
constructor(context) {
this.api = context.api;
this.logger = context.logger;
}
async activate() {
this.api.commands.register({
id: 'wordCount.count',
title: 'Count Words',
handler: () => this.countWords()
});
this.api.ui.registerStatusBarItem({
id: 'wordCount.status',
text: 'Words: 0',
tooltip: 'Click to count words',
alignment: 'right',
priority: 100
});
this.api.editor.onUpdate(() => this.updateCount());
}
async deactivate() {
// Cleanup happens automatically for API-registered resources
}
async countWords() {
const text = await this.api.editor.getText();
const count = text.trim().split(/\s+/).filter(Boolean).length;
this.api.ui.showInformationMessage(`Word count: ${count}`);
}
}

The SDK provides definePlugin() and createPlugin() for a simpler functional style:

import { definePlugin } from 'lokus-plugin-sdk';
export default definePlugin({
async activate(context) {
const { api } = context;
api.commands.register({
id: 'hello.sayHello',
title: 'Say Hello',
handler: () => {
api.ui.showInformationMessage('Hello from my plugin!');
}
});
},
async deactivate() {
// Optional cleanup
}
});

For more structure, extend BasePlugin. The SDK provides built-in logger, config manager, disposable tracking, and utility methods (safeExecute, debounce, throttle, retry, measurePerformance):

import { BasePlugin, PluginContext } from 'lokus-plugin-sdk';
export default class MyPlugin extends BasePlugin {
async activate(context: PluginContext) {
await this.initialize(context);
this.markActivationTime();
// Use built-in helpers
this.registerCommand('myPlugin.hello', () => {
this.showNotification('Hello!', 'info');
});
}
}

For plugins that need command palette integration, status bar items, and quick picks, extend EnhancedBasePlugin:

import { EnhancedBasePlugin, PluginContext } from 'lokus-plugin-sdk';
export default class MyPlugin extends EnhancedBasePlugin {
async activate(context: PluginContext) {
await this.initialize(context);
// Register command with palette visibility
this.registerCommandWithPalette({
id: 'myPlugin.action',
title: 'My Action',
category: 'My Plugin',
handler: () => { /* ... */ }
});
// Create a status bar item
this.createStatusBarItem('myPlugin.status', 'Ready', {
tooltip: 'Plugin status',
alignment: 'left',
priority: 100
});
}
}

Plugins move through these states:

discovered -> loaded -> active
| |
error error

The four lifecycle states are:

StateDescription
discoveredPlugin folder found and manifest validated
loadedPlugin module imported and initialized
activePlugin activate() called successfully
errorAn error occurred during loading or activation

Your constructor receives a PluginContext object:

interface PluginContext {
pluginId: string; // Your plugin's ID
manifest: PluginManifest; // Parsed manifest
api: LokusPluginAPI; // Full API access
logger: Logger; // Scoped logger
commands: CommandsAPI; // Shortcut to api.commands
ui: UIAPI; // Shortcut to api.ui
}

When your plugin deactivates, Lokus automatically cleans up:

  • Registered commands
  • Editor extensions, slash commands, toolbar items
  • UI panels, status bar items, webviews
  • Tree data providers
  • Menu and toolbar contributions
  • Output channels
  • Notifications
  • Terminal instances

For resources not tracked by the API, clean them up in deactivate():

async deactivate() {
clearInterval(this.syncTimer);
this.cache.clear();
await this.saveState();
}

Lokus automatically loads CSS stylesheets from your plugin directory. Place a style.css, index.css, or styles.css file in your plugin root, and it will be loaded when the plugin is activated. No manifest configuration is needed.

Plugins run inside a sandboxed environment (PluginSandbox) that provides:

  • Isolated execution via Web Workers (when available)
  • Resource quotas: 50 MB memory limit, 1 second CPU time per task, 30 second network timeout, 10 MB max file access
  • Permission-based API access: only manifest-declared permissions are granted
  • Code signing verification (optional, when requireSignature is enabled)
  • Runtime monitoring: memory usage, CPU time, network requests, and API call tracking

The security manager (PluginSecurityManager) enforces these constraints and can terminate plugins that exceed their quotas.

Declare every permission your plugin needs. The runtime enforces these — calling an API without the matching permission throws PermissionDeniedError.

{
"permissions": [
"editor:read",
"editor:write",
"ui:create",
"ui:notifications",
"commands:register",
"storage:read",
"storage:write"
]
}
CategoryPermissions
Editoreditor:read, editor:write
UIui:create, ui:notifications, ui:dialogs, ui:menus, ui:toolbars
Filesystemfilesystem:read, filesystem:write
Workspaceworkspace:read, workspace:write
Commandscommands:register, commands:execute, commands:list
Networknetwork:http
Storagestorage:read, storage:write
Clipboardclipboard:read, clipboard:write
Terminalterminal:create, terminal:write, terminal:read
Languageslanguages:register, languages:read
Themesthemes:register, themes:read, themes:set
Configconfig:read, config:write
Debugdebug:session, debug:register

Request only what you need. Plugins requesting all are flagged during security scanning.

Lokus includes a built-in MCP (Model Context Protocol) plugin system for AI-integrated plugins. The system consists of:

  • MCPPluginManager: manages MCP-enabled plugin lifecycle with states (discovered, loading, loaded, initializing, active, error, disposed)
  • MCPProtocol: handles JSON-RPC communication between MCP servers and clients
  • MCPClient: connects to external MCP servers
  • MCPServerHost: hosts MCP servers within Lokus

MCP plugins can be typed as mcp-server, mcp-client, or mcp-hybrid. They register resources, tools, and prompts in global registries accessible across the plugin system.

To create an MCP plugin, set the type in your manifest and use the onMCPServer:id activation event:

{
"id": "my-mcp-plugin",
"type": "mcp-server",
"activationEvents": ["onMCPServer:my-mcp-plugin"],
"permissions": ["editor:read", "commands:register"]
}

Build with the CLI (lokus-plugin build) or add scripts to package.json:

{
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"validate": "lokus-plugin validate"
}
}

Run lokus-plugin validate before publishing to check manifest fields, version format, and permissions.

import { createMockContext } from 'lokus-plugin-sdk/testing';
describe('WordCountPlugin', () => {
it('activates without error', async () => {
const context = createMockContext();
const plugin = new WordCountPlugin(context);
await plugin.activate();
});
});
Terminal window
lokus-plugin dev # Watch mode with hot reload
lokus-plugin link # Symlink to ~/.lokus/plugins/ for live testing
lokus-plugin package # Create .tgz for distribution
lokus-plugin publish # Upload to registry (run lokus-plugin login first)

The following commands are registered and functional in the CLI:

CommandDescription
lokus-plugin create <name>Scaffold a new plugin project
lokus-plugin buildBuild the plugin
lokus-plugin devStart development mode with hot reload
lokus-plugin validateValidate the plugin manifest
lokus-plugin packageCreate a distributable package
lokus-plugin publishPublish to a registry
lokus-plugin linkSymlink plugin for local development
lokus-plugin loginAuthenticate with the registry
lokus-plugin testRun plugin tests
lokus-plugin docsGenerate plugin documentation

A plugin that adds a /date slash command:

src/index.js
export default class InsertDatePlugin {
constructor(context) { this.api = context.api; }
async activate() {
await this.api.editor.addSlashCommand({
name: 'date',
description: 'Insert current date',
icon: 'calendar',
execute: () => {
this.api.editor.replaceSelection(new Date().toLocaleDateString());
}
});
}
async deactivate() {}
}

Set "permissions": ["editor:write"] in plugin.json, build, copy to ~/.lokus/plugins/, and restart Lokus.