multibot-sdk
v0.6.0
Published
Headless multi-bot host: local HTTP gateway, group chat, third-party LLMs, directory-scoped tools.
Maintainers
Readme
sdk-bots
Headless multi-bot host: a local HTTP gateway, group-chat orchestration, and directory-scoped tools. No Electron UI. Models come from you (OpenRouter / local OpenAI-compatible / Claude Code / Codex).
npm package: multibot-sdk (sdk-bots was already taken on the registry).
License / provenance: derived from an unofficial reconstruction of a commercial product (Grok Bot / Cursor by Anysphere). Published as UNLICENSED (source-available for evaluation) — see
NOTICE.mdandLICENSE. Not affiliated with Anysphere.
Install
pnpm add multibot-sdk
# npm install multibot-sdkRequires Node >= 22 (node:sqlite). Native modules tree-sitter / tree-sitter-bash ship prebuilds for macOS arm64 and common Linux.
import { startHost, SdkBotsClient } from "multibot-sdk";
const host = await startHost(); // data at ~/.sdk-bots (never ~/.cursor)
const sdk = host.client;
const a1 = await sdk.createAgent({ name: "researcher", description: "research" });
const a2 = await sdk.createAgent({ name: "writer", description: "writes" });
const group = await sdk.createGroup({ name: "war room", memberIds: [a1.agent.id, a2.agent.id] });
await sdk.setHostSettings({ inferenceProvider: "openrouter" });
await sdk.sendPrompt({ agentId: group.agent.id, prompt: "@researcher pick a plan" });
const dispose = sdk.subscribe(ev => console.log(ev.channel, ev.payload));Console (when the host is running): http://127.0.0.1:7331/ if you set SAND_HOST_PORT=7331.
Demo:
NODE_OPTIONS="--use-system-ca" npm run example:group-chat
NODE_OPTIONS="--use-system-ca" npm run example:group-chat -- 帮我想一个周末徒步计划See examples/README.md.
startHost() options: { dataDir?, port?, token?, startupTimeoutMs? }. Discovery is <dataDir>/gateway.json (pid-stamped; stale files are ignored).
Sandbox
There is a local box exec-daemon (loopback, default port 1337). File tools map /workspace → ~/.sdk-bots/box-workspace. Paths that escape that directory are rejected. That is the intended sandbox: restrict to a directory, not a VM.
Shell starts with cwd inside that folder. It is still a normal shell, so treat the host as a trusted local process and keep the gateway on loopback.
Tools and third-party models
Turns use this host's tool loop, not a plug-in of some other agent runtime (LangGraph, CrewAI, …). You swap the model:
| inferenceProvider | transport | tools |
|---|---|---|
| openrouter | OpenAI-compatible /chat/completions (local freeroute or OpenRouter cloud) | SendMessage, SendToAgent, Shell, Read, Write, Grep, … |
| claude-code | Claude Agent SDK / CLI | Claude's tools + optional MCP |
| codex | Codex Responses API | Codex tool surface |
| mock (SAND_AGENT_MOCK_RESPONSE) | no network | scripted SendMessage / tool calls |
Set localToolPermission: "always" in host settings.json (or setHostSettings) so file/shell tools do not wait on a UI prompt. The user-visible reply is still SendMessage.
Group chat: @name addresses one member; omit @ or use @所有人 for everyone.
Zero-credential mock
process.env.SAND_AGENT_MOCK_RESPONSE = "MOCK-REPLY: hello from the mock model";Plain string, or {"sendMessage": "..."} / {"toolCalls": [...]} (see parseSandMockScript).
Tests
npm run test:unit # no host
npm run test:smoke # boot + CRUD + SSE
npm run test:e2e # mock group-chat loop
npm run test:integration # isolated host per caseHost-requiring scripts build the daemon/worker bundles first (pre hooks).
If a parent shell injects a NODE_OPTIONS preload, run tests with NODE_OPTIONS="--use-system-ca" so recursive fs.rm is not intercepted.
Build
npm run build # clean + daemon + workers + tsc (JS + public d.ts)
npm run typecheckGenerated (gitignored): dist/box-exec-daemon/main.cjs, dist/host-workers/*.cjs — built into the single dist/ output; nothing generated lives inside src/.
Environment
| var | purpose |
|---|---|
| SAND_DATA_ROOT | host data root (default ~/.sdk-bots) |
| SAND_GATEWAY_TOKEN | gateway auth token |
| SAND_HOST_PORT | gateway port |
| SAND_GATEWAY_BIND_HOST | bind host (default 127.0.0.1) |
| SAND_OPENROUTER_BASE_URL | default http://127.0.0.1:3080/freeroute/v1 |
| SAND_OPENROUTER_MODEL | default auto locally |
| OPENROUTER_API_KEY | required only for official OpenRouter cloud |
| SAND_LOCAL_CHAT_TIMEOUT_MS | per-request timeout for local OpenAI-compatible inference (default 120000) |
| SAND_AGENT_MOCK_RESPONSE | mock inference script |
| SAND_BOX_EXEC_DAEMON_ENTRY | daemon bundle path |
| SAND_USE_EXISTING_BOX_EXEC_DAEMON | 1 = do not spawn the daemon |
| SAND_BOX_EXEC_DAEMON_HOST | host connects here (default 127.0.0.1; non-loopback = remote box, no local spawn) |
| SAND_BOX_EXEC_DAEMON_BIND_HOST | daemon listen address (default loopback; 0.0.0.0 to accept remote clients) |
| SAND_BOX_EXEC_DAEMON_PORT | daemon port (default 1337) |
| SAND_BOX_EXEC_DAEMON_AUTH_TOKEN | daemon bearer token (default local) |
| SAND_LOCAL_EXEC_GATEWAY_URL | standalone multibot-host --local-exec-daemon attach URL |
| SAND_LOCAL_EXEC_ADVERTISE_HOST | host written into the local-exec connection file (defaults to loopback when the gateway binds 0.0.0.0) |
| SAND_GOAL_STALL_LIMIT | goal-guard stall limit: stop a routine after this many consecutive cycles with no ledger update (default 3) |
| SAND_SWARM_COLLABORATION | force unattended-swarm collaboration rules for every bot (default off; per-agent opt-in is the SWARM-v1 marker in a bot's description) |
| SAND_SWARM_BOARD | shared task board path handed to swarm-mode bots (default <data root>/swarm/TASKS.md) |
Goal loop (routine that iterates until the objective is met)
A cron routine becomes a goal loop by dropping a goal.json next to its automation.json
(<data root>/agents/<agentId>/automations/<routineId>/goal.json). The engine then:
- injects a
<goal_guard>block into every wake prompt (objective, acceptance checklist, cycle number); - advances
cycleon each scheduled fire and recordslastFiredAt; - stops the loop by itself once
statusisachieved/blocked/abandoned, once every acceptance item is proven, or afterSAND_GOAL_STALL_LIMITcycles with no ledger update — and disarms the routine, leaving a run record that says why.
The model owns objective, acceptance[].done, status and updatedAt; the engine owns
cycle, stallCount and lastFiredAt. A cycle without a ledger write counts as no progress.
Routines without a goal.json behave exactly as before. See
examples/verify-goal-loop.mjs for an end-to-end check.
Layout
src/sdk—SdkBotsClient+startHost()(public entry)src/bootstrap— headless CLI bootstrap + shared host composition (composition.ts)src/host— gateway, group chat, inference (recovered runtime)src/lib— recovered library shelf (agent tools,agent-exec, chat-inference, …)src/box-exec-daemon— sandbox process for Shell/Read/Write (the remote computer is this box; bind/connect viaSAND_BOX_EXEC_DAEMON_*)src/host/local-exec— in-process ExternalShell/ExternalRead provider backed by the same box daemon (alsomultibot-host --local-exec-daemonfor a standalone SSE attach)src/proto— generated protobuf closures (generated/+redacted/; machine-owned, never hand-edit)src/shared— shared kernel: contracts (host-extensions), path policy (sand-paths), backend clientstest/unit— no host;test/integration— isolated host per case
Strict layering: sdk → bootstrap → host → {lib, proto, shared}; shared never imports host.
CLI (host process): npx multibot-host after installing this package, or npm start in this repo.
Status
Headless boot, 37 host extensions, gateway /health + POST /api/* + SSE, mock turns, and live OpenRouter/freeroute turns are exercised in-tree. See CHANGELOG.md.
This is not MIT/Apache. Evaluate locally; do not assume you may ship it as a dependency in a commercial product until provenance is reviewed.
Roadmap: web Grok-bots on dsh
Inference rides on the local dsh freeroute gateway (http://127.0.0.1:3080/freeroute/v1, model auto) by default — verified end-to-end including tool round-trips. The web UI ships as a dsh Cordis plugin (collapsible Bots panel). Full architecture, gateway/SSE API reference, plugin spec, and roadmap: ../dsh-bots/DEVELOPMENT.md.
