@versdotsh/reef 0.1.2

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.
Files changed (83) hide show
  1. package/.github/workflows/test.yml +47 -0
  2. package/README.md +257 -0
  3. package/bun.lock +587 -0
  4. package/examples/services/board/board.test.ts +215 -0
  5. package/examples/services/board/index.ts +155 -0
  6. package/examples/services/board/routes.ts +335 -0
  7. package/examples/services/board/store.ts +329 -0
  8. package/examples/services/board/tools.ts +214 -0
  9. package/examples/services/commits/commits.test.ts +74 -0
  10. package/examples/services/commits/index.ts +14 -0
  11. package/examples/services/commits/routes.ts +43 -0
  12. package/examples/services/commits/store.ts +114 -0
  13. package/examples/services/feed/behaviors.ts +23 -0
  14. package/examples/services/feed/feed.test.ts +101 -0
  15. package/examples/services/feed/index.ts +117 -0
  16. package/examples/services/feed/routes.ts +224 -0
  17. package/examples/services/feed/store.ts +194 -0
  18. package/examples/services/feed/tools.ts +83 -0
  19. package/examples/services/journal/index.ts +15 -0
  20. package/examples/services/journal/journal.test.ts +57 -0
  21. package/examples/services/journal/routes.ts +45 -0
  22. package/examples/services/journal/store.ts +119 -0
  23. package/examples/services/journal/tools.ts +32 -0
  24. package/examples/services/log/index.ts +15 -0
  25. package/examples/services/log/log.test.ts +70 -0
  26. package/examples/services/log/routes.ts +44 -0
  27. package/examples/services/log/store.ts +105 -0
  28. package/examples/services/log/tools.ts +57 -0
  29. package/examples/services/registry/behaviors.ts +128 -0
  30. package/examples/services/registry/index.ts +37 -0
  31. package/examples/services/registry/registry.test.ts +135 -0
  32. package/examples/services/registry/routes.ts +76 -0
  33. package/examples/services/registry/store.ts +224 -0
  34. package/examples/services/registry/tools.ts +116 -0
  35. package/examples/services/reports/index.ts +14 -0
  36. package/examples/services/reports/reports.test.ts +75 -0
  37. package/examples/services/reports/routes.ts +42 -0
  38. package/examples/services/reports/store.ts +110 -0
  39. package/examples/services/ui/auth.ts +61 -0
  40. package/examples/services/ui/index.ts +16 -0
  41. package/examples/services/ui/routes.ts +160 -0
  42. package/examples/services/ui/static/app.js +369 -0
  43. package/examples/services/ui/static/index.html +42 -0
  44. package/examples/services/ui/static/style.css +157 -0
  45. package/examples/services/usage/behaviors.ts +166 -0
  46. package/examples/services/usage/index.ts +19 -0
  47. package/examples/services/usage/routes.ts +53 -0
  48. package/examples/services/usage/store.ts +341 -0
  49. package/examples/services/usage/tools.ts +75 -0
  50. package/examples/services/usage/usage.test.ts +91 -0
  51. package/package.json +29 -0
  52. package/services/agent/index.ts +465 -0
  53. package/services/board/index.ts +155 -0
  54. package/services/board/routes.ts +335 -0
  55. package/services/board/store.ts +329 -0
  56. package/services/board/tools.ts +214 -0
  57. package/services/docs/index.ts +391 -0
  58. package/services/feed/behaviors.ts +23 -0
  59. package/services/feed/index.ts +117 -0
  60. package/services/feed/routes.ts +224 -0
  61. package/services/feed/store.ts +194 -0
  62. package/services/feed/tools.ts +83 -0
  63. package/services/installer/index.ts +574 -0
  64. package/services/services/index.ts +165 -0
  65. package/services/ui/auth.ts +61 -0
  66. package/services/ui/index.ts +16 -0
  67. package/services/ui/routes.ts +160 -0
  68. package/services/ui/static/app.js +369 -0
  69. package/services/ui/static/index.html +42 -0
  70. package/services/ui/static/style.css +157 -0
  71. package/skills/create-service/SKILL.md +698 -0
  72. package/src/core/auth.ts +28 -0
  73. package/src/core/client.ts +99 -0
  74. package/src/core/discover.ts +152 -0
  75. package/src/core/events.ts +44 -0
  76. package/src/core/extension.ts +66 -0
  77. package/src/core/server.ts +262 -0
  78. package/src/core/testing.ts +155 -0
  79. package/src/core/types.ts +194 -0
  80. package/src/extension.ts +16 -0
  81. package/src/main.ts +11 -0
  82. package/tests/server.test.ts +1338 -0
  83. package/tsconfig.json +29 -0
