Node.js SDK for building openbee engine plugins. An engine plugin wraps one agent CLI (Claude Code, Codex, pi, ...) so openbee can run it. The SDK implements the one-shot JSON protocol between openbee and the plugin and ships the helpers every wrapper needs: locating the CLI, spawning it with a log file, and reading its version.
Requires Node.js 20 or newer. Plugins also run on Bun.
npm install @theopenbee/engine-sdkFor every engine call openbee starts the plugin command (for example node dist/index.js) as a new process group, writes one JSON request line to its stdin and closes stdin, then reads one JSON response line from its stdout. The plugin starts the agent CLI as a child process, waits for it and answers. One process serves one call.
Request (protocol version 1):
{
"protocol_version": 1,
"method": "run",
"openbee_version": "0.9.0",
"engine_id": "claude",
"language": "en",
"config": { "path": "/usr/local/bin/claude" },
"data_dir": "~/.openbee/plugins/data/claude/",
"state_dir": "~/.openbee/.claude/",
"params": {}
}| Field | Description |
|---|---|
method |
check, run or usage |
config |
The plugin's configuration as set by the user (for example path) |
data_dir |
Private directory for the plugin, deleted when the plugin is uninstalled |
state_dir |
Directory for state that must survive an uninstall; openbee never deletes it |
params |
Method specific, see below |
Response, exactly one line:
{"result": {}}
{"error": {"message": "what went wrong"}}runEngine produces the response and exits with code 0, including for errors. An unknown method, an unsupported protocol_version or an exception thrown by the engine becomes an {"error": ...} response.
| Method | params |
result |
|---|---|---|
check |
{} |
{ version, path } |
run |
{ role, work_dir, prompt, session_id, resume, extra_args, log_path, mcp? } |
{ text, error? } |
usage |
{ session_ids } |
{ usage: { <session_id>: TokenUsage[] } } |
mcp is { name, url, api_key_env }: the MCP server the CLI should use and the name of the environment variable that holds its API key. check must finish within 30 seconds and usage within 60 seconds; run is only bounded by openbee stopping the task.
interface Engine {
check(req: EngineRequest<Record<string, never>>): Promise<CheckResult>;
run(req: EngineRequest<RunParams>): Promise<RunResult>;
usage(req: EngineRequest<{ session_ids: string[] }>): Promise<Record<string, TokenUsage[]>>;
}
interface CheckResult { version: string; path: string }
interface RunResult { text: string; error?: string }
interface TokenUsage {
model: string;
input_tokens: number;
output_tokens: number;
cache_creation_tokens: number;
cache_read_tokens: number;
}runEngine(engine) reads the request, dispatches on method and writes the response. usage returns a map from session id to its token usage; runEngine wraps it as { "usage": ... } for you.
import { runEngine, findCli, spawnCli, cliVersion } from "@theopenbee/engine-sdk";
runEngine({
async check(req) {
const path = findCli(req.config, "mycli");
return { version: await cliVersion(path), path };
},
async run(req) {
const { work_dir, prompt, extra_args, log_path } = req.params;
const command = findCli(req.config, "mycli");
let text = "";
const res = await spawnCli({
command,
args: ["--print", ...extra_args],
cwd: work_dir,
input: prompt,
logPath: log_path,
onStdoutLine(line) {
text += line + "\n";
return line;
},
});
if (res.code !== 0) {
return { text: text.trim(), error: res.stderrTail || `mycli exited with code ${res.code}` };
}
return { text: text.trim() };
},
async usage() {
return {};
},
});| Helper | Description |
|---|---|
findCli(config, name) |
Returns config.path when it is set and exists, otherwise searches PATH (honouring PATHEXT on Windows). Throws <path> not found or <name> not found on PATH. |
spawnCli(opts) |
Spawns the CLI in the plugin's process group, writes input to stdin and closes it, and appends stdout lines and stderr to logPath (created or truncated). onStdoutLine sees every stdout line before it is logged: return the text to log or null to drop it. Resolves with { code, signal, stderrTail } where stderrTail is the last 4 KB of stderr. opts.env is merged over the plugin's environment; a key set to undefined is removed. |
cliVersion(path, args?) |
Runs <path> --version (or args) with a 10 second timeout and returns the trimmed first line. |
log.info/warn/error(msg) |
Writes a line to stderr. |
- stdout carries only the response. The SDK redirects
console.log,console.infoandconsole.debugto stderr as soon asrunEnginestarts, but never write toprocess.stdoutyourself and do not let the CLI inherit your stdout. - Logs go to stderr. Use
log.info/warn/errororconsole.error; openbee collects the plugin's stderr. - Never spawn the CLI detached. openbee starts the plugin as its own process group and kills the whole group when a task is stopped or times out. A detached child, a
setsidwrapper or a daemonised helper escapes that and is left running.spawnClinever detaches. - Exit once you have answered.
runEngineexits with code 0 right after writing the response; a plugin that exits without a response, or non-zero, counts as failed. - Stay portable. Use
node:built-ins only so the plugin runs on Node 20+ and on Bun.
npm install
npm testnpm install also builds dist/; run npm run build after changing src/. The tests run the engine fixture in child node processes, and under Bun when it is installed (bun on PATH or ~/.openbee/runtime/bun-*/bun).
MIT