Skip to main content

Plugins

Plugins extend the CodeBolt application. They run as separate Node.js processes, connect to the server via WebSocket, and can add custom LLM providers, connect messaging platforms, proxy code execution, or show custom UI panels.

What is a Plugin?

A plugin is a Node.js package with a codebolt.plugin field in package.json. When CodeBolt starts, it discovers plugins from specific directories, spawns them as child processes, and they connect back to the server via WebSocket using @codebolt/plugin-sdk.

Plugins are different from agent extensions:

  • Agent extensions (skills, tools, action blocks) add capabilities to agents
  • Plugins add capabilities to the application itself

Plugin Directories

DirectoryScope
~/.codebolt/plugins/Global — available in all projects
<project>/.codeboltPlugins/Per-project — overrides global

Types of Plugins

Typepackage.json type fieldPurposeKey SDK Module
LLM Provider"llmProvider"Add a custom AI model providerplugin.llmProvider
Chat Gateway"channel"Connect external messaging platforms and persistent message surfacesplugin.gateway
Execution"execution"Proxy code execution to remote environmentsplugin.executionGateway
Generic(none)Any custom extensionAny modules

Quick Start: Minimal Plugin

1. Project structure

my-plugin/
├── package.json
├── tsconfig.json
├── src/
│ └── index.ts
└── dist/
└── index.js

2. package.json

{
"name": "my-plugin",
"version": "1.0.0",
"main": "dist/index.js",
"dependencies": {
"@codebolt/plugin-sdk": "*"
},
"devDependencies": {
"typescript": "^5.4.5"
},
"codebolt": {
"plugin": {
"type": "llmProvider",
"triggers": [{ "type": "startup" }]
}
}
}

3. src/index.ts

import plugin from '@codebolt/plugin-sdk';

plugin.onStart(async (ctx) => {
console.log(`Plugin started: ${ctx.pluginId}`);
console.log(`Plugin dir: ${ctx.pluginDir}`);
// Register capabilities, set up listeners
});

plugin.onStop(async () => {
// Cleanup
});

4. Build and install

npm install && npx tsc
cp -r my-plugin/ ~/.codebolt/plugins/my-plugin/
# Reload from Plugins panel in CodeBolt UI, then click Start

Plugin Lifecycle

Start triggers

TriggerWhen it starts
startupAutomatically when CodeBolt launches
manualWhen user clicks Start in the Plugins panel
eventWhen a specific application event fires

Lifecycle hooks

import plugin from '@codebolt/plugin-sdk';

// Called when plugin process starts and WebSocket is connected
plugin.onStart(async (ctx) => {
// ctx.pluginId — unique plugin identifier
// ctx.pluginDir — absolute path to plugin directory
// ctx.manifest — the codebolt.plugin config from package.json
});

// Called when plugin is being stopped
plugin.onStop(async () => {
// Clean up connections, timers, etc.
});

// Listen for any raw WebSocket message (advanced)
plugin.onRawMessage((message) => {
// Handle custom message types
});

Environment variables

When CodeBolt spawns a plugin process, these env vars are set:

VariableValue
SOCKET_PORTServer port for WebSocket connection
pluginIdPlugin identifier
pluginTokenAuth token for this plugin session
PLUGIN_DIRAbsolute path to plugin directory
IS_PLUGIN"true"

You don't need to use these directly — @codebolt/plugin-sdk reads them automatically.


Creating an LLM Provider Plugin

Makes a custom AI provider available in CodeBolt's model selector.

package.json

{
"name": "my-llm-plugin",
"version": "1.0.0",
"main": "dist/index.js",
"codebolt": {
"plugin": {
"type": "llmProvider",
"triggers": [{ "type": "startup" }]
}
},
"dependencies": {
"@codebolt/plugin-sdk": "*"
}
}

src/index.ts

import plugin from '@codebolt/plugin-sdk';

const PROVIDER_ID = 'my-llm';

plugin.onStart(async () => {
// 1. Register the provider — appears in CodeBolt's model selector
await plugin.llmProvider.register({
providerId: PROVIDER_ID,
name: 'My LLM',
models: [
{ id: 'my-model-fast', name: 'My Model (Fast)' },
{ id: 'my-model-pro', name: 'My Model (Pro)' },
],
requiresKey: true,
configFields: [
{ key: 'apiKey', label: 'API Key', type: 'password', required: true },
{ key: 'apiUrl', label: 'Base URL', type: 'url', placeholder: 'https://api.example.com' },
],
});

// 2. Handle non-streaming completion requests
plugin.llmProvider.onCompletionRequest(async (req) => {
try {
const apiKey = req.options.providerConfig?.apiKey;
const response = await fetch('https://api.example.com/v1/chat', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: req.options.model,
messages: req.options.messages,
}),
});
const result = await response.json();
plugin.llmProvider.sendReply(req.requestId, result, true);
} catch (error) {
plugin.llmProvider.sendError(req.requestId, error.message);
}
});

