1 #!/usr/bin/env node
2 // ACP driver for the `sigit` binary.
3 //
4 // `sigit` runs as an Agent Client Protocol server when stdin is NOT a TTY
5 // (newline-delimited JSON-RPC 2.0 over stdio — the same surface Zed / VS Code
6 // drive). This script spawns the binary in that mode, runs a scripted handshake,
7 // prints every request/response/notification, and exits non-zero on failure.
8 //
9 // It deliberately avoids triggering on-device inference: `initialize`,
10 // `session/new`, and slash commands like `/whoami` and `/status` answer without
11 // loading a multi-GB GGUF model, so the driver works on a clean machine with no
12 // model cached and no network.
13 //
14 // Usage:
15 // node driver.mjs [path-to-binary] # default: target/debug/sigit
16 // SIGIT_BIN=target/release/sigit node driver.mjs
17 //
18 // Exit code 0 = every step got a well-formed JSON-RPC result.
19
20 import { spawn } from "node:child_process";
21 import { createInterface } from "node:readline";
22 import { existsSync } from "node:fs";
23
24 const bin = process.argv[2] || process.env.SIGIT_BIN || "target/debug/sigit";
25 if (!existsSync(bin)) {
26 console.error(`binary not found: ${bin} — run \`cargo build\` first`);
27 process.exit(2);
28 }
29
30 // Force ACP mode regardless of how the driver itself was launched: pipe stdin so
31 // the child's stdin is not a TTY.
32 const child = spawn(bin, [], { stdio: ["pipe", "pipe", "pipe"] });
33
34 // Surface the agent's own logs (it writes them to stderr in ACP mode).
35 createInterface({ input: child.stderr }).on("line", (l) =>
36 console.error(`[sigit] ${l}`),
37 );
38
39 const pending = new Map(); // id -> {resolve, method}
40 const notifications = [];
41 let nextId = 1;
42 let failed = false;
43
44 createInterface({ input: child.stdout }).on("line", (line) => {
45 line = line.trim();
46 if (!line) return;
47 let msg;
48 try {
49 msg = JSON.parse(line);
50 } catch {
51 console.error(`<-- (non-JSON) ${line}`);
52 return;
53 }
54 if (msg.id !== undefined && (msg.result !== undefined || msg.error)) {
55 const waiter = pending.get(msg.id);
56 console.log(`<-- response #${msg.id} (${waiter?.method ?? "?"})`);
57 console.log(JSON.stringify(msg.result ?? msg.error, null, 2));
58 if (msg.error) failed = true;
59 waiter?.resolve(msg);
60 pending.delete(msg.id);
61 } else if (msg.method) {
62 // A notification or a server->client request. We only observe these.
63 notifications.push(msg);
64 const update = msg.params?.update;
65 let detail = "";
66 if (update?.sessionUpdate === "agent_message_chunk") {
67 // The streamed assistant text — one chunk per AgentMessageChunk. With the
68 // streaming backend a real prompt produces many of these.
69 detail = ` ${JSON.stringify(update.content?.text ?? update.content)}`;
70 } else if (update?.sessionUpdate) {
71 detail = ` (${update.sessionUpdate})`;
72 }
73 console.log(`<-- notify ${msg.method}${detail}`);
74 }
75 });
76
77 function send(method, params) {
78 const id = nextId++;
79 const req = { jsonrpc: "2.0", id, method, params };
80 console.log(`--> request #${id} ${method}`);
81 child.stdin.write(JSON.stringify(req) + "\n");
82 return new Promise((resolve, reject) => {
83 pending.set(id, { resolve, method });
84 setTimeout(() => {
85 if (pending.has(id)) {
86 pending.delete(id);
87 reject(new Error(`timeout waiting for ${method} (#${id})`));
88 }
89 }, 20_000);
90 });
91 }
92
93 async function main() {
94 // 1. Handshake.
95 await send("initialize", {
96 protocolVersion: 1,
97 clientCapabilities: {},
98 });
99
100 // 2. Open a session rooted at the repo. `cwd` must be absolute.
101 const sessionRes = await send("session/new", {
102 cwd: process.cwd(),
103 mcpServers: [],
104 });
105 const sessionId = sessionRes.result?.sessionId;
106 if (!sessionId) throw new Error("session/new returned no sessionId");
107
108 // 3. Drive a no-inference slash command through the prompt surface. `/whoami`
109 // reports the signed-in account; it never touches the model.
110 await send("session/prompt", {
111 sessionId,
112 prompt: [{ type: "text", text: "/whoami" }],
113 });
114
115 console.log("\nOK — ACP handshake, session, and /whoami round-tripped.");
116 }
117
118 main()
119 .catch((err) => {
120 console.error(`FAILED: ${err.message}`);
121 failed = true;
122 })
123 .finally(() => {
124 child.kill("SIGTERM");
125 setTimeout(() => process.exit(failed ? 1 : 0), 150);
126 });