@m64/nats-pi-bridge 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +350 -0
- package/LICENSE +201 -0
- package/README.md +238 -0
- package/dist/server.js +689 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# nats-pi-bridge
|
|
2
|
+
|
|
3
|
+
Standalone headless service that spawns and manages PI coding agent sessions on demand via NATS.
|
|
4
|
+
|
|
5
|
+
> **Zero PI install required.** The bridge embeds the full [`@mariozechner/pi-coding-agent`](https://github.com/badlogic/pi-mono) runtime as library code — agent loop, tool execution (read/write/edit/bash), session management, model registry, all in-process. One `npx @m64/nats-pi-bridge` command gives you a fully operational PI worker addressable over NATS. No global `pi` binary, no TUI, no interactive CLI. Ideal for Docker containers, CI runners, and remote hosts where a full interactive coding agent would be overkill.
|
|
6
|
+
>
|
|
7
|
+
> **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.
|
|
8
|
+
|
|
9
|
+
This bridge exposes session lifecycle + prompting as a `pi-exec` NATS microservice. Callers send structured JSON to a permanent **intake** 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:
|
|
10
|
+
|
|
11
|
+
- `nats-pi-channel` — PI extension for **interactive** PI sessions
|
|
12
|
+
- `nats-claude-channel` — Claude Code MCP channel
|
|
13
|
+
- `nats-channel` (OpenClaw) — OpenClaw NATS channel
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
No install, no setup files — just [Node.js](https://nodejs.org) ≥20.6 and the [nats CLI](https://github.com/nats-io/natscli). You'll be streaming agent output over NATS in under a minute.
|
|
18
|
+
|
|
19
|
+
> **Prerequisite:** An API key in `~/.pi/agent/auth.json`. Create it directly — PI doesn't need to be installed:
|
|
20
|
+
>
|
|
21
|
+
> ```bash
|
|
22
|
+
> mkdir -p ~/.pi/agent && cat > ~/.pi/agent/auth.json <<'EOF'
|
|
23
|
+
> {"anthropic": {"type": "api_key", "key": "sk-ant-YOUR-KEY-HERE"}}
|
|
24
|
+
> EOF
|
|
25
|
+
> ```
|
|
26
|
+
>
|
|
27
|
+
> Swap `anthropic` for any other provider (`openrouter`, `openai`, `google`, …). If you already have PI installed locally, `pi /login` populates this file for you automatically.
|
|
28
|
+
|
|
29
|
+
### 1. Run the bridge
|
|
30
|
+
|
|
31
|
+
In one terminal:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx @m64/nats-pi-bridge
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That's it. It connects to `demo.nats.io` by default and registers a `pi-exec` NATS microservice with an intake on `agents.pi-exec.$USER`:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
pi-exec: connecting to demo.nats.io (context: default)
|
|
41
|
+
pi-exec: connected
|
|
42
|
+
pi-exec: intake registered on agents.pi-exec.yourname
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. Fire a one-shot prompt
|
|
46
|
+
|
|
47
|
+
In a second terminal:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
nats --server demo.nats.io req agents.pi-exec.$USER \
|
|
51
|
+
'{"sessionMode":"run","body":"Write a haiku about NATS","cwd":"/tmp"}' \
|
|
52
|
+
--wait-for-empty --timeout 60s
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The 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.
|
|
56
|
+
|
|
57
|
+
### 3. Keep a session alive across prompts
|
|
58
|
+
|
|
59
|
+
Now create a persistent session, prove it remembers context with a follow-up, then stop it cleanly:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Start the session + run the initial prompt
|
|
63
|
+
nats --server demo.nats.io req agents.pi-exec.$USER \
|
|
64
|
+
'{"sessionMode":"session","body":"Pick a random 3-digit number and remember it","cwd":"/tmp","sessionId":"demo"}' \
|
|
65
|
+
--wait-for-empty --timeout 120s
|
|
66
|
+
|
|
67
|
+
# See it in the live session list
|
|
68
|
+
nats --server demo.nats.io req agents.pi-exec.$USER \
|
|
69
|
+
'{"sessionMode":"list"}' --timeout 5s
|
|
70
|
+
|
|
71
|
+
# Follow-up on the per-session subject — proves the session remembers
|
|
72
|
+
nats --server demo.nats.io req agents.pi-exec.$USER.demo \
|
|
73
|
+
"What number did you pick?" --wait-for-empty --timeout 60s
|
|
74
|
+
|
|
75
|
+
# Stop it cleanly — the per-session instance disappears from `nats micro list`
|
|
76
|
+
nats --server demo.nats.io req agents.pi-exec.$USER \
|
|
77
|
+
'{"sessionMode":"stop","sessionId":"demo"}' --timeout 5s
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Ctrl-C the bridge when you're done. Every active session is disposed during graceful shutdown.
|
|
81
|
+
|
|
82
|
+
## Architecture
|
|
83
|
+
|
|
84
|
+
See [DESIGN.md](./DESIGN.md) for the full architectural document.
|
|
85
|
+
|
|
86
|
+
The bridge runs as a single Node.js process. The `pi-exec` service has:
|
|
87
|
+
|
|
88
|
+
- **Intake instance** (permanent): `agents.pi-exec.<owner>` — control plane
|
|
89
|
+
- **Per-session instances** (dynamic): `agents.pi-exec.<owner>.<sessionId>` — data plane
|
|
90
|
+
|
|
91
|
+
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()`.
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
nats micro list
|
|
95
|
+
→ pi-exec │ 0.0.1 │ ABC123 │ intake
|
|
96
|
+
│ │ DEF456 │ worker-1 — /home/mario/code/nats-zig
|
|
97
|
+
│ │ GHI789 │ sid-gpt — /home/mario/code/sid-gpt
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Install
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Run without installing (what the Quick Start shows)
|
|
104
|
+
npx @m64/nats-pi-bridge
|
|
105
|
+
|
|
106
|
+
# Or install globally
|
|
107
|
+
npm install -g @m64/nats-pi-bridge
|
|
108
|
+
nats-pi-bridge
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Requires Node.js ≥20.6 (pulled in via the PI SDK's engine requirement).
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone <this repo>
|
|
117
|
+
cd nats-pi-bridge
|
|
118
|
+
npm install
|
|
119
|
+
|
|
120
|
+
# Run from TypeScript source (tsx)
|
|
121
|
+
npm start
|
|
122
|
+
|
|
123
|
+
# Watch mode
|
|
124
|
+
npm run dev
|
|
125
|
+
|
|
126
|
+
# Build the Node-compatible bundle into dist/
|
|
127
|
+
npm run build
|
|
128
|
+
node dist/server.js
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
Optional config file: `~/.pi-exec/config.json`
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"context": "my-nats-context",
|
|
138
|
+
"defaultModel": "anthropic/claude-sonnet-4-5",
|
|
139
|
+
"defaultThinkingLevel": "off",
|
|
140
|
+
"defaultMaxLifetime": 1800
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Environment overrides:
|
|
145
|
+
|
|
146
|
+
| Var | Description |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `NATS_CONTEXT` | NATS CLI context name (looks for `~/.config/nats/context/<name>.json`). If unset, connects to `demo.nats.io`. |
|
|
149
|
+
| `PI_EXEC_DEFAULT_MODEL` | Default model spec, e.g. `anthropic/claude-sonnet-4-5` |
|
|
150
|
+
| `PI_EXEC_DEFAULT_MAX_LIFETIME` | Default max session lifetime in seconds |
|
|
151
|
+
|
|
152
|
+
PI credentials are read from `~/.pi/agent/auth.json` (the standard PI agent location). Run `pi /login` interactively once to populate it.
|
|
153
|
+
|
|
154
|
+
## Intake protocol
|
|
155
|
+
|
|
156
|
+
Send structured JSON to `agents.pi-exec.<owner>`:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
type IntakeRequest = {
|
|
160
|
+
sessionMode: "run" | "session" | "stop" | "list";
|
|
161
|
+
body?: string; // prompt text (run/session)
|
|
162
|
+
cwd?: string; // working directory (run/session) — resolved to absolute
|
|
163
|
+
from?: string; // caller identity (optional)
|
|
164
|
+
sessionId?: string; // REQUIRED for session and stop modes
|
|
165
|
+
model?: string; // e.g. "anthropic/claude-sonnet-4-5"
|
|
166
|
+
thinkingLevel?: string; // off|minimal|low|medium|high|xhigh
|
|
167
|
+
maxLifetime?: number; // seconds, 0 = no expiry
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
| `sessionMode` | Required fields | Behaviour |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| `run` | `body`, `cwd` | Ephemeral: create session, prompt, stream chunks, dispose |
|
|
174
|
+
| `session` | `body`, `cwd`, `sessionId` | Persistent: create session + per-session NATS instance, prompt initial, stream chunks |
|
|
175
|
+
| `stop` | `sessionId` | Dispose session, remove instance |
|
|
176
|
+
| `list` | — | Return all active sessions as JSON |
|
|
177
|
+
|
|
178
|
+
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.
|
|
179
|
+
|
|
180
|
+
## Testing
|
|
181
|
+
|
|
182
|
+
Start the bridge:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm start # from source
|
|
186
|
+
# or
|
|
187
|
+
node dist/server.js # from the built bundle
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
In another terminal:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Discoverability
|
|
194
|
+
nats micro list
|
|
195
|
+
nats micro info pi-exec
|
|
196
|
+
|
|
197
|
+
# Single-shot run
|
|
198
|
+
nats req agents.pi-exec.$USER \
|
|
199
|
+
'{"sessionMode":"run","body":"What files are here?","cwd":"/tmp"}' \
|
|
200
|
+
--wait-for-empty --timeout 120s
|
|
201
|
+
|
|
202
|
+
# Create persistent session
|
|
203
|
+
nats req agents.pi-exec.$USER \
|
|
204
|
+
'{"sessionMode":"session","body":"List the project structure","cwd":"/path/to/project","sessionId":"worker-1"}' \
|
|
205
|
+
--wait-for-empty --timeout 120s
|
|
206
|
+
|
|
207
|
+
# Follow-up via per-session subject
|
|
208
|
+
nats req agents.pi-exec.$USER.worker-1 "Now fix the failing tests" \
|
|
209
|
+
--wait-for-empty --timeout 120s
|
|
210
|
+
|
|
211
|
+
# List sessions
|
|
212
|
+
nats req agents.pi-exec.$USER '{"sessionMode":"list"}' --timeout 5s
|
|
213
|
+
|
|
214
|
+
# Inspect a session
|
|
215
|
+
nats req agents.pi-exec.$USER.worker-1.inspect "" --timeout 5s
|
|
216
|
+
|
|
217
|
+
# Stop a session
|
|
218
|
+
nats req agents.pi-exec.$USER \
|
|
219
|
+
'{"sessionMode":"stop","sessionId":"worker-1"}' --timeout 5s
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Error responses
|
|
223
|
+
|
|
224
|
+
All error responses follow `{"error": "<code>", ...context}`. Examples:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{"error":"invalid_json", "message":"..."}
|
|
228
|
+
{"error":"invalid_request", "message":"sessionMode required"}
|
|
229
|
+
{"error":"invalid_sessionMode", "sessionMode":"explode"}
|
|
230
|
+
{"error":"missing_fields", "required":["body","cwd","sessionId"]}
|
|
231
|
+
{"error":"invalid_sessionId", "sessionId":"...", "message":"..."}
|
|
232
|
+
{"error":"session_exists", "sessionId":"...", "subject":"...", "message":"..."}
|
|
233
|
+
{"error":"session_create_failed", "message":"..."}
|
|
234
|
+
{"error":"not_found", "sessionId":"..."}
|
|
235
|
+
{"error":"shutting_down"}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
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.
|