import { ClientSideConnection, PROTOCOL_VERSION, ndJsonStream, type RequestPermissionRequest, type SessionNotification, } from "@agentclientprotocol/sdk"; import { spawn, type ChildProcess } from "node:child_process"; import * as readline from "node:readline"; import { Readable, Writable } from "node:stream"; import { ensureOpenClawCliOnPath } from "../infra/path-env.js"; export type AcpClientOptions = { cwd?: string; serverCommand?: string; serverArgs?: string[]; serverVerbose?: boolean; verbose?: boolean; }; export type AcpClientHandle = { client: ClientSideConnection; agent: ChildProcess; sessionId: string; }; function toArgs(value: string[] | string | undefined): string[] { if (!value) { return []; } return Array.isArray(value) ? value : [value]; } function buildServerArgs(opts: AcpClientOptions): string[] { const args = ["acp", ...toArgs(opts.serverArgs)]; if (opts.serverVerbose && !args.includes("--verbose") && !args.includes("-v")) { args.push("--verbose"); } return args; } function printSessionUpdate(notification: SessionNotification): void { const update = notification.update; if (!("sessionUpdate" in update)) { return; } switch (update.sessionUpdate) { case "agent_message_chunk": { if (update.content?.type === "text") { process.stdout.write(update.content.text); } return; } case "tool_call": { console.log(`\n[tool] ${update.title} (${update.status})`); return; } case "tool_call_update": { if (update.status) { console.log(`[tool update] ${update.toolCallId}: ${update.status}`); } return; } case "available_commands_update": { const names = update.availableCommands?.map((cmd) => `/${cmd.name}`).join(" "); if (names) { console.log(`\n[commands] ${names}`); } return; } default: return; } } export async function createAcpClient(opts: AcpClientOptions = {}): Promise { const cwd = opts.cwd ?? process.cwd(); const verbose = Boolean(opts.verbose); const log = verbose ? (msg: string) => console.error(`[acp-client] ${msg}`) : () => {}; ensureOpenClawCliOnPath({ cwd }); const serverCommand = opts.serverCommand ?? "openclaw"; const serverArgs = buildServerArgs(opts); log(`spawning: ${serverCommand} ${serverArgs.join(" ")}`); const agent = spawn(serverCommand, serverArgs, { stdio: ["pipe", "pipe", "inherit"], cwd, }); if (!agent.stdin || !agent.stdout) { throw new Error("Failed to create ACP stdio pipes"); } const input = Writable.toWeb(agent.stdin); const output = Readable.toWeb(agent.stdout) as unknown as ReadableStream; const stream = ndJsonStream(input, output); const client = new ClientSideConnection( () => ({ sessionUpdate: async (params: SessionNotification) => { printSessionUpdate(params); }, requestPermission: async (params: RequestPermissionRequest) => { console.log("\n[permission requested]", params.toolCall?.title ?? "tool"); const options = params.options ?? []; const allowOnce = options.find((option) => option.kind === "allow_once"); const fallback = options[0]; return { outcome: { outcome: "selected", optionId: allowOnce?.optionId ?? fallback?.optionId ?? "allow", }, }; }, }), stream, ); log("initializing"); await client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: true, }, clientInfo: { name: "openclaw-acp-client", version: "1.0.0" }, }); log("creating session"); const session = await client.newSession({ cwd, mcpServers: [], }); return { client, agent, sessionId: session.sessionId, }; } export async function runAcpClientInteractive(opts: AcpClientOptions = {}): Promise { const { client, agent, sessionId } = await createAcpClient(opts); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); console.log("OpenClaw ACP client"); console.log(`Session: ${sessionId}`); console.log('Type a prompt, or "exit" to quit.\n'); const prompt = () => { rl.question("> ", async (input) => { const text = input.trim(); if (!text) { prompt(); return; } if (text === "exit" || text === "quit") { agent.kill(); rl.close(); process.exit(0); } try { const response = await client.prompt({ sessionId, prompt: [{ type: "text", text }], }); console.log(`\n[${response.stopReason}]\n`); } catch (err) { console.error(`\n[error] ${String(err)}\n`); } prompt(); }); }; prompt(); agent.on("exit", (code) => { console.log(`\nAgent exited with code ${code ?? 0}`); rl.close(); process.exit(code ?? 0); }); }