@m64/nats-pi-bridge
v0.0.4
Published
Standalone headless service that spawns and manages PI coding agent sessions on demand via NATS. Control plane / data plane split, streaming wire protocol, multi-session microservice registration.
Downloads
44
Maintainers
Readme
nats-pi-bridge
Standalone headless service that spawns and manages PI coding agent sessions on demand via NATS.
Zero PI install required. The bridge embeds the full
@mariozechner/pi-coding-agentruntime as library code — agent loop, tool execution (read/write/edit/bash), session management, model registry, all in-process. Onenpx @m64/nats-pi-bridgecommand gives you a fully operational PI worker addressable over NATS. No globalpibinary, no TUI, no interactive CLI. Ideal for Docker containers, CI runners, and remote hosts where a full interactive coding agent would be overkill.All you need on the host: Node.js ≥20.6, a NATS server (or
demo.nats.io), and an API key for any supported model provider.
This bridge exposes session lifecycle + prompting as a pi-exec NATS microservice. Callers send structured JSON to a permanent control endpoint to create, list, and stop sessions. Persistent sessions get their own per-session subject for follow-ups using the standard streaming wire protocol shared with the sibling implementations:
nats-pi-channel— PI extension for interactive PI sessionsnats-claude-channel— Claude Code MCP channelnats-channel(OpenClaw) — OpenClaw NATS channel
Quick start
No install, no setup files — just Node.js ≥20.6 and the nats CLI. You'll be streaming agent output over NATS in under a minute.
Prerequisite: An API key in
~/.pi/agent/auth.json. Create it directly — PI doesn't need to be installed:mkdir -p ~/.pi/agent && cat > ~/.pi/agent/auth.json <<'EOF' {"anthropic": {"type": "api_key", "key": "sk-ant-YOUR-KEY-HERE"}} EOFSwap
anthropicfor any other provider (openrouter,openai,pi /loginpopulates this file for you automatically.
1. Run the bridge
In one terminal:
npx @m64/nats-pi-bridgeThat's it. It connects to demo.nats.io by default and registers a pi-exec NATS microservice with a control endpoint on agents.pi-exec.$USER:
pi-exec: connecting to demo.nats.io (context: default)
pi-exec: connected
pi-exec: control registered on agents.pi-exec.yourname2. Fire a one-shot prompt
In a second terminal:
nats --server demo.nats.io req agents.pi-exec.$USER \
'{"sessionMode":"run","body":"Write a haiku about NATS","cwd":"/tmp"}' \
--wait-for-empty --timeout 60sThe agent's reply streams back chunk-by-chunk and an empty message closes the stream. The session is disposed the moment it's done — fire and forget.
3. Keep a session alive across prompts
Now create a persistent session, prove it remembers context with a follow-up, then stop it cleanly:
# Start the session + run the initial prompt
nats --server demo.nats.io req agents.pi-exec.$USER \
'{"sessionMode":"session","body":"Pick a random 3-digit number and remember it","cwd":"/tmp","sessionId":"demo"}' \
--wait-for-empty --timeout 120s
# See it in the live session list
nats --server demo.nats.io req agents.pi-exec.$USER \
'{"sessionMode":"list"}' --timeout 5s
# Follow-up on the per-session subject — proves the session remembers
nats --server demo.nats.io req agents.pi-exec.$USER.demo \
"What number did you pick?" --wait-for-empty --timeout 60s
# Stop it cleanly — the per-session instance disappears from `nats micro list`
nats --server demo.nats.io req agents.pi-exec.$USER \
'{"sessionMode":"stop","sessionId":"demo"}' --timeout 5sCtrl-C the bridge when you're done. Every active session is disposed during graceful shutdown.
Architecture
The bridge runs as a single Node.js process. The pi-exec service has:
- Control instance (permanent):
agents.pi-exec.<owner>— control plane - Per-session instances (dynamic):
agents.pi-exec.<owner>.<sessionId>— data plane
Each session is its own NATS microservice instance (the same pattern that nats-pi-channel uses across processes — but here within one process). On stop, the session's instance is removed cleanly via service.stop().
nats micro list
→ pi-exec │ 0.0.4 │ ABC123 │ control
│ │ DEF456 │ worker-1 — /home/mario/code/nats-zig
│ │ GHI789 │ sid-gpt — /home/mario/code/sid-gptNATS server
The Quick Start uses demo.nats.io because it's zero-setup, but you'll usually want your own server. The bridge picks the server from a NATS CLI context named via the NATS_CONTEXT environment variable.
Localhost
# 1. One-time: create a context pointing at your local NATS
nats context save local --server nats://localhost:4222
# 2. Run the bridge against it
NATS_CONTEXT=local npx @m64/nats-pi-bridgepi-exec: connecting to nats://localhost:4222 (context: local)
pi-exec: connected
pi-exec: control registered on agents.pi-exec.m64Don't have a local NATS server yet? The fastest way:
docker run --rm -p 4222:4222 nats:latestOr grab a binary from the nats-server releases.
Make a context the default
To avoid setting NATS_CONTEXT on every invocation, drop a one-line config file:
mkdir -p ~/.pi-exec
echo '{"context":"local"}' > ~/.pi-exec/config.jsonAfter that, plain npx @m64/nats-pi-bridge connects to your chosen server.
Authenticated and remote servers
The same context format works for any NATS auth scheme — credentials files, NKeys, JWTs, TLS, user/password. Anything nats context save can store, the bridge can use:
nats context save prod \
--server nats://nats.example.com:4222 \
--creds ~/.nkeys/creds/synadia/MyAccount/me.creds
NATS_CONTEXT=prod npx @m64/nats-pi-bridgeContext files live at ~/.config/nats/context/<name>.json. See the NATS CLI context docs for the full list of supported fields.
Install
# Run without installing (what the Quick Start shows)
npx @m64/nats-pi-bridge
# Or install globally
npm install -g @m64/nats-pi-bridge
nats-pi-bridgeRequires Node.js ≥20.6 (pulled in via the PI SDK's engine requirement).
Development
git clone <this repo>
cd nats-pi-bridge
npm install
# Run from TypeScript source (tsx)
npm start
# Watch mode
npm run dev
# Build the Node-compatible bundle into dist/
npm run build
node dist/server.jsConfiguration
Optional config file: ~/.pi-exec/config.json
{
"context": "my-nats-context",
"defaultModel": "anthropic/claude-sonnet-4-5",
"defaultThinkingLevel": "off",
"defaultMaxLifetime": 1800
}Environment overrides:
| Var | Description |
|---|---|
| NATS_CONTEXT | NATS CLI context name (looks for ~/.config/nats/context/<name>.json). If unset, connects to demo.nats.io. |
| PI_EXEC_DEFAULT_MODEL | Default model spec, e.g. anthropic/claude-sonnet-4-5 |
| PI_EXEC_DEFAULT_MAX_LIFETIME | Default max session lifetime in seconds |
PI credentials are read from ~/.pi/agent/auth.json (the standard PI agent location). Create the file directly — see the Quick Start prerequisite for the one-liner. If you have PI installed locally, pi /login populates it automatically.
Control protocol
Send structured JSON to agents.pi-exec.<owner>:
type ControlRequest = {
sessionMode: "run" | "session" | "stop" | "list";
body?: string; // prompt text (run/session)
cwd?: string; // working directory (run/session) — resolved to absolute
from?: string; // caller identity (optional)
sessionId?: string; // REQUIRED for session and stop modes
model?: string; // e.g. "anthropic/claude-sonnet-4-5"
thinkingLevel?: string; // off|minimal|low|medium|high|xhigh
maxLifetime?: number; // seconds, 0 = no expiry
};| sessionMode | Required fields | Behaviour |
|---|---|---|
| run | body, cwd | Ephemeral: create session, prompt, stream chunks, dispose |
| session | body, cwd, sessionId | Persistent: create session + per-session NATS instance, prompt initial, stream chunks |
| stop | sessionId | Dispose session, remove instance |
| list | — | Return all active sessions as JSON |
Per-session prompt subject (agents.pi-exec.<owner>.<sessionId>) accepts the standard wire protocol: plain text or {from, body} JSON, streaming chunks back, empty payload terminates the stream.
Testing
Start the bridge:
npm start # from source
# or
node dist/server.js # from the built bundleIn another terminal:
# Discoverability
nats micro list
nats micro info pi-exec
# Single-shot run
nats req agents.pi-exec.$USER \
'{"sessionMode":"run","body":"What files are here?","cwd":"/tmp"}' \
--wait-for-empty --timeout 120s
# Create persistent session
nats req agents.pi-exec.$USER \
'{"sessionMode":"session","body":"List the project structure","cwd":"/path/to/project","sessionId":"worker-1"}' \
--wait-for-empty --timeout 120s
# Follow-up via per-session subject
nats req agents.pi-exec.$USER.worker-1 "Now fix the failing tests" \
--wait-for-empty --timeout 120s
# List sessions
nats req agents.pi-exec.$USER '{"sessionMode":"list"}' --timeout 5s
# Inspect a session
nats req agents.pi-exec.$USER.worker-1.inspect "" --timeout 5s
# Stop a session
nats req agents.pi-exec.$USER \
'{"sessionMode":"stop","sessionId":"worker-1"}' --timeout 5sError responses
All error responses follow {"error": "<code>", ...context}. Examples:
{"error":"invalid_json", "message":"..."}
{"error":"invalid_request", "message":"sessionMode required"}
{"error":"invalid_sessionMode", "sessionMode":"explode"}
{"error":"missing_fields", "required":["body","cwd","sessionId"]}
{"error":"invalid_sessionId", "sessionId":"...", "message":"..."}
{"error":"session_exists", "sessionId":"...", "subject":"...", "message":"..."}
{"error":"session_create_failed", "message":"..."}
{"error":"not_found", "sessionId":"..."}
{"error":"shutting_down"}Streaming modes (run, initial session prompt) emit text chunks + an empty terminator instead of a JSON wrapper. Errors during streaming are emitted as error: <message> text chunks followed by the empty terminator.
