82 lines
3.7 KiB
JavaScript
82 lines
3.7 KiB
JavaScript
#!/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 });
|
|
if (process.env.FAKE_ACP_LOAD_FAIL === "1" || (process.env.FAKE_ACP_LOAD_FAIL_FILE && fs.existsSync(process.env.FAKE_ACP_LOAD_FAIL_FILE))) throw new Error("load failed");
|
|
return {};
|
|
})
|
|
.onRequest(acp.methods.agent.session.resume, ({ params }) => {
|
|
log({ method: "session/resume", sessionId: params.sessionId });
|
|
if (process.env.FAKE_ACP_RESUME_FAIL === "1" || (process.env.FAKE_ACP_RESUME_FAIL_FILE && fs.existsSync(process.env.FAKE_ACP_RESUME_FAIL_FILE))) throw new Error("resume failed");
|
|
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;
|