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 devThis 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 jestVS Code Setup (Recommended)
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 rulesCreating 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 devOpen Lokus and test your plugin:
- Open command palette (Ctrl/Cmd + Shift + P)
- Type “Say Hello” and execute
- Press Ctrl/Cmd + Shift + G to insert greeting
- 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 contenteditor:write- Modify editor contenteditor:create- Create new editorsfilesystem:read- Read filesfilesystem:write- Write filesfilesystem:delete- Delete filesfilesystem:watch- Watch file changesnetwork:fetch- Make HTTP requestsnetwork:websocket- WebSocket connectionsworkspace:read- Read workspace dataworkspace:write- Modify workspaceui:create- Create UI elementsui:modify- Modify UIui:notifications- Show notificationscommands:register- Register commandscommands:execute- Execute commandsstorage:read- Read plugin storagestorage:write- Write plugin storagestorage:secrets- Access secret storageshell:execute- Execute shell commandsclipboard:read- Read clipboardclipboard: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.tsAssets 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
-
Start development mode:
lokus-plugin dev --debug -
Open Lokus Developer Tools:
- View → Developer → Toggle Developer Tools
- Or press Ctrl/Cmd + Shift + I
-
Navigate to Sources tab
-
Find your plugin code in the sources tree
-
Set breakpoints by clicking line numbers
-
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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
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 package2. 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
MIT3. Publish to Registry
# Login to registry
lokus-plugin login --token YOUR_TOKEN
# Publish plugin
lokus-plugin publish
# Publish beta version
lokus-plugin publish --tag beta4. 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 --verboseHot Reload Not Working
Check:
- Dev mode is enabled
- File watcher is running
- No compilation errors
Solution:
lokus-plugin dev --cleanBuild Errors
Check:
- TypeScript configuration is correct
- All imports are valid
- Types are properly defined
Solution:
npm run clean
npm install
npm run buildPerformance 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:
- Plugin Manifest Reference - Complete manifest documentation
- Plugin API Reference - Detailed API documentation
- Editor Plugins - Custom editor extensions
- UI Plugins - Custom UI components
- MCP Integration - AI assistant integration
- Publishing Guide - Publishing to registry
- Plugin Examples - Example plugins
Resources
Support
Need help? Here’s how to get support:
- GitHub Discussions: lokus-ai/lokus/discussions
- Discord: Join our Discord server for real-time help
- Email: developers@lokus.ai
- Documentation: docs.lokus.ai
Happy plugin development!