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 (v1) or manifest.json (v2) at its root. This file defines metadata, permissions, activation triggers, and contribution points.

{
"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"]
}

Lokus tries manifest.json first, then falls back to plugin.json.

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",
"onCommand:myPlugin.hello",
"onLanguage:markdown",
"onView:myPlugin.sidebar",
"workspaceContains:**/*.custom",
"onFileType:*.csv"
]
}
EventTrigger
onStartupApp launches (use sparingly — impacts startup)
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
onFileType:patternA matching file type opens
onMCPServer:idAn MCP server is requested

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

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: 2,
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:

import { BasePlugin, PluginContext } from 'lokus-plugin-sdk';
export default class MyPlugin extends BasePlugin {
async activate(context: PluginContext) {
super.activate(context);
// Your activation logic
}
}

Plugins move through these states:

NOT_LOADED -> LOADING -> LOADED -> ACTIVATING -> ACTIVE -> DEACTIVATING -> DEACTIVATED
|
ERROR

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

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.

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)
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 install <id>Install a plugin
lokus-plugin linkSymlink plugin for local development
lokus-plugin listList installed plugins
lokus-plugin info <id>Show plugin details
lokus-plugin loginAuthenticate with the registry
lokus-plugin testRun plugin tests

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.