-
Notifications
You must be signed in to change notification settings - Fork 0
Plugin Authoring
Drop a .js file into <userData>/clusterspace-data/config/tools/ and ClusterSpace loads it as a tool plugin. Plugins can register new AI tools, override built-in tools, and are hot-reloaded on file change without an app restart.
Source: src/main/ai-tools/plugin-loader.ts.
<userData>/clusterspace-data/config/tools/
├── my-slack-notify.js
├── my-internal-api.js
└── recipes/ # subdirs are ignored — only top-level *.js
<userData> paths:
- Windows:
%APPDATA%\ClusterSpace\ - macOS:
~/Library/Application Support/ClusterSpace/ - Linux:
~/.config/ClusterSpace/
The tools/ directory is created if missing. Plugins are loaded on app launch and re-loaded on file save (fs.watch with require-cache busting).
Every plugin must export a register(registry) function. It's called once on load (and again on every reload).
// my-time-tool.js
function register(registry) {
registry.register({
name: 'get_current_time',
description: 'Returns the current ISO timestamp in a specified timezone.',
parameters: {
type: 'object',
properties: {
timezone: {
type: 'string',
description: 'IANA timezone (default UTC)'
}
}
},
run: async ({ timezone }) => {
const tz = timezone || 'UTC'
return {
now: new Date().toLocaleString('en-US', { timeZone: tz, hour12: false }),
timezone: tz
}
}
})
}
module.exports = { register }The registry argument is the live toolRegistry singleton. You can .register, .unregister, .has, etc.
Plugins run in the main process with full Node.js access. They can:
- Read and write files (
require('fs')) - Make HTTP requests (
require('https')ornode-fetch) - Spawn child processes (
require('child_process')) - Use any installed npm module (if
require()can find it) - Call ClusterSpace internals via
ctx.*
This is intentional — plugins are how you extend ClusterSpace without recompiling. Trade-off: only install plugins you trust. A malicious plugin can do anything your user account can.
function register(registry) {
registry.register({
name: 'slack_notify',
description: 'Post a message to a Slack channel via webhook.',
parameters: {
type: 'object',
properties: {
channel: { type: 'string' },
text: { type: 'string' }
},
required: ['channel', 'text']
},
run: async ({ channel, text }) => {
const url = process.env.SLACK_WEBHOOK_URL
if (!url) return { success: false, error: 'SLACK_WEBHOOK_URL not set' }
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel, text })
})
return res.ok
? { success: true }
: { success: false, error: `HTTP ${res.status}` }
}
})
}
module.exports = { register }function register(registry) {
registry.register({
name: 'github_get_pr',
description: 'Fetch a GitHub PR by repo/number.',
parameters: {
type: 'object',
properties: {
repo: { type: 'string', description: 'owner/name' },
number: { type: 'number' }
},
required: ['repo', 'number']
},
run: async ({ repo, number }) => {
const token = process.env.GITHUB_TOKEN
const res = await fetch(`https://api.github.com/repos/${repo}/pulls/${number}`, {
headers: token ? { Authorization: `token ${token}` } : {}
})
if (!res.ok) return { success: false, error: `HTTP ${res.status}` }
return await res.json()
}
})
}
module.exports = { register }For plugins needing secrets, the recommended path:
- Set env vars in the shell that launches ClusterSpace
- Read via
process.env.MY_SECRETin the plugin
Or, for secrets you want persistent across launches, use a JSON file in <userData>/clusterspace-data/config/ with restrictive file permissions (chmod 600).
ClusterSpace doesn't currently expose safeStorage to plugins — it's reserved for first-party stores (SSH passwords, saved logins, API keys). If you need encrypted-at-rest secrets in plugins, request the API in an issue.
Your plugin's run receives ctx as the second argument:
run: async (args, ctx) => {
const panes = ctx.workspaceStore.get(
ctx.workspaceStore.getSettings().activeWorkspaceId
)?.panes ?? []
// … do something with panes
}See Tool-Registry for the full list of ctx fields.
Register a tool with the same name as a built-in. The registry warns on overwrite but allows it.
function register(registry) {
registry.register({
name: 'browser_navigate', // overrides the built-in
description: 'Custom navigate with extra logging.',
parameters: { /* same shape as built-in */ },
run: async (args, ctx) => {
console.log('[my-plugin] navigating:', args.url)
// … your custom impl
}
})
}
module.exports = { register }This is powerful but easy to misuse — make sure your override implements the same return-value contract the model expects, or things break in subtle ways.
plugin-loader.ts runs fs.watch on the tools/ directory. On any change event:
- Bust the
require.cacheentry for the file -
require()again - Call
register(toolRegistry)again — overwrites existing registrations with the same names
This works for new files too — drop a .js and it loads without restart. Removing a file does NOT unregister its tools (the registry has no record of which plugin registered which tool); restart to clear.
Plugin tools have an unknown risk in the Goal-Policy-and-Risk-Levels — they default to network_write (the highest non-money tier). So a goal at risk read_only will prompt for any plugin tool call.
To declare a safer risk for your plugin, currently you'd edit BUILTIN_PERMISSIONS in src/main/goal-policy.ts. A plugin-friendly API for declaring permissions is on the Roadmap.
- Console logs (
console.log,console.error) go to the main process console, not the renderer DevTools. Watch the terminal where you rannpm run dev, or look at the Electron main process logs in a packaged build. - Throw an error inside
runto see it surfaced to the model as a tool error. - Use the AI-Chat-Panel to ask "call my_tool with X" and see the model's tool-call card.
- No async
register()— registration must be sync (the function can return a promise but it's not awaited; do all work insiderun) - No way to unregister cleanly when a plugin file is deleted — restart instead
- No isolation — plugins share the main process's globals
- No TypeScript out of the box (write in JS or transpile yourself; .ts files are not loaded)
- Tool-Registry — the API your plugin uses
- AI-Tools-Reference — built-in tools you may override or build on
- Goal-Policy-and-Risk-Levels — how plugin tools interact with goal policies
-
Settings-and-Configuration — where the
tools/directory lives
ClusterSpace · Issues · Releases · MIT License · Edit any page via the Edit button (top right of the wiki).
- Workspaces-and-Layout
- Terminal-Panes
- Per-Pane-Tabs
- SSH-and-tmux
- Browser-Panes
- Saved-Logins
- Command-Palette
- Broadcast-Mode
- Settings-and-Configuration
- AI-Overview
- AI-Providers
- AI-Chat-Panel
- AI-Tools-Reference
- Personas
- Skills
- Task-Templates
- Agent-Orchestration
- Fleet-Dashboard
- Goal-Runner-Overview
- Starting-a-Goal
- Success-Criteria
- Goal-Policy-and-Risk-Levels
- Critic-and-Replan
- Vision-Verification
- Goal-Dashboard