diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..81229e8 Binary files /dev/null and b/.DS_Store differ diff --git a/src/acp/client.ts b/src/acp/client.ts deleted file mode 100644 index e1b8697..0000000 --- a/src/acp/client.ts +++ /dev/null @@ -1,191 +0,0 @@ -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); - }); -} diff --git a/src/acp/commands.ts b/src/acp/commands.ts deleted file mode 100644 index 6bd8e85..0000000 --- a/src/acp/commands.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { AvailableCommand } from "@agentclientprotocol/sdk"; - -export function getAvailableCommands(): AvailableCommand[] { - return [ - { name: "help", description: "Show help and common commands." }, - { name: "commands", description: "List available commands." }, - { name: "status", description: "Show current status." }, - { - name: "context", - description: "Explain context usage (list|detail|json).", - input: { hint: "list | detail | json" }, - }, - { name: "whoami", description: "Show sender id (alias: /id)." }, - { name: "id", description: "Alias for /whoami." }, - { name: "subagents", description: "List or manage sub-agents." }, - { name: "config", description: "Read or write config (owner-only)." }, - { name: "debug", description: "Set runtime-only overrides (owner-only)." }, - { name: "usage", description: "Toggle usage footer (off|tokens|full)." }, - { name: "stop", description: "Stop the current run." }, - { name: "restart", description: "Restart the gateway (if enabled)." }, - { name: "dock-telegram", description: "Route replies to Telegram." }, - { name: "dock-discord", description: "Route replies to Discord." }, - { name: "dock-slack", description: "Route replies to Slack." }, - { name: "activation", description: "Set group activation (mention|always)." }, - { name: "send", description: "Set send mode (on|off|inherit)." }, - { name: "reset", description: "Reset the session (/new)." }, - { name: "new", description: "Reset the session (/reset)." }, - { - name: "think", - description: "Set thinking level (off|minimal|low|medium|high|xhigh).", - }, - { name: "verbose", description: "Set verbose mode (on|full|off)." }, - { name: "reasoning", description: "Toggle reasoning output (on|off|stream)." }, - { name: "elevated", description: "Toggle elevated mode (on|off)." }, - { name: "model", description: "Select a model (list|status|)." }, - { name: "queue", description: "Adjust queue mode and options." }, - { name: "bash", description: "Run a host command (if enabled)." }, - { name: "compact", description: "Compact the session history." }, - ]; -} diff --git a/src/acp/event-mapper.test.ts b/src/acp/event-mapper.test.ts deleted file mode 100644 index 0b7682e..0000000 --- a/src/acp/event-mapper.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { extractAttachmentsFromPrompt, extractTextFromPrompt } from "./event-mapper.js"; - -describe("acp event mapper", () => { - it("extracts text and resource blocks into prompt text", () => { - const text = extractTextFromPrompt([ - { type: "text", text: "Hello" }, - { type: "resource", resource: { text: "File contents" } }, - { type: "resource_link", uri: "https://example.com", title: "Spec" }, - { type: "image", data: "abc", mimeType: "image/png" }, - ]); - - expect(text).toBe("Hello\nFile contents\n[Resource link (Spec)] https://example.com"); - }); - - it("extracts image blocks into gateway attachments", () => { - const attachments = extractAttachmentsFromPrompt([ - { type: "image", data: "abc", mimeType: "image/png" }, - { type: "image", data: "", mimeType: "image/png" }, - { type: "text", text: "ignored" }, - ]); - - expect(attachments).toEqual([ - { - type: "image", - mimeType: "image/png", - content: "abc", - }, - ]); - }); -}); diff --git a/src/acp/event-mapper.ts b/src/acp/event-mapper.ts deleted file mode 100644 index 5e1179f..0000000 --- a/src/acp/event-mapper.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { ContentBlock, ImageContent, ToolKind } from "@agentclientprotocol/sdk"; - -export type GatewayAttachment = { - type: string; - mimeType: string; - content: string; -}; - -export function extractTextFromPrompt(prompt: ContentBlock[]): string { - const parts: string[] = []; - for (const block of prompt) { - if (block.type === "text") { - parts.push(block.text); - continue; - } - if (block.type === "resource") { - const resource = block.resource as { text?: string } | undefined; - if (resource?.text) { - parts.push(resource.text); - } - continue; - } - if (block.type === "resource_link") { - const title = block.title ? ` (${block.title})` : ""; - const uri = block.uri ?? ""; - const line = uri ? `[Resource link${title}] ${uri}` : `[Resource link${title}]`; - parts.push(line); - } - } - return parts.join("\n"); -} - -export function extractAttachmentsFromPrompt(prompt: ContentBlock[]): GatewayAttachment[] { - const attachments: GatewayAttachment[] = []; - for (const block of prompt) { - if (block.type !== "image") { - continue; - } - const image = block as ImageContent; - if (!image.data || !image.mimeType) { - continue; - } - attachments.push({ - type: "image", - mimeType: image.mimeType, - content: image.data, - }); - } - return attachments; -} - -export function formatToolTitle( - name: string | undefined, - args: Record | undefined, -): string { - const base = name ?? "tool"; - if (!args || Object.keys(args).length === 0) { - return base; - } - const parts = Object.entries(args).map(([key, value]) => { - const raw = typeof value === "string" ? value : JSON.stringify(value); - const safe = raw.length > 100 ? `${raw.slice(0, 100)}...` : raw; - return `${key}: ${safe}`; - }); - return `${base}: ${parts.join(", ")}`; -} - -export function inferToolKind(name?: string): ToolKind { - if (!name) { - return "other"; - } - const normalized = name.toLowerCase(); - if (normalized.includes("read")) { - return "read"; - } - if (normalized.includes("write") || normalized.includes("edit")) { - return "edit"; - } - if (normalized.includes("delete") || normalized.includes("remove")) { - return "delete"; - } - if (normalized.includes("move") || normalized.includes("rename")) { - return "move"; - } - if (normalized.includes("search") || normalized.includes("find")) { - return "search"; - } - if (normalized.includes("exec") || normalized.includes("run") || normalized.includes("bash")) { - return "execute"; - } - if (normalized.includes("fetch") || normalized.includes("http")) { - return "fetch"; - } - return "other"; -} diff --git a/src/acp/index.ts b/src/acp/index.ts deleted file mode 100644 index 6af9eff..0000000 --- a/src/acp/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { serveAcpGateway } from "./server.js"; -export { createInMemorySessionStore } from "./session.js"; -export type { AcpSessionStore } from "./session.js"; -export type { AcpServerOptions } from "./types.js"; diff --git a/src/acp/meta.ts b/src/acp/meta.ts deleted file mode 100644 index eccd865..0000000 --- a/src/acp/meta.ts +++ /dev/null @@ -1,47 +0,0 @@ -export function readString( - meta: Record | null | undefined, - keys: string[], -): string | undefined { - if (!meta) { - return undefined; - } - for (const key of keys) { - const value = meta[key]; - if (typeof value === "string" && value.trim()) { - return value.trim(); - } - } - return undefined; -} - -export function readBool( - meta: Record | null | undefined, - keys: string[], -): boolean | undefined { - if (!meta) { - return undefined; - } - for (const key of keys) { - const value = meta[key]; - if (typeof value === "boolean") { - return value; - } - } - return undefined; -} - -export function readNumber( - meta: Record | null | undefined, - keys: string[], -): number | undefined { - if (!meta) { - return undefined; - } - for (const key of keys) { - const value = meta[key]; - if (typeof value === "number" && Number.isFinite(value)) { - return value; - } - } - return undefined; -} diff --git a/src/acp/server.ts b/src/acp/server.ts deleted file mode 100644 index 4a2c835..0000000 --- a/src/acp/server.ts +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env node -import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk"; -import { Readable, Writable } from "node:stream"; -import { fileURLToPath } from "node:url"; -import type { AcpServerOptions } from "./types.js"; -import { loadConfig } from "../config/config.js"; -import { resolveGatewayAuth } from "../gateway/auth.js"; -import { buildGatewayConnectionDetails } from "../gateway/call.js"; -import { GatewayClient } from "../gateway/client.js"; -import { isMainModule } from "../infra/is-main.js"; -import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; -import { AcpGatewayAgent } from "./translator.js"; - -export function serveAcpGateway(opts: AcpServerOptions = {}): void { - const cfg = loadConfig(); - const connection = buildGatewayConnectionDetails({ - config: cfg, - url: opts.gatewayUrl, - }); - - const isRemoteMode = cfg.gateway?.mode === "remote"; - const remote = isRemoteMode ? cfg.gateway?.remote : undefined; - const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, env: process.env }); - - const token = - opts.gatewayToken ?? - (isRemoteMode ? remote?.token?.trim() : undefined) ?? - process.env.OPENCLAW_GATEWAY_TOKEN ?? - auth.token; - const password = - opts.gatewayPassword ?? - (isRemoteMode ? remote?.password?.trim() : undefined) ?? - process.env.OPENCLAW_GATEWAY_PASSWORD ?? - auth.password; - - let agent: AcpGatewayAgent | null = null; - const gateway = new GatewayClient({ - url: connection.url, - token: token || undefined, - password: password || undefined, - clientName: GATEWAY_CLIENT_NAMES.CLI, - clientDisplayName: "ACP", - clientVersion: "acp", - mode: GATEWAY_CLIENT_MODES.CLI, - onEvent: (evt) => { - void agent?.handleGatewayEvent(evt); - }, - onHelloOk: () => { - agent?.handleGatewayReconnect(); - }, - onClose: (code, reason) => { - agent?.handleGatewayDisconnect(`${code}: ${reason}`); - }, - }); - - const input = Writable.toWeb(process.stdout); - const output = Readable.toWeb(process.stdin) as unknown as ReadableStream; - const stream = ndJsonStream(input, output); - - new AgentSideConnection((conn: AgentSideConnection) => { - agent = new AcpGatewayAgent(conn, gateway, opts); - agent.start(); - return agent; - }, stream); - - gateway.start(); -} - -function parseArgs(args: string[]): AcpServerOptions { - const opts: AcpServerOptions = {}; - for (let i = 0; i < args.length; i += 1) { - const arg = args[i]; - if (arg === "--url" || arg === "--gateway-url") { - opts.gatewayUrl = args[i + 1]; - i += 1; - continue; - } - if (arg === "--token" || arg === "--gateway-token") { - opts.gatewayToken = args[i + 1]; - i += 1; - continue; - } - if (arg === "--password" || arg === "--gateway-password") { - opts.gatewayPassword = args[i + 1]; - i += 1; - continue; - } - if (arg === "--session") { - opts.defaultSessionKey = args[i + 1]; - i += 1; - continue; - } - if (arg === "--session-label") { - opts.defaultSessionLabel = args[i + 1]; - i += 1; - continue; - } - if (arg === "--require-existing") { - opts.requireExistingSession = true; - continue; - } - if (arg === "--reset-session") { - opts.resetSession = true; - continue; - } - if (arg === "--no-prefix-cwd") { - opts.prefixCwd = false; - continue; - } - if (arg === "--verbose" || arg === "-v") { - opts.verbose = true; - continue; - } - if (arg === "--help" || arg === "-h") { - printHelp(); - process.exit(0); - } - } - return opts; -} - -function printHelp(): void { - console.log(`Usage: openclaw acp [options] - -Gateway-backed ACP server for IDE integration. - -Options: - --url Gateway WebSocket URL - --token Gateway auth token - --password Gateway auth password - --session Default session key (e.g. "agent:main:main") - --session-label