#!/usr/bin/env node import fs from "node:fs"; import { Readable, Writable } from "node:stream"; import * as acp from "@agentclientprotocol/sdk"; const pending = new Map(); const logFile = process.env.FAKE_ACP_LOG; const log = (entry) => { if (logFile) fs.appendFileSync(logFile, `${JSON.stringify(entry)}\n`); }; const app = acp.agent({ name: "fake-acp-agent" }) .onRequest(acp.methods.agent.initialize, ({ params }) => { log({ method: "initialize" }); return { protocolVersion: params.protocolVersion, agentCapabilities: { loadSession: true, sessionCapabilities: { resume: {}, close: {}, list: {} } }, agentInfo: { name: "fake-acp-agent", version: "1" } }; }) .onRequest(acp.methods.agent.session.new, ({ params }) => { const sessionId = `fake-${Date.now()}-${Math.random().toString(16).slice(2)}`; log({ method: "session/new", sessionId, cwd: params.cwd }); return { sessionId }; }) .onRequest(acp.methods.agent.session.load, ({ params }) => { log({ method: "session/load", sessionId: params.sessionId }); return {}; }) .onRequest(acp.methods.agent.session.resume, ({ params }) => { log({ method: "session/resume", sessionId: params.sessionId }); return {}; }) .onRequest(acp.methods.agent.session.close, ({ params }) => { log({ method: "session/close", sessionId: params.sessionId }); return {}; }) .onRequest(acp.methods.agent.session.list, () => ({ sessions: [] })) .onRequest(acp.methods.agent.session.prompt, async ({ params, client, signal }) => { const text = params.prompt.filter((item) => item.type === "text").map((item) => item.text).join(""); log({ method: "session/prompt", sessionId: params.sessionId, text }); if (text === "crash") process.exit(9); if (text === "permission") { const response = await client.request(acp.methods.client.session.requestPermission, { sessionId: params.sessionId, toolCall: { toolCallId: "bash-1", title: "bash git status", kind: "execute", name: "bash", rawInput: "git status" }, options: [ { optionId: "allow", name: "Allow", kind: "allow_once" }, { optionId: "reject", name: "Reject", kind: "reject_once" } ] }); await update(client, params.sessionId, response.outcome.outcome === "selected" ? response.outcome.optionId : "cancelled"); return { stopReason: "end_turn" }; } if (text === "hang") { await new Promise((resolve) => { const done = () => resolve(undefined); pending.set(params.sessionId, done); signal.addEventListener("abort", done, { once: true }); }); pending.delete(params.sessionId); return { stopReason: "cancelled" }; } await update(client, params.sessionId, text.includes("Initialize this ACP session") ? "READY" : `reply:${params.sessionId}:${text}`); return { stopReason: "end_turn" }; }) .onNotification(acp.methods.agent.session.cancel, ({ params }) => { log({ method: "session/cancel", sessionId: params.sessionId }); pending.get(params.sessionId)?.(); }); function update(client, sessionId, text) { return client.notify(acp.methods.client.session.update, { sessionId, update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } } }); } const stream = acp.ndJsonStream( Writable.toWeb(process.stdout), Readable.toWeb(process.stdin) ); const connection = app.connect(stream); await connection.closed;