claude-clew-bus
Let Claude Code and clew talk and command each other — two-way. Twotransports in one repo: a simple MCP message bus (pull), and a nativepeer adapter that joins clew's own /peer mesh (push, with auto-reply).
┌───────────────────────────────┐
│ THE SAME MACHINE │
└───────────────────────────────┘
┌─────────────┐ ┌─────────────┐
│ Claude Code │ │ clew │
│ (this CLI) │ │ /peer … │
└──────┬──────┘ └──────┬──────┘
│ MCP tools │ native peer
│ peer_send / peer_exec / peer_inbox │ /peer-msg,
▼ │ /peer-exec (token)
┌────────────────────────── peer-adapter.mjs ──────────────────────────┐
│ │
│ MCP server :7334/mcp ◄──drives──► peer HTTP server (advertised │
│ as "claude" in ~/.clew/peers) │
│ │
│ SSE receiver ◄── /peer-events ── clew pushes chat/todo live │
│ │
│ AUTO_REPLY=1: clew pushes a task ─► worker (clew -p) does it ─► reply│
└────────────────────────────────────────────────────────────────────────┘
── OR, the simpler pull-based bus ──
┌──────── server.mjs (MCP streamable-http, :7333/mcp) ────────┐
│ tools: send(to,text) · poll(inbox) · peek · history │
└──────────────────────────────────────────────────────────────┘
▲ Claude Code ▲ clew
│ send("clew") / poll("claude") │ send("claude") / poll("clew")
| Mode | File | Push? | Native to clew? | Best when |
|---|---|---|---|---|
| Native peer | peer-adapter.mjs |
✅ (SSE + auto-reply) | ✅ /peer discover sees "claude" |
real agent↔agent, run commands, auto-reply |
| MCP bus | server.mjs + bridge.mjs |
⚠️ needs poll/bridge | ❌ generic MCP | quick message passing, no clew peer session |
Run
npm install
npm start # listens on http://127.0.0.1:7333/mcp
# override: BUS_PORT=8000 BUS_HOST=0.0.0.0 npm start
Health check: curl http://127.0.0.1:7333/health
Connect Claude Code
Add to .mcp.json (or ~/.claude.json):
{
"mcpServers": {
"clew-bus": { "type": "http", "url": "http://127.0.0.1:7333/mcp" }
}
}
Connect clew
clew mcp add --transport http clew-bus http://127.0.0.1:7333/mcp
Tools
| Tool | Args | Purpose |
|---|---|---|
send |
to, text, from? |
Put a message in another agent's inbox |
poll |
inbox, max? |
Drain unread messages addressed to you (marks read) |
peek |
inbox |
View unread without draining |
history |
inbox |
Full log for an inbox |
Conversation pattern
- Claude Code:
send(to:"clew", text:"build the thing") - clew:
poll(inbox:"clew")→ sees it → does work →send(to:"claude", text:"done, output: …") - Claude Code:
poll(inbox:"claude")→ sees the reply → continues
Messages persist to bus/<inbox>.jsonl so nothing is lost across restarts.
Native peer mode (Claude ↔ clew via clew's own /peer mesh)
The MCP bus above is pull-based — clew won't see a message until somethingpolls. peer-adapter.mjs instead joins clew's native peer mesh, wheremessages are pushed straight into clew's REPL (like two clew instances).
How it works (from src/peer/PeerDiscovery.ts + PeerServer.ts): same-machinediscovery is file-based — each peer writes ~/.clew/peers/<pid>.json({id,hostname,ip,port,token,…}) and scans that dir to find others and learntheir auth token. Protected calls carry that token:POST /peer-msg (chat), POST /peer-exec (task), POST /peer-todo.
The adapter writes its own peer file (so clew's /peer discover sees claude),serves those endpoints, and reads clew's peer file to reach clew.
The daemon runs three things in one process, sharing one inbox:
- Peer HTTP server — serves
/peer-info/peer-msg/peer-exec/peer-todoso clew reaches us. - MCP server (
:7334/mcp) — toolspeer_listpeer_sendpeer_execpeer_todopeer_inboxso Claude drives the mesh via tools, not bash. - SSE receiver — subscribes to each clew's
/peer-eventsand pushes incoming events into the inbox live.
node peer-adapter.mjs # daemon (peer server + MCP + SSE)
node peer-adapter.mjs list # discovered clew peers
node peer-adapter.mjs send [--to name] "hi clew" # chat TO clew
node peer-adapter.mjs exec [--to name] "git branch" # run a command ON clew
Claude connects via MCP
Add to .mcp.json:
{ "mcpServers": { "clew-peer": { "type": "http", "url": "http://127.0.0.1:7334/mcp" } } }
Then call peer_list, peer_send, peer_exec, peer_inbox as native tools.
Auto-reply — clew pushes a task, Claude's side does it, replies automatically
AUTO_REPLY=1 node peer-adapter.mjs
A clew peer sends a chat message prefixed with the trigger (@claude bydefault). The adapter strips the trigger, runs a worker (clew headless -p,full tools) on the rest, and sends the result back to the sender via /peer-msg.
clew: /peer send claude "@claude summarize the last commit"
│
▼ trigger matched → strip "@claude" → spawn worker → answer
└─────────────────► /peer-msg back to clew: "↩ <answer>"
Loop safety — auto-reply only fires when:
- the message starts with the trigger (
@claude); ordinary chatter is ignored, and - our own replies (prefixed
↩) are never re-answered, and - the same sender isn't replied to more than once per cooldown window.
So two agents can chat freely without an infinite ping-pong.
Config:
| env | default | meaning |
|---|---|---|
AUTO_REPLY |
off | 1 to enable |
AUTO_REPLY_TRIGGER |
@claude |
required message prefix; set '' to reply to everything (cooldown still applies) |
AUTO_REPLY_COOLDOWN_MS |
8000 |
min gap between replies to the same sender |
WORKER_DIR |
D:/Projects/Github/clew-code |
cwd the worker runs in |
WORKER_ENTRY |
dist/main.js |
worker entrypoint (run with bun) |
WORKER_TOOLS |
Bash,Read,Glob,Grep |
tools the worker may use |
The default target for
send/exec/auto-reply is the most recently activeclew (peer files sorted by mtime), so a stale file from a closed session isnever picked. Use--to <name|port>to target a specific peer.
Completing the loop (in the clew session)
/peer share # clew starts advertising + peer server
/peer discover # clew sees "claude" (the adapter)
/peer send claude "hello" # → lands in peer-inbox.jsonl
/peer swarm claude <cmd> # → adapter runs cmd, returns result
CLEW_MESH_LAN=1is only needed for cross-machine UDP discovery. On onemachine everything works file-based with no env var.