diff --git a/packages/cli/src/commands/agent/index.ts b/packages/cli/src/commands/agent/index.ts index ba2bbf770..8001642de 100644 --- a/packages/cli/src/commands/agent/index.ts +++ b/packages/cli/src/commands/agent/index.ts @@ -45,6 +45,10 @@ export function createAgentCommand(): Command { .option('--mode ', 'Provider-specific mode (e.g., plan, default, bypass)') .option('--cwd ', 'Working directory (default: current)') .option('--label ', 'Add label(s) to the agent (can be used multiple times)', collectMultiple, []) + .option( + '--wait-timeout ', + 'Maximum time to wait for agent to finish (e.g., 30s, 5m, 1h). Default: no limit' + ) .option('--output-schema ', 'Output JSON matching the provided schema file path or inline JSON schema') ).action(withOutput(runRunCommand)) diff --git a/packages/cli/src/commands/agent/run.ts b/packages/cli/src/commands/agent/run.ts index 19cf77b15..7c22cac2d 100644 --- a/packages/cli/src/commands/agent/run.ts +++ b/packages/cli/src/commands/agent/run.ts @@ -9,6 +9,7 @@ import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '. import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import { lookup } from 'mime-types' +import { parseDuration } from '../../utils/duration.js' /** Result type for agent run command */ export interface AgentRunResult { @@ -43,6 +44,7 @@ export interface AgentRunOptions extends CommandOptions { image?: string[] cwd?: string label?: string[] + waitTimeout?: string outputSchema?: string } @@ -223,6 +225,7 @@ export async function runRunCommand( ): Promise> { const host = getDaemonHost({ host: options.host as string | undefined }) const outputSchema = options.outputSchema ? loadOutputSchema(options.outputSchema) : undefined + let waitTimeoutMs = 0 // Validate prompt is provided if (!prompt || prompt.trim().length === 0) { @@ -254,6 +257,23 @@ export async function runRunCommand( throw error } + if (options.waitTimeout) { + try { + waitTimeoutMs = parseDuration(options.waitTimeout) + if (waitTimeoutMs <= 0) { + throw new Error('Timeout must be positive') + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + const error: CommandError = { + code: 'INVALID_TIMEOUT', + message: 'Invalid wait timeout value', + details: message, + } + throw error + } + } + const resolvedProviderModel = resolveProviderAndModel(options) let client @@ -359,7 +379,7 @@ export async function runRunCommand( await client.sendMessage(structuredAgent.id, structuredPrompt) } - const state = await client.waitForFinish(structuredAgent.id, 10 * 60 * 1000) + const state = await client.waitForFinish(structuredAgent.id, waitTimeoutMs) if (state.status === 'timeout') { throw new StructuredRunStatusError('timeout', 'Timed out waiting for structured output') } @@ -456,7 +476,7 @@ export async function runRunCommand( // Default run behavior is foreground: wait for completion unless --detach is set. if (!options.detach) { - const state = await client.waitForFinish(agent.id, 10 * 60 * 1000) + const state = await client.waitForFinish(agent.id, waitTimeoutMs) await client.close() const finalAgent = state.final ?? agent diff --git a/packages/cli/src/commands/agent/wait.ts b/packages/cli/src/commands/agent/wait.ts index 73ec33095..8b3aee813 100644 --- a/packages/cli/src/commands/agent/wait.ts +++ b/packages/cli/src/commands/agent/wait.ts @@ -2,6 +2,7 @@ import type { Command } from 'commander' import { connectToDaemon, getDaemonHost } from '../../utils/client.js' import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js' import { fetchAgentTimelineItems, formatAgentActivityTranscript } from './logs.js' +import { parseDuration } from '../../utils/duration.js' /** Result type for agent wait command */ export interface AgentWaitResult { @@ -47,50 +48,6 @@ async function getRecentActivityTranscript( } } -/** - * Parse duration string to milliseconds. - * Supports formats like: 5m, 30s, 1h, 2h30m, 90, etc. - * If no unit is specified, assumes seconds. - */ -function parseDuration(input: string): number { - const trimmed = input.trim() - - // If it's just a number, treat as seconds - if (/^\d+$/.test(trimmed)) { - return parseInt(trimmed, 10) * 1000 - } - - // Parse duration with units - let totalMs = 0 - const regex = /(\d+)([smh])/g - let match - let hasMatch = false - - while ((match = regex.exec(trimmed)) !== null) { - hasMatch = true - const value = parseInt(match[1], 10) - const unit = match[2] - - switch (unit) { - case 's': - totalMs += value * 1000 - break - case 'm': - totalMs += value * 60 * 1000 - break - case 'h': - totalMs += value * 60 * 60 * 1000 - break - } - } - - if (!hasMatch) { - throw new Error(`Invalid duration format: ${input}. Use formats like: 5m, 30s, 1h, 2h30m`) - } - - return totalMs -} - export async function runWaitCommand( agentIdArg: string, options: AgentWaitOptions, diff --git a/packages/cli/src/utils/duration.ts b/packages/cli/src/utils/duration.ts new file mode 100644 index 000000000..2c439a750 --- /dev/null +++ b/packages/cli/src/utils/duration.ts @@ -0,0 +1,43 @@ +/** + * Parse duration string to milliseconds. + * Supports formats like: 5m, 30s, 1h, 2h30m, 90, etc. + * If no unit is specified, assumes seconds. + */ +export function parseDuration(input: string): number { + const trimmed = input.trim() + + // If it's just a number, treat as seconds + if (/^\d+$/.test(trimmed)) { + return parseInt(trimmed, 10) * 1000 + } + + // Parse duration with units + let totalMs = 0 + const regex = /(\d+)([smh])/g + let match + let hasMatch = false + + while ((match = regex.exec(trimmed)) !== null) { + hasMatch = true + const value = parseInt(match[1], 10) + const unit = match[2] + + switch (unit) { + case 's': + totalMs += value * 1000 + break + case 'm': + totalMs += value * 60 * 1000 + break + case 'h': + totalMs += value * 60 * 60 * 1000 + break + } + } + + if (!hasMatch) { + throw new Error(`Invalid duration format: ${input}. Use formats like: 5m, 30s, 1h, 2h30m`) + } + + return totalMs +}