development
claude/code-feature-parity-q003hm
claude/elegant-carson-l1menh
claude/sigit-acp-local-chat-cx6380
claude/sigit-cloud-agent-expansion-reox0a
claude/tool-permission-system
claude/zen-feynman-0u78dk
development
feature/agent-tools-multiedit-glob-todos-remember
feature/background-commands
feature/commit-coauthor-attribution
feature/headless-mode
feature/init-command
feature/load-local-model-explicitly
feature/session-persistence-compaction
feature/sigit-code-cloud
feature/subagent-tool
feature/tool-permission-system
feature/tui-repo-tabs
feature/tui-tabs
main
release/v1.3.1
| 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 | }); |