Plugins live in ~/.localcode/plugins/<name>/ and extend the agent with custom tools (callable by the LLM) and slash commands (callable by the user).
~/.localcode/plugins/my-plugin/
├── localcode.plugin.json ← manifest (required)
└── index.js ← CommonJS module (required)
{
"name": "my-plugin",
"version": "1.0.0",
"description": "Short description",
"author": "username",
"tools": ["my_tool"],
"commands": ["/mytool"]
}| Field | Required | Description |
|---|---|---|
name |
✓ | kebab-case, unique identifier |
version |
✓ | semver (e.g. 1.0.0) |
description |
✓ | one-line summary |
author |
✓ | your username |
tools |
✓ | list of tool names your plugin registers |
commands |
optional | list of slash commands your plugin registers |
module.exports = {
register(registry) {
// ── Agent tool (callable by LLM) ────────────────────────────────────────
registry.addTool({
name: 'my_tool',
description: 'What this tool does — injected into the agent system prompt',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'File path' }
},
required: ['path']
},
execute: async ({ path }) => {
// Must return a string — sent back to the LLM as the tool result
return `processed: ${path}`
}
})
// ── User slash command (callable with /mytool) ───────────────────────────
registry.addCommand({
cmd: '/mytool', // exact command; add trailing space if it takes args
description: 'Run my tool manually',
handler: async (args, ctx) => {
// args: everything after the command name (string)
// ctx.cwd: current working directory
return { type: 'done', content: `Result: ${args} in ${ctx.cwd}` }
}
})
}
}Handler return types:
type |
Effect |
|---|---|
done |
Green success message |
text |
Plain text output |
error |
Red error message |
command |
Rich command output (uses title) |
/plugin install user/repo
/plugin install https://github.com/user/repo
Downloads the tarball, validates the manifest, and loads immediately — no restart needed.
/plugin install ./path/to/my-plugin
/plugin install /absolute/path/to/plugin
Copies the directory to ~/.localcode/plugins/<name>/ and loads it.
| Command | Description |
|---|---|
/plugin or /plugin list |
Show all installed plugins with load status |
/plugin install <source> |
Install from GitHub (user/repo) or local path |
/plugin remove <name> |
Remove a plugin and unregister its tools/commands |
/plugin reload |
Reload all plugins (picks up changes without restart) |
src/
types/plugin.ts — shared interfaces: ToolDefinition, PluginCommand, PluginManifest, PluginLoadResult
plugins/
registry.ts — ToolRegistry + CommandRegistry + singletons (globalRegistry, globalCommandRegistry)
validator.ts — validateManifest(json) → { valid, errors }
loader.ts — loadPlugins(registry, commandRegistry), reloadPlugin(name, ...)
installer.ts — installPlugin(source, ...), removePlugin(name, ...), listInstalledPlugins(registry)
- Startup:
localcode.tsxcallsloadPlugins(globalRegistry, globalCommandRegistry)before the TUI renders. - Tool dispatch:
agent/tools/index.tschecksglobalRegistryfor any unknown tool name. - System prompt:
AgentRuntime.tsappends all registered tools to the LLM system prompt. - Slash commands:
app.tsxusesglobalCommandRegistry.getCommand(input)to route/inputs.
- Invalid manifest → plugin skipped, warning printed to console
index.jsfails to load → plugin skipped, warning printedregister()throws → plugin skipped, warning printed- Tool
execute()throws → error string returned to LLM (agent continues) - Two plugins register the same tool/command name → second wins, warning printed