From 193c723714ff34458b6135b86cbe8f7c18c180d7 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 14 Feb 2026 01:23:01 +0700 Subject: [PATCH] refactor(cli): simplify agent mode command control flow Context from recent agent work - Prior commits focused on voice/background model gating, speech resolver typing, and timeline cursor loading. - This change targets a separate CLI command path to avoid overlap. What changed - Normalized the optional mode argument once (`mode?.trim()`) and reused it. - Removed duplicated `client.close()` calls and centralized cleanup in a `finally` block. - Typed the daemon client variable explicitly (`Awaited> | undefined`). - Kept user-facing behavior and error codes stable, including DAEMON_NOT_RUNNING and MODE_OPERATION_FAILED. Reasoning - The previous implementation duplicated lifecycle handling and relied on non-null assertions for mode values. - Centralized resource cleanup lowers leak risk and makes control flow easier to audit. - This is a behavior-preserving refactor aligned with the refactor skill contract. Verification - `npm run typecheck` - `npx tsx packages/cli/tests/10-agent-mode.test.ts` Accomplishments for next agent - `runModeCommand` now has a single cleanup path and clearer branching for list vs set mode. - Mode argument handling is explicit and trimmed before use. Challenges / follow-up - CLI output typing still requires `AnyCommandResult` for mixed-shape commands due the current `withOutput` generic design. - If desired, a future refactor can redesign output typing to support per-branch result schemas without `any`. --- packages/cli/src/commands/agent/mode.ts | 51 +++++++++++++++---------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/packages/cli/src/commands/agent/mode.ts b/packages/cli/src/commands/agent/mode.ts index d30bd8b86..d567e903f 100644 --- a/packages/cli/src/commands/agent/mode.ts +++ b/packages/cli/src/commands/agent/mode.ts @@ -37,6 +37,8 @@ export interface AgentModeOptions extends CommandOptions { list?: boolean } +// This command returns two different data shapes (set result vs mode list). +// Keep `any` here to match the existing output wrapper generic contract. // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AgentModeResult = AnyCommandResult @@ -46,10 +48,11 @@ export async function runModeCommand( options: AgentModeOptions, _command: Command ): Promise { + const normalizedMode = mode?.trim() const host = getDaemonHost({ host: options.host as string | undefined }) // Validate arguments - if (!options.list && !mode) { + if (!options.list && !normalizedMode) { const error: CommandError = { code: 'MISSING_ARGUMENT', message: 'Mode argument required unless --list is specified', @@ -58,20 +61,9 @@ export async function runModeCommand( throw error } - let client + let client: Awaited> | undefined try { client = await connectToDaemon({ host: options.host as string | undefined }) - } catch (err) { - const message = err instanceof Error ? err.message : String(err) - const error: CommandError = { - code: 'DAEMON_NOT_RUNNING', - message: `Cannot connect to daemon at ${host}: ${message}`, - details: 'Start the daemon with: paseo daemon start', - } - throw error - } - - try { const agent = await client.fetchAgent(id) if (!agent) { const error: CommandError = { @@ -87,8 +79,6 @@ export async function runModeCommand( // List available modes for this agent const availableModes = agent.availableModes ?? [] - await client.close() - const items: AgentMode[] = availableModes.map((m) => ({ id: m.id, label: m.label, @@ -101,31 +91,52 @@ export async function runModeCommand( schema: modeListSchema, } } else { - // Set the agent mode - await client.setAgentMode(resolvedId, mode!) + if (!normalizedMode) { + const error: CommandError = { + code: 'MISSING_ARGUMENT', + message: 'Mode argument required unless --list is specified', + details: 'Usage: paseo agent mode | paseo agent mode --list ', + } + throw error + } - await client.close() + // Set the agent mode + await client.setAgentMode(resolvedId, normalizedMode) return { type: 'single', data: { agentId: resolvedId.slice(0, 7), - mode: mode!, + mode: normalizedMode, }, schema: setModeSchema, } } } catch (err) { - await client.close().catch(() => {}) // Re-throw if it's already a CommandError if (err && typeof err === 'object' && 'code' in err) { throw err } + + if (!client) { + const message = err instanceof Error ? err.message : String(err) + const error: CommandError = { + code: 'DAEMON_NOT_RUNNING', + message: `Cannot connect to daemon at ${host}: ${message}`, + details: 'Start the daemon with: paseo daemon start', + } + throw error + } + const message = err instanceof Error ? err.message : String(err) const error: CommandError = { code: 'MODE_OPERATION_FAILED', message: `Failed to ${options.list ? 'list modes' : 'set mode'}: ${message}`, } throw error + } finally { + if (client) { + await client.close().catch(() => {}) + } } }