@@ -0,0 +1,91 @@
1
+ import { describe, test, expect, afterAll } from "bun:test";
2
+ import { createTestHarness, type TestHarness } from "../../../src/core/testing.js";
3
+ import usage from "./index.js";
4
+
5
+ let t: TestHarness;
6
+ const setup = (async () => {
7
+ t = await createTestHarness({ services: [usage] });
8
+ })();
9
+ afterAll(() => t?.cleanup());
10
+
11
+ describe("usage", () => {
12
+ test("records a session", async () => {
13
+ await setup;
14
+ const { status, data } = await t.json("/usage/sessions", {
15
+ method: "POST",
16
+ auth: true,
17
+ body: {
18
+ sessionId: "sess-001",
19
+ agent: "worker-1",
20
+ model: "claude-sonnet-4",
21
+ tokens: { input: 1000, output: 500, cacheRead: 200, cacheWrite: 100, total: 1800 },
22
+ cost: { input: 0.003, output: 0.0075, cacheRead: 0.0002, cacheWrite: 0.000375, total: 0.011075 },
23
+ turns: 5,
24
+ toolCalls: { bash: 3, read: 2 },
25
+ startedAt: new Date(Date.now() - 60000).toISOString(),
26
+ endedAt: new Date().toISOString(),
27
+ },
28
+ });
29
+ expect(status).toBe(201);
30
+ expect(data.sessionId).toBe("sess-001");
31
+ });
32
+
33
+ test("lists sessions", async () => {
34
+ await setup;
35
+ const { status, data } = await t.json<{ sessions: any[]; count: number }>("/usage/sessions", {
36
+ auth: true,
37
+ });
38
+ expect(status).toBe(200);
39
+ expect(data.sessions.length).toBeGreaterThanOrEqual(1);
40
+ });
41
+
42
+ test("filters sessions by agent", async () => {
43
+ await setup;
44
+ const { data } = await t.json<{ sessions: any[] }>("/usage/sessions?agent=worker-1", {
45
+ auth: true,
46
+ });
47
+ for (const s of data.sessions) {
48
+ expect(s.agent).toBe("worker-1");
49
+ }
50
+ });
51
+
52
+ test("records a VM lifecycle event", async () => {
53
+ await setup;
54
+ const { status, data } = await t.json("/usage/vms", {
55
+ method: "POST",
56
+ auth: true,
57
+ body: {
58
+ vmId: "vm-usage-001",
59
+ role: "worker",
60
+ agent: "coordinator",
61
+ createdAt: new Date().toISOString(),
62
+ },
63
+ });
64
+ expect(status).toBe(201);
65
+ expect(data.vmId).toBe("vm-usage-001");
66
+ });
67
+
68
+ test("lists VM records", async () => {
69
+ await setup;
70
+ const { status, data } = await t.json<{ vms: any[]; count: number }>("/usage/vms", {
71
+ auth: true,
72
+ });
73
+ expect(status).toBe(200);
74
+ expect(data.vms.length).toBeGreaterThanOrEqual(1);
75
+ });
76
+
77
+ test("returns usage summary", async () => {
78
+ await setup;
79
+ const { status, data } = await t.json<any>("/usage", { auth: true });
80
+ expect(status).toBe(200);
81
+ expect(data.totals).toBeDefined();
82
+ expect(data.totals.cost).toBeDefined();
83
+ expect(data.totals.tokens).toBeDefined();
84
+ });
85
+
86
+ test("requires auth", async () => {
87
+ await setup;
88
+ const { status } = await t.json("/usage");
89
+ expect(status).toBe(401);
90
+ });
91
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@versdotsh/reef",
3
+ "version": "0.1.2",
4
+ "description": "Self-improving fleet infrastructure — the minimum kernel agents need to build their own tools",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun --watch run src/main.ts",
8
+ "start": "bun run src/main.ts",
9
+ "test": "bun test"
10
+ },
11
+ "keywords": ["pi-package"],
12
+ "pi": {
13
+ "extensions": ["./src/extension.ts"],
14
+ "skills": ["./skills"]
15
+ },
16
+ "dependencies": {
17
+ "hono": "^4.12.2",
18
+ "ulid": "^3.0.2"
19
+ },
20
+ "devDependencies": {
21
+ "@mariozechner/pi-ai": "^0.54.2",
22
+ "@mariozechner/pi-coding-agent": "^0.54.2",
23
+ "@sinclair/typebox": "^0.34.48",
24
+ "@types/bun": "latest"
25
+ },
26
+ "peerDependencies": {
27
+ "typescript": "^5"
28
+ }
29
+ }
@@ -0,0 +1,465 @@
1
+ /**
2
+ * Agent service — run tasks and interactive sessions using pi.
3
+ *
4
+ * Fire-and-forget tasks (automation):
5
+ * POST /agent/tasks — submit a task (spawns pi -p)
6
+ * GET /agent/tasks — list runs
7
+ * GET /agent/tasks/:id — get run status + output
8
+ * POST /agent/tasks/:id/cancel — cancel a running task
9
+ *
10
+ * Interactive sessions (chat):
11
+ * POST /agent/sessions — start a session (spawns pi --mode rpc)
12
+ * GET /agent/sessions — list sessions
13
+ * GET /agent/sessions/:id/events — SSE stream of pi events
14
+ * POST /agent/sessions/:id/message — send a message
15
+ * POST /agent/sessions/:id/abort — abort current operation
16
+ * DELETE /agent/sessions/:id — end session
17
+ *
18
+ * The UI service (examples/services/ui) provides the chat web interface.
19
+ *
20
+ * Config (env vars):
21
+ * PI_PATH — path to pi binary (default: "pi")
22
+ * PI_MODEL — model to use (default: "claude-sonnet-4-20250514")
23
+ * PI_PROVIDER — provider (default: "anthropic")
24
+ */
25
+
26
+ import { Hono } from "hono";
27
+ import { spawn, execSync, type ChildProcess } from "node:child_process";
28
+ import { createInterface, type Interface as ReadlineInterface } from "node:readline";
29
+ import { ulid } from "ulid";
30
+ import { readFileSync, existsSync } from "node:fs";
31
+ import { join } from "node:path";
32
+ import type { ServiceModule, ServiceContext } from "../src/core/types.js";
33
+
34
+ let ctx: ServiceContext;
35
+ let piAvailable = false;
36
+
37
+ // =============================================================================
38
+ // Shared config
39
+ // =============================================================================
40
+
41
+ function piPath(): string {
42
+ return process.env.PI_PATH || "pi";
43
+ }
44
+ function piModel(): string {
45
+ return process.env.PI_MODEL || "claude-sonnet-4-20250514";
46
+ }
47
+ function piProvider(): string {
48
+ return process.env.PI_PROVIDER || "anthropic";
49
+ }
50
+ function serverPort(): number {
51
+ return parseInt(process.env.PORT || "3000", 10);
52
+ }
53
+
54
+ // =============================================================================
55
+ // Fire-and-forget tasks
56
+ // =============================================================================
57
+
58
+ interface Run {
59
+ id: string;
60
+ task: string;
61
+ status: "running" | "done" | "error" | "cancelled";
62
+ output: string;
63
+ pid?: number;
64
+ createdAt: string;
65
+ finishedAt?: string;
66
+ exitCode?: number | null;
67
+ }
68
+
69
+ const runs = new Map<string, Run>();
70
+ const taskProcesses = new Map<string, ChildProcess>();
71
+
72
+ function buildSystemAppend(projectRoot: string, port: number): string {
73
+ const lines: string[] = [];
74
+ lines.push(`You are working inside a reef server (running on localhost:${port}).`);
75
+ lines.push(`Project root: ${projectRoot}`);
76
+ lines.push("");
77
+ lines.push("## Reef conventions");
78
+ lines.push("- Service modules go in services/<name>/index.ts");
79
+ lines.push("- Each module default-exports a ServiceModule (see src/core/types.ts)");
80
+ lines.push("- After writing a service, reload it:");
81
+ lines.push(
82
+ ` curl -X POST localhost:${port}/services/reload/<name> -H "Authorization: Bearer $VERS_AUTH_TOKEN"`,
83
+ );
84
+ lines.push("- Example services are in examples/services/");
85
+ lines.push("");
86
+
87
+ const skillPath = join(projectRoot, "skills/create-service/SKILL.md");
88
+ if (existsSync(skillPath)) {
89
+ lines.push("## Create-service skill reference");
90
+ lines.push("");
91
+ lines.push(readFileSync(skillPath, "utf-8"));
92
+ }
93
+
94
+ return lines.join("\n");
95
+ }
96
+
97
+ function spawnTask(run: Run, projectRoot: string, port: number): ChildProcess {
98
+ const contextText = buildSystemAppend(projectRoot, port);
99
+ const args = [
100
+ "-p",
101
+ "--no-session",
102
+ "--provider", piProvider(),
103
+ "--model", piModel(),
104
+ "--append-system-prompt", contextText,
105
+ run.task,
106
+ ];
107
+
108
+ const child = spawn(piPath(), args, {
109
+ cwd: projectRoot,
110
+ env: { ...process.env, VERS_AUTH_TOKEN: process.env.VERS_AUTH_TOKEN || "" },
111
+ stdio: ["ignore", "pipe", "pipe"],
112
+ });
113
+
114
+ child.stdout?.on("data", (chunk: Buffer) => {
115
+ run.output += chunk.toString();
116
+ });
117
+ child.stderr?.on("data", (chunk: Buffer) => {
118
+ run.output += chunk.toString();
119
+ });
120
+
121
+ child.on("close", (code) => {
122
+ run.exitCode = code;
123
+ run.finishedAt = new Date().toISOString();
124
+ if (run.status !== "cancelled") {
125
+ run.status = code === 0 ? "done" : "error";
126
+ }
127
+ taskProcesses.delete(run.id);
128
+ });
129
+
130
+ child.on("error", (err) => {
131
+ run.output += `\nProcess error: ${err.message}`;
132
+ run.status = "error";
133
+ run.finishedAt = new Date().toISOString();
134
+ taskProcesses.delete(run.id);
135
+ });
136
+
137
+ return child;
138
+ }
139
+
140
+ // =============================================================================
141
+ // Interactive sessions (pi RPC mode)
142
+ // =============================================================================
143
+
144
+ interface Session {
145
+ id: string;
146
+ process: ChildProcess;
147
+ rl: ReadlineInterface;
148
+ sseClients: Set<ReadableStreamDefaultController<Uint8Array>>;
149
+ status: "active" | "closed";
150
+ createdAt: string;
151
+ model: string;
152
+ provider: string;
153
+ recentEvents: string[];
154
+ }
155
+
156
+ const sessions = new Map<string, Session>();
157
+ const MAX_RECENT_EVENTS = 200;
158
+
159
+ function spawnSession(id: string): Session {
160
+ const projectRoot = process.cwd();
161
+ const contextText = buildSystemAppend(projectRoot, serverPort());
162
+
163
+ const args = [
164
+ "--mode", "rpc",
165
+ "--no-session",
166
+ "--provider", piProvider(),
167
+ "--model", piModel(),
168
+ "--append-system-prompt", contextText,
169
+ ];
170
+
171
+ const child = spawn(piPath(), args, {
172
+ cwd: projectRoot,
173
+ env: { ...process.env, VERS_AUTH_TOKEN: process.env.VERS_AUTH_TOKEN || "" },
174
+ stdio: ["pipe", "pipe", "pipe"],
175
+ });
176
+
177
+ const session: Session = {
178
+ id,
179
+ process: child,
180
+ rl: createInterface({ input: child.stdout! }),
181
+ sseClients: new Set(),
182
+ status: "active",
183
+ createdAt: new Date().toISOString(),
184
+ model: piModel(),
185
+ provider: piProvider(),
186
+ recentEvents: [],
187
+ };
188
+
189
+ session.rl.on("line", (line) => {
190
+ try {
191
+ JSON.parse(line);
192
+ const sseData = `data: ${line}\n\n`;
193
+ const encoded = new TextEncoder().encode(sseData);
194
+ for (const controller of session.sseClients) {
195
+ try {
196
+ controller.enqueue(encoded);
197
+ } catch {
198
+ session.sseClients.delete(controller);
199
+ }
200
+ }
201
+ session.recentEvents.push(line);
202
+ if (session.recentEvents.length > MAX_RECENT_EVENTS) {
203
+ session.recentEvents.shift();
204
+ }
205
+ } catch {}
206
+ });
207
+
208
+ child.stderr?.on("data", (chunk: Buffer) => {
209
+ const text = chunk.toString().trim();
210
+ if (text) console.log(` [agent] session ${id} stderr: ${text}`);
211
+ });
212
+
213
+ child.on("close", () => {
214
+ session.status = "closed";
215
+ for (const controller of session.sseClients) {
216
+ try { controller.close(); } catch {}
217
+ }
218
+ session.sseClients.clear();
219
+ });
220
+
221
+ return session;
222
+ }
223
+
224
+ function sendToSession(session: Session, command: Record<string, unknown>): boolean {
225
+ if (session.status !== "active" || !session.process.stdin?.writable) return false;
226
+ session.process.stdin.write(JSON.stringify(command) + "\n");
227
+ return true;
228
+ }
229
+
230
+ function endSession(session: Session): void {
231
+ session.status = "closed";
232
+ session.process.kill("SIGTERM");
233
+ setTimeout(() => {
234
+ try { session.process.kill("SIGKILL"); } catch {}
235
+ }, 3000);
236
+ for (const controller of session.sseClients) {
237
+ try { controller.close(); } catch {}
238
+ }
239
+ session.sseClients.clear();
240
+ }
241
+
242
+ // =============================================================================
243
+ // Routes
244
+ // =============================================================================
245
+
246
+ const routes = new Hono();
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Tasks (fire-and-forget)
250
+ // ---------------------------------------------------------------------------
251
+
252
+ routes.post("/tasks", async (c) => {
253
+ const body = await c.req.json().catch(() => ({}));
254
+ const task = (body.task as string)?.trim();
255
+
256
+ if (!task) return c.json({ error: "task is required" }, 400);
257
+ if (!piAvailable) return c.json({ error: `pi not found at "${piPath()}". Set PI_PATH env var.` }, 500);
258
+
259
+ const id = ulid();
260
+ const run: Run = { id, task, status: "running", output: "", createdAt: new Date().toISOString() };
261
+ runs.set(id, run);
262
+
263
+ const child = spawnTask(run, process.cwd(), serverPort());
264
+ run.pid = child.pid;
265
+ taskProcesses.set(id, child);
266
+
267
+ console.log(` [agent] task ${id}: ${task.slice(0, 80)}${task.length > 80 ? "..." : ""}`);
268
+ return c.json({ id, status: run.status, task, createdAt: run.createdAt }, 201);
269
+ });
270
+
271
+ routes.get("/tasks", (c) => {
272
+ const items = Array.from(runs.values())
273
+ .map(({ id, task, status, createdAt, finishedAt }) => ({ id, task, status, createdAt, finishedAt }))
274
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
275
+ return c.json({ runs: items, count: items.length });
276
+ });
277
+
278
+ routes.get("/tasks/:id", (c) => {
279
+ const run = runs.get(c.req.param("id"));
280
+ if (!run) return c.json({ error: "run not found" }, 404);
281
+
282
+ const tail = parseInt(c.req.query("tail") || "0", 10);
283
+ const output = tail > 0 ? run.output.slice(-tail) : run.output;
284
+ return c.json({ ...run, output, outputLength: run.output.length });
285
+ });
286
+
287
+ routes.post("/tasks/:id/cancel", (c) => {
288
+ const run = runs.get(c.req.param("id"));
289
+ if (!run) return c.json({ error: "run not found" }, 404);
290
+ if (run.status !== "running") return c.json({ error: `run is already ${run.status}` }, 400);
291
+
292
+ const child = taskProcesses.get(run.id);
293
+ if (child) {
294
+ child.kill("SIGTERM");
295
+ setTimeout(() => { if (taskProcesses.has(run.id)) child.kill("SIGKILL"); }, 5000);
296
+ }
297
+ run.status = "cancelled";
298
+ run.finishedAt = new Date().toISOString();
299
+ console.log(` [agent] cancelled task ${run.id}`);
300
+ return c.json({ id: run.id, status: "cancelled" });
301
+ });
302
+
303
+ // ---------------------------------------------------------------------------
304
+ // Sessions (interactive chat via pi RPC)
305
+ // ---------------------------------------------------------------------------
306
+
307
+ routes.post("/sessions", (c) => {
308
+ if (!piAvailable) return c.json({ error: `pi not found at "${piPath()}". Set PI_PATH env var.` }, 500);
309
+
310
+ const id = ulid();
311
+ const session = spawnSession(id);
312
+ sessions.set(id, session);
313
+
314
+ console.log(` [agent] session ${id} started`);
315
+ return c.json({ id, status: session.status, createdAt: session.createdAt, model: session.model }, 201);
316
+ });
317
+
318
+ routes.get("/sessions", (c) => {
319
+ const items = Array.from(sessions.values()).map(({ id, status, createdAt, model }) => ({
320
+ id, status, createdAt, model,
321
+ }));
322
+ return c.json({ sessions: items, count: items.length });
323
+ });
324
+
325
+ routes.get("/sessions/:id/events", (c) => {
326
+ const session = sessions.get(c.req.param("id"));
327
+ if (!session) return c.json({ error: "session not found" }, 404);
328
+
329
+ const stream = new ReadableStream<Uint8Array>({
330
+ start(controller) {
331
+ for (const evt of session.recentEvents) {
332
+ controller.enqueue(new TextEncoder().encode(`data: ${evt}\n\n`));
333
+ }
334
+ session.sseClients.add(controller);
335
+ },
336
+ cancel(controller) {
337
+ session.sseClients.delete(controller);
338
+ },
339
+ });
340
+
341
+ return new Response(stream, {
342
+ headers: {
343
+ "Content-Type": "text/event-stream",
344
+ "Cache-Control": "no-cache",
345
+ Connection: "keep-alive",
346
+ "Transfer-Encoding": "chunked",
347
+ },
348
+ });
349
+ });
350
+
351
+ routes.post("/sessions/:id/message", async (c) => {
352
+ const session = sessions.get(c.req.param("id"));
353
+ if (!session) return c.json({ error: "session not found" }, 404);
354
+ if (session.status !== "active") return c.json({ error: "session is closed" }, 400);
355
+
356
+ const body = await c.req.json().catch(() => ({}));
357
+ const message = (body.message as string)?.trim();
358
+ if (!message) return c.json({ error: "message is required" }, 400);
359
+
360
+ const streamingBehavior = body.streamingBehavior as string | undefined;
361
+ const command: Record<string, unknown> = { type: "prompt", message };
362
+ if (streamingBehavior) command.streamingBehavior = streamingBehavior;
363
+
364
+ const ok = sendToSession(session, command);
365
+ if (!ok) return c.json({ error: "failed to send to pi process" }, 500);
366
+
367
+ return c.json({ ok: true });
368
+ });
369
+
370
+ routes.post("/sessions/:id/abort", (c) => {
371
+ const session = sessions.get(c.req.param("id"));
372
+ if (!session) return c.json({ error: "session not found" }, 404);
373
+
374
+ sendToSession(session, { type: "abort" });
375
+ return c.json({ ok: true });
376
+ });
377
+
378
+ routes.delete("/sessions/:id", (c) => {
379
+ const session = sessions.get(c.req.param("id"));
380
+ if (!session) return c.json({ error: "session not found" }, 404);
381
+
382
+ endSession(session);
383
+ console.log(` [agent] session ${session.id} ended`);
384
+ return c.json({ id: session.id, status: "closed" });
385
+ });
386
+
387
+ // =============================================================================
388
+ // Module
389
+ // =============================================================================
390
+
391
+ const agent: ServiceModule = {
392
+ name: "agent",
393
+ description: "Run tasks and interactive sessions using pi",
394
+ routes,
395
+
396
+ init(serviceCtx: ServiceContext) {
397
+ ctx = serviceCtx;
398
+ try {
399
+ execSync(`${piPath()} --help`, { stdio: "ignore", timeout: 5000 });
400
+ piAvailable = true;
401
+ console.log(` [agent] pi found at "${piPath()}"`);
402
+ } catch {
403
+ console.warn(` [agent] pi not found at "${piPath()}" — set PI_PATH env var`);
404
+ }
405
+ },
406
+
407
+ routeDocs: {
408
+ "POST /tasks": {
409
+ summary: "Submit a fire-and-forget task",
410
+ detail: "Spawns pi in print mode. Returns immediately with a run ID.",
411
+ body: { task: { type: "string", required: true, description: "What to do" } },
412
+ response: "{ id, status, task, createdAt }",
413
+ },
414
+ "GET /tasks": {
415
+ summary: "List all task runs",
416
+ response: "{ runs: [...], count }",
417
+ },
418
+ "GET /tasks/:id": {
419
+ summary: "Get task run status and output",
420
+ params: { id: { type: "string", required: true, description: "Run ID" } },
421
+ query: { tail: { type: "number", description: "Last N chars of output" } },
422
+ response: "{ id, task, status, output, outputLength, ... }",
423
+ },
424
+ "POST /tasks/:id/cancel": {
425
+ summary: "Cancel a running task",
426
+ params: { id: { type: "string", required: true, description: "Run ID" } },
427
+ response: "{ id, status: 'cancelled' }",
428
+ },
429
+ "POST /sessions": {
430
+ summary: "Start an interactive chat session",
431
+ detail: "Spawns pi in RPC mode. Connect to /sessions/:id/events for SSE stream.",
432
+ response: "{ id, status, createdAt, model }",
433
+ },
434
+ "GET /sessions": {
435
+ summary: "List all sessions",
436
+ response: "{ sessions: [...], count }",
437
+ },
438
+ "GET /sessions/:id/events": {
439
+ summary: "SSE stream of pi events",
440
+ params: { id: { type: "string", required: true, description: "Session ID" } },
441
+ response: "text/event-stream",
442
+ },
443
+ "POST /sessions/:id/message": {
444
+ summary: "Send a message to a session",
445
+ params: { id: { type: "string", required: true, description: "Session ID" } },
446
+ body: {
447
+ message: { type: "string", required: true, description: "Message text" },
448
+ streamingBehavior: { type: "string", description: "'steer' or 'followUp' (if agent is mid-response)" },
449
+ },
450
+ response: "{ ok: true }",
451
+ },
452
+ "POST /sessions/:id/abort": {
453
+ summary: "Abort current operation in a session",
454
+ params: { id: { type: "string", required: true, description: "Session ID" } },
455
+ response: "{ ok: true }",
456
+ },
457
+ "DELETE /sessions/:id": {
458
+ summary: "End a session",
459
+ params: { id: { type: "string", required: true, description: "Session ID" } },
460
+ response: "{ id, status: 'closed' }",
461
+ },
462
+ },
463
+ };
464
+
465
+ export default agent;