// 3. Handle streaming requests
plugin.llmProvider.onStreamRequest(async (req) => {
try {
const apiKey = req.options.providerConfig?.apiKey;
const response = await fetch('https://api.example.com/v1/chat', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: req.options.model,
messages: req.options.messages,
stream: true,
}),
});

const reader = response.body!.getReader();
const decoder = new TextDecoder();
let fullContent = '';

while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
plugin.llmProvider.sendChunk(req.requestId, {
choices: [{ delta: { content: chunk } }],
});
fullContent += chunk;
}

plugin.llmProvider.sendReply(req.requestId, {
choices: [{ message: { content: fullContent, role: 'assistant' } }],
}, true);
} catch (error) {
plugin.llmProvider.sendError(req.requestId, error.message);
}
});
});

plugin.onStop(async () => {
await plugin.llmProvider.unregister(PROVIDER_ID);
});

LLM Provider API

MethodDescription
plugin.llmProvider.register(manifest)Register provider with models and config fields
plugin.llmProvider.unregister(providerId)Remove provider
plugin.llmProvider.onCompletionRequest(cb)Handle non-streaming requests
plugin.llmProvider.onStreamRequest(cb)Handle streaming requests
plugin.llmProvider.sendReply(requestId, data, success)Send final response
plugin.llmProvider.sendChunk(requestId, chunk)Send a stream chunk
plugin.llmProvider.sendError(requestId, error)Send error response

Provider config injection

When a user configures your provider in Settings (enters API key, base URL, etc.), the server automatically includes their config in every request as req.options.providerConfig:

req.options.providerConfig = {
apiKey: 'user-entered-key',
apiUrl: 'user-entered-url',
custom: { /* any other configFields values */ },
};

Creating a Chat Gateway Plugin

Connects an external messaging platform or persistent message surface to CodeBolt agents. The manifest type is still "channel", but the routing model is documented as the Chat Gateway.

package.json

{
"name": "my-channel-plugin",
"version": "1.0.0",
"main": "dist/index.js",
"codebolt": {
"plugin": {
"type": "channel",
"triggers": [{ "type": "manual" }],
"ui": { "path": "src/ui/index.html" }
}
},
"dependencies": {
"@codebolt/plugin-sdk": "*"
}
}

src/index.ts

import plugin from '@codebolt/plugin-sdk';

let botConnection: any = null;

plugin.onStart(async (ctx) => {
// Load saved config
const configRaw = await plugin.kvStore.get(ctx.pluginId, 'config', 'settings');
const config = configRaw?.value ? JSON.parse(configRaw.value) : null;

if (config?.botToken) {
await connect(config);
}

// Handle UI panel messages (for configuration)
plugin.dynamicPanel.onMessage(`plugin-ui-${ctx.pluginId}`, async (data) => {
if (data.type === 'connect') {
await connect(data.config);
await plugin.kvStore.set(ctx.pluginId, 'config', 'settings', JSON.stringify(data.config), true);
}
if (data.type === 'disconnect') {
await disconnect();
}
});

// Handle agent replies → send back to external platform
plugin.gateway.onReply(async (reply) => {
await sendToExternalPlatform(reply.externalThreadId, reply.text);
});
});

async function connect(config: any) {
botConnection = await createBotConnection(config.botToken);

await plugin.gateway.registerChannel({
name: 'My Channel',
platform: 'my-platform',
agentId: config.agentId || 'default',
threadStrategy: config.threadStrategy || 'per-user',
});

botConnection.onMessage(async (msg: any) => {
await plugin.gateway.routeMessage({
source: 'channel',
sourceId: 'my-channel-plugin',
threadStrategy: config.threadStrategy || 'per-user',
text: msg.text,
userId: msg.userId,
externalThreadId: msg.chatId,
replyTo: {
channelId: 'my-channel-plugin',
externalThreadId: msg.chatId,
userId: msg.userId,
},
});
});
}

async function disconnect() {
if (botConnection) {
await botConnection.close();
botConnection = null;
}
}

plugin.onStop(async () => {
await disconnect();
});

Thread strategies

StrategyBehavior
singleAll messages go to one thread
per-userEach platform user gets their own thread
per-conversationEach chat/group gets a thread
per-messageEvery message creates a new thread

Channel API

MethodDescription
plugin.gateway.registerChannel(config)Register as a message channel
plugin.gateway.routeMessage(message)Route external message to an agent
plugin.gateway.onReply(cb)Receive agent replies to send back to platform
plugin.gateway.onMessageToChannel(cb)Receive proactive messages from the app

Creating an Execution Plugin

Intercepts all code execution and routes it to a remote environment.

package.json

{
"name": "my-execution-plugin",
"version": "1.0.0",
"main": "dist/index.js",
"codebolt": {
"plugin": {
"type": "execution",
"gateway": { "claimsExecutionGateway": true },
"triggers": [{ "type": "startup" }]
}
},
"dependencies": {
"@codebolt/plugin-sdk": "*"
}
}

src/index.ts

import plugin from '@codebolt/plugin-sdk';

