From b1fc9d6998fde4967289cc2e4649c3c34ab9835b Mon Sep 17 00:00:00 2001 From: taosu Date: Sat, 28 Feb 2026 15:44:55 +0800 Subject: [PATCH] feat: add --session-meta and --claude-agent CLI options Allow passing _meta to ACP newSession requests, enabling adapter-specific configuration from the CLI. --session-meta : generic JSON passthrough set as _meta on newSession --claude-agent : sugar for claude-agent-acp, builds { claudeCode: { options: { agent: "" } } } Both options can be combined; --claude-agent deep-merges into --session-meta. Supported on exec, sessions new, and sessions ensure commands. --- src/cli.ts | 70 +++++++++++++++++++++++++++++++++++++++++- src/client.ts | 1 + src/session-runtime.ts | 6 ++++ src/types.ts | 1 + 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index c4640ce..ee8163e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -83,6 +83,8 @@ type GlobalFlags = PermissionFlags & { ttl: number; verbose?: boolean; format: OutputFormat; + sessionMeta?: string; + claudeAgent?: string; }; type PromptFlags = { @@ -315,7 +317,15 @@ function addGlobalFlags(command: Command): Command { "Queue owner idle TTL before shutdown (0 = keep alive forever) (default: 300)", parseTtlSeconds, ) - .option("--verbose", "Enable verbose debug logs"); + .option("--verbose", "Enable verbose debug logs") + .option( + "--session-meta ", + "JSON metadata to pass as _meta in newSession request", + ) + .option( + "--claude-agent ", + "Agent name for claude-agent-acp (shorthand for --session-meta)", + ); } function addSessionOption(command: Command): Command { @@ -400,9 +410,64 @@ function resolveGlobalFlags(command: Command, config: ResolvedAcpxConfig): Globa approveAll: opts.approveAll ? true : undefined, approveReads: opts.approveReads ? true : undefined, denyAll: opts.denyAll ? true : undefined, + sessionMeta: opts.sessionMeta, + claudeAgent: opts.claudeAgent, }; } +function resolveSessionMeta( + globalFlags: GlobalFlags, +): Record | undefined { + let meta: Record | undefined; + + if (globalFlags.sessionMeta) { + try { + meta = JSON.parse(globalFlags.sessionMeta) as Record; + } catch { + throw new InvalidArgumentError("--session-meta must be valid JSON"); + } + if (typeof meta !== "object" || meta === null || Array.isArray(meta)) { + throw new InvalidArgumentError("--session-meta must be a JSON object"); + } + } + + if (globalFlags.claudeAgent) { + const claudeMeta = { + claudeCode: { options: { agent: globalFlags.claudeAgent } }, + }; + meta = meta ? deepMerge(meta, claudeMeta) : claudeMeta; + } + + return meta; +} + +function deepMerge( + target: Record, + source: Record, +): Record { + const result = { ...target }; + for (const key of Object.keys(source)) { + const srcVal = source[key]; + const tgtVal = result[key]; + if ( + typeof srcVal === "object" && + srcVal !== null && + !Array.isArray(srcVal) && + typeof tgtVal === "object" && + tgtVal !== null && + !Array.isArray(tgtVal) + ) { + result[key] = deepMerge( + tgtVal as Record, + srcVal as Record, + ); + } else { + result[key] = srcVal; + } + } + return result; +} + function resolveOutputPolicy(format: OutputFormat, jsonStrict: boolean): OutputPolicy { return { format, @@ -786,6 +851,7 @@ async function handleExec( nonInteractivePermissions: globalFlags.nonInteractivePermissions, authCredentials: config.auth, authPolicy: globalFlags.authPolicy, + sessionMeta: resolveSessionMeta(globalFlags), outputFormatter, suppressSdkConsoleErrors: outputPolicy.suppressSdkConsoleErrors, timeoutMs: globalFlags.timeout, @@ -1050,6 +1116,7 @@ async function handleSessionsNew( nonInteractivePermissions: globalFlags.nonInteractivePermissions, authCredentials: config.auth, authPolicy: globalFlags.authPolicy, + sessionMeta: resolveSessionMeta(globalFlags), timeoutMs: globalFlags.timeout, verbose: globalFlags.verbose, }); @@ -1086,6 +1153,7 @@ async function handleSessionsEnsure( nonInteractivePermissions: globalFlags.nonInteractivePermissions, authCredentials: config.auth, authPolicy: globalFlags.authPolicy, + sessionMeta: resolveSessionMeta(globalFlags), timeoutMs: globalFlags.timeout, verbose: globalFlags.verbose, }); diff --git a/src/client.ts b/src/client.ts index 4e7d47d..4a7a8a2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -533,6 +533,7 @@ export class AcpClient { const result = await connection.newSession({ cwd: asAbsoluteCwd(cwd), mcpServers: [], + ...(this.options.sessionMeta ? { _meta: this.options.sessionMeta } : {}), }); return { sessionId: result.sessionId, diff --git a/src/session-runtime.ts b/src/session-runtime.ts index 9fc8935..6ebae5e 100644 --- a/src/session-runtime.ts +++ b/src/session-runtime.ts @@ -94,6 +94,7 @@ export type RunOnceOptions = { nonInteractivePermissions?: NonInteractivePermissionPolicy; authCredentials?: Record; authPolicy?: AuthPolicy; + sessionMeta?: Record; outputFormatter: OutputFormatter; suppressSdkConsoleErrors?: boolean; verbose?: boolean; @@ -107,6 +108,7 @@ export type SessionCreateOptions = { nonInteractivePermissions?: NonInteractivePermissionPolicy; authCredentials?: Record; authPolicy?: AuthPolicy; + sessionMeta?: Record; verbose?: boolean; } & TimedRunOptions; @@ -134,6 +136,7 @@ export type SessionEnsureOptions = { nonInteractivePermissions?: NonInteractivePermissionPolicy; authCredentials?: Record; authPolicy?: AuthPolicy; + sessionMeta?: Record; verbose?: boolean; walkBoundary?: string; } & TimedRunOptions; @@ -785,6 +788,7 @@ export async function runOnce(options: RunOnceOptions): Promise nonInteractivePermissions: options.nonInteractivePermissions, authCredentials: options.authCredentials, authPolicy: options.authPolicy, + sessionMeta: options.sessionMeta, suppressSdkConsoleErrors: options.suppressSdkConsoleErrors, verbose: options.verbose, onSessionUpdate: (notification) => output.onSessionUpdate(notification), @@ -832,6 +836,7 @@ export async function createSession( nonInteractivePermissions: options.nonInteractivePermissions, authCredentials: options.authCredentials, authPolicy: options.authPolicy, + sessionMeta: options.sessionMeta, verbose: options.verbose, }); @@ -904,6 +909,7 @@ export async function ensureSession( nonInteractivePermissions: options.nonInteractivePermissions, authCredentials: options.authCredentials, authPolicy: options.authPolicy, + sessionMeta: options.sessionMeta, timeoutMs: options.timeoutMs, verbose: options.verbose, }); diff --git a/src/types.ts b/src/types.ts index 38d2985..65e27a5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -245,6 +245,7 @@ export type AcpClientOptions = { nonInteractivePermissions?: NonInteractivePermissionPolicy; authCredentials?: Record; authPolicy?: AuthPolicy; + sessionMeta?: Record; suppressSdkConsoleErrors?: boolean; verbose?: boolean; onSessionUpdate?: (notification: SessionNotification) => void;