| import type { Command } from "commander"; |
| import { runAcpClientInteractive } from "../acp/client.js"; |
| import { readSecretFromFile } from "../acp/secret-file.js"; |
| import { serveAcpGateway } from "../acp/server.js"; |
| import { normalizeAcpProvenanceMode } from "../acp/types.js"; |
| import { defaultRuntime } from "../runtime.js"; |
| import { formatDocsLink } from "../terminal/links.js"; |
| import { theme } from "../terminal/theme.js"; |
| import { inheritOptionFromParent } from "./command-options.js"; |
|
|
| function resolveSecretOption(params: { |
| direct?: string; |
| file?: string; |
| directFlag: string; |
| fileFlag: string; |
| label: string; |
| }) { |
| const direct = params.direct?.trim(); |
| const file = params.file?.trim(); |
| if (direct && file) { |
| throw new Error(`Use either ${params.directFlag} or ${params.fileFlag} for ${params.label}.`); |
| } |
| if (file) { |
| return readSecretFromFile(file, params.label); |
| } |
| return direct || undefined; |
| } |
|
|
| function warnSecretCliFlag(flag: "--token" | "--password") { |
| defaultRuntime.error( |
| `Warning: ${flag} can be exposed via process listings. Prefer ${flag}-file or environment variables.`, |
| ); |
| } |
|
|
| export function registerAcpCli(program: Command) { |
| const acp = program.command("acp").description("Run an ACP bridge backed by the Gateway"); |
|
|
| acp |
| .option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)") |
| .option("--token <token>", "Gateway token (if required)") |
| .option("--token-file <path>", "Read gateway token from file") |
| .option("--password <password>", "Gateway password (if required)") |
| .option("--password-file <path>", "Read gateway password from file") |
| .option("--session <key>", "Default session key (e.g. agent:main:main)") |
| .option("--session-label <label>", "Default session label to resolve") |
| .option("--require-existing", "Fail if the session key/label does not exist", false) |
| .option("--reset-session", "Reset the session key before first use", false) |
| .option("--no-prefix-cwd", "Do not prefix prompts with the working directory", false) |
| .option("--provenance <mode>", "ACP provenance mode: off, meta, or meta+receipt") |
| .option("-v, --verbose", "Verbose logging to stderr", false) |
| .addHelpText( |
| "after", |
| () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/acp", "docs.openclaw.ai/cli/acp")}\n`, |
| ) |
| .action(async (opts) => { |
| try { |
| const gatewayToken = resolveSecretOption({ |
| direct: opts.token as string | undefined, |
| file: opts.tokenFile as string | undefined, |
| directFlag: "--token", |
| fileFlag: "--token-file", |
| label: "Gateway token", |
| }); |
| const gatewayPassword = resolveSecretOption({ |
| direct: opts.password as string | undefined, |
| file: opts.passwordFile as string | undefined, |
| directFlag: "--password", |
| fileFlag: "--password-file", |
| label: "Gateway password", |
| }); |
| if (opts.token) { |
| warnSecretCliFlag("--token"); |
| } |
| if (opts.password) { |
| warnSecretCliFlag("--password"); |
| } |
| const provenanceMode = normalizeAcpProvenanceMode(opts.provenance as string | undefined); |
| if (opts.provenance && !provenanceMode) { |
| throw new Error("Invalid --provenance value. Use off, meta, or meta+receipt."); |
| } |
| await serveAcpGateway({ |
| gatewayUrl: opts.url as string | undefined, |
| gatewayToken, |
| gatewayPassword, |
| defaultSessionKey: opts.session as string | undefined, |
| defaultSessionLabel: opts.sessionLabel as string | undefined, |
| requireExistingSession: Boolean(opts.requireExisting), |
| resetSession: Boolean(opts.resetSession), |
| prefixCwd: !opts.noPrefixCwd, |
| provenanceMode, |
| verbose: Boolean(opts.verbose), |
| }); |
| } catch (err) { |
| defaultRuntime.error(String(err)); |
| defaultRuntime.exit(1); |
| } |
| }); |
|
|
| acp |
| .command("client") |
| .description("Run an interactive ACP client against the local ACP bridge") |
| .option("--cwd <dir>", "Working directory for the ACP session") |
| .option("--server <command>", "ACP server command (default: openclaw)") |
| .option("--server-args <args...>", "Extra arguments for the ACP server") |
| .option("--server-verbose", "Enable verbose logging on the ACP server", false) |
| .option("-v, --verbose", "Verbose client logging", false) |
| .action(async (opts, command) => { |
| const inheritedVerbose = inheritOptionFromParent<boolean>(command, "verbose"); |
| try { |
| await runAcpClientInteractive({ |
| cwd: opts.cwd as string | undefined, |
| serverCommand: opts.server as string | undefined, |
| serverArgs: opts.serverArgs as string[] | undefined, |
| serverVerbose: Boolean(opts.serverVerbose), |
| verbose: Boolean(opts.verbose || inheritedVerbose), |
| }); |
| } catch (err) { |
| defaultRuntime.error(String(err)); |
| defaultRuntime.exit(1); |
| } |
| }); |
| } |
|
|