plugin.onStart(async () => {
// Claim exclusive control of execution — only ONE plugin can do this
const result = await plugin.executionGateway.claim();
if (!result.success) {
console.error('Failed to claim gateway:', result.error);
return;
}

// All execution requests now come here instead of running locally
plugin.executionGateway.onRequest(async (request) => {
try {
const result = await fetch('https://my-remote-server.com/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: request.originalType,
action: request.originalAction,
payload: request.originalMessage,
}),
});
const data = await result.json();
plugin.executionGateway.sendReply(request.requestId, data, true);
} catch (err: any) {
plugin.executionGateway.sendReply(request.requestId, { error: err.message }, false);
}
});
});

plugin.onStop(async () => {
await plugin.executionGateway.relinquish();
});

Execution Gateway API

MethodDescription
plugin.executionGateway.claim()Take exclusive control of execution
plugin.executionGateway.relinquish()Release control (local execution resumes)
plugin.executionGateway.onRequest(cb)Handle execution requests
plugin.executionGateway.sendReply(id, result, success)Send response
plugin.executionGateway.subscribe()Subscribe to notifications (non-exclusive, read-only)
plugin.executionGateway.onNotification(cb)Observe execution without intercepting

Only one plugin can claim the gateway at a time. Use subscribe() if you just want to monitor execution.


Plugin UI Panels

Any plugin can show a custom UI panel by setting ui.path in the manifest:

"codebolt": {
"plugin": {
"ui": { "path": "src/ui/index.html" }
}
}

Panel HTML (iframe)

<script>
// Send message to plugin process
window.codeboltPlugin.sendMessage({ type: 'myAction', data: { key: 'value' } });

// Receive messages from plugin process
window.codeboltPlugin.onMessage((data) => {
console.log('From plugin:', data);
});
</script>

Plugin side

// Send data to UI panel
await plugin.dynamicPanel.send('panel-id', { status: 'connected' });

// Receive messages from UI panel
plugin.dynamicPanel.onMessage('panel-id', (data) => {
if (data.type === 'myAction') {
// handle
}
});

// Open a new panel programmatically
await plugin.dynamicPanel.open('my-panel', 'Panel Title', '<h1>Hello</h1>');

// Open and wait for user response (blocks until panel submits)
const response = await plugin.dynamicPanel.open('my-panel', 'Configure', html, {
waitForResponse: true,
timeout: 300000,
});

Persistent Storage (KV Store)

Plugins should use kvStore (not the filesystem) for configuration and state:

// Create a named instance (once)
await plugin.kvStore.createInstance('my-plugin-config', 'Plugin configuration');

// Save data
await plugin.kvStore.set('my-plugin-config', 'settings', 'apiKey', 'sk-...', true);
// Parameters: instanceId, namespace, key, value, autoCreateInstance

// Load data
const result = await plugin.kvStore.get('my-plugin-config', 'settings', 'apiKey');
const apiKey = result?.value;

// Delete
await plugin.kvStore.delete('my-plugin-config', 'settings', 'apiKey');

// List all keys in a namespace
const namespaces = await plugin.kvStore.getNamespaces('my-plugin-config');

All SDK Modules

Every plugin (regardless of type) has access to these modules:

ModuleWhat it does
Core
plugin.fsRead, write, search, list files and directories
plugin.terminalExecute shell commands (sync, streaming, interruptible)
plugin.gitGit operations
plugin.chatSend messages, ask questions, confirmations
plugin.llmMake LLM calls through CodeBolt's provider system
plugin.mcpUse MCP tools
Storage
plugin.kvStorePersistent key-value storage with namespaces
plugin.vectordbVector database operations
plugin.dbmemoryMemory operations
plugin.knowledgeKnowledge base operations
UI
plugin.dynamicPanelOpen, update, close UI panels
Project
plugin.projectProject info and paths
plugin.environmentEnvironment variables
plugin.stateState management
plugin.codebaseSearchSemantic code search
plugin.codemapCode structure map
Tasks & Threads
plugin.threadThread management
plugin.taskTask management
plugin.todoTodo list operations
plugin.jobJob management
Communication
plugin.mailMail operations
plugin.calendarCalendar operations
plugin.browserBrowser automation
System
plugin.hookHook into application events
plugin.narrativeNarrative context
plugin.eventLogEvent logging
plugin.debugDebug utilities

package.json Reference

{
"codebolt": {
"plugin": {
"type": "llmProvider | channel | execution",
"triggers": [
{ "type": "startup" },
{ "type": "manual" },
{ "type": "event", "eventTypes": ["CHAT_MESSAGE_SENT"] }
],
"gateway": {
"claimsExecutionGateway": true
},
"ui": {
"path": "src/ui/index.html"
},
"entryPoint": "index.js"
}
}
}
FieldDescription
typePlugin type — determines which SDK modules are primary
triggersWhen the plugin starts
triggers[].type"startup", "manual", or "event"
triggers[].eventTypesFor event triggers — which events to listen for
gateway.claimsExecutionGatewayFor execution plugins — claim exclusive control
ui.pathPath to HTML file for the plugin's UI panel
entryPointOverride the main field for plugin process entry

See Also