mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
refactor: use --json flag alias instead of --format json throughout CLI
This commit is contained in:
@@ -380,7 +380,7 @@ async function renderStream<T>(
|
|||||||
|
|
||||||
### NDJSON for Streaming
|
### NDJSON for Streaming
|
||||||
|
|
||||||
When `--format json` is used with streaming commands, output is newline-delimited JSON (NDJSON) for easy parsing:
|
When `--json` (or `--format json`) is used with streaming commands, output is newline-delimited JSON (NDJSON) for easy parsing:
|
||||||
|
|
||||||
```
|
```
|
||||||
{"timestamp":"2024-01-15T10:30:00Z","type":"stdout","content":"Hello"}
|
{"timestamp":"2024-01-15T10:30:00Z","type":"stdout","content":"Hello"}
|
||||||
@@ -451,8 +451,8 @@ E2E tests can verify both structured data (for correctness) and formatted output
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Verify JSON output is valid and contains expected data
|
// Verify JSON output is valid and contains expected data
|
||||||
test('agent list --format json', async () => {
|
test('agent list --json', async () => {
|
||||||
const output = await ctx.paseo('agent list --format json')
|
const output = await ctx.paseo('agent list --json')
|
||||||
const data = JSON.parse(output.stdout)
|
const data = JSON.parse(output.stdout)
|
||||||
|
|
||||||
expect(data).toBeInstanceOf(Array)
|
expect(data).toBeInstanceOf(Array)
|
||||||
@@ -475,7 +475,8 @@ Add output options to the root command:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
program
|
program
|
||||||
.option('-f, --format <format>', 'Output format: table, json, yaml', 'table')
|
.option('-o, --format <format>', 'Output format: table, json, yaml', 'table')
|
||||||
|
.option('--json', 'Output in JSON format (alias for --format json)')
|
||||||
.option('-q, --quiet', 'Minimal output (IDs only)')
|
.option('-q, --quiet', 'Minimal output (IDs only)')
|
||||||
.option('--no-headers', 'Omit table headers')
|
.option('--no-headers', 'Omit table headers')
|
||||||
.option('--no-color', 'Disable colored output')
|
.option('--no-color', 'Disable colored output')
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export function createCli(): Command {
|
|||||||
.description('Paseo CLI - control your AI coding agents from the command line')
|
.description('Paseo CLI - control your AI coding agents from the command line')
|
||||||
.version(VERSION, '-v, --version', 'output the version number')
|
.version(VERSION, '-v, --version', 'output the version number')
|
||||||
// Global output options
|
// Global output options
|
||||||
.option('-f, --format <format>', 'output format: table, json, yaml', 'table')
|
.option('-o, --format <format>', 'output format: table, json, yaml', 'table')
|
||||||
|
.option('--json', 'output in JSON format (alias for --format json)')
|
||||||
.option('-q, --quiet', 'minimal output (IDs only)')
|
.option('-q, --quiet', 'minimal output (IDs only)')
|
||||||
.option('--no-headers', 'omit table headers')
|
.option('--no-headers', 'omit table headers')
|
||||||
.option('--no-color', 'disable colored output')
|
.option('--no-color', 'disable colored output')
|
||||||
@@ -47,12 +48,7 @@ export function createCli(): Command {
|
|||||||
.option('--ui', 'Show only UI agents (equivalent to --label ui=true)')
|
.option('--ui', 'Show only UI agents (equivalent to --label ui=true)')
|
||||||
.option('--json', 'Output in JSON format')
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action((options, command) => {
|
.action(withOutput(runLsCommand))
|
||||||
if (options.json) {
|
|
||||||
command.parent.opts().format = 'json'
|
|
||||||
}
|
|
||||||
return withOutput(runLsCommand)(options, command)
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('run')
|
.command('run')
|
||||||
@@ -69,6 +65,7 @@ export function createCli(): Command {
|
|||||||
.option('--cwd <path>', 'Working directory (default: current)')
|
.option('--cwd <path>', 'Working directory (default: current)')
|
||||||
.option('--label <key=value>', 'Add label(s) to the agent (can be used multiple times)', collectMultiple, [])
|
.option('--label <key=value>', 'Add label(s) to the agent (can be used multiple times)', collectMultiple, [])
|
||||||
.option('--ui', 'Mark as UI agent (equivalent to --label ui=true)')
|
.option('--ui', 'Mark as UI agent (equivalent to --label ui=true)')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runRunCommand))
|
.action(withOutput(runRunCommand))
|
||||||
|
|
||||||
@@ -96,6 +93,7 @@ export function createCli(): Command {
|
|||||||
.argument('[id]', 'Agent ID (or prefix) - optional if --all or --cwd specified')
|
.argument('[id]', 'Agent ID (or prefix) - optional if --all or --cwd specified')
|
||||||
.option('--all', 'Stop all agents')
|
.option('--all', 'Stop all agents')
|
||||||
.option('--cwd <path>', 'Stop all agents in directory')
|
.option('--cwd <path>', 'Stop all agents in directory')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runStopCommand))
|
.action(withOutput(runStopCommand))
|
||||||
|
|
||||||
@@ -106,6 +104,7 @@ export function createCli(): Command {
|
|||||||
.argument('<prompt>', 'The message to send')
|
.argument('<prompt>', 'The message to send')
|
||||||
.option('--no-wait', 'Return immediately without waiting for completion')
|
.option('--no-wait', 'Return immediately without waiting for completion')
|
||||||
.option('--image <path>', 'Attach image(s) to the message', collectMultiple, [])
|
.option('--image <path>', 'Attach image(s) to the message', collectMultiple, [])
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runSendCommand))
|
.action(withOutput(runSendCommand))
|
||||||
|
|
||||||
@@ -115,18 +114,14 @@ export function createCli(): Command {
|
|||||||
.argument('<id>', 'Agent ID (or prefix)')
|
.argument('<id>', 'Agent ID (or prefix)')
|
||||||
.option('--json', 'Output in JSON format')
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action((id, options, command) => {
|
.action(withOutput(runInspectCommand))
|
||||||
if (options.json) {
|
|
||||||
command.parent.opts().format = 'json'
|
|
||||||
}
|
|
||||||
return withOutput(runInspectCommand)(id, options, command)
|
|
||||||
})
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('wait')
|
.command('wait')
|
||||||
.description('Wait for an agent to become idle')
|
.description('Wait for an agent to become idle')
|
||||||
.argument('<id>', 'Agent ID (or prefix)')
|
.argument('<id>', 'Agent ID (or prefix)')
|
||||||
.option('--timeout <seconds>', 'Maximum wait time (default: 600)')
|
.option('--timeout <seconds>', 'Maximum wait time (default: 600)')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runWaitCommand))
|
.action(withOutput(runWaitCommand))
|
||||||
|
|
||||||
|
|||||||
@@ -29,12 +29,7 @@ export function createAgentCommand(): Command {
|
|||||||
.option('--ui', 'Show only UI agents (equivalent to --label ui=true)')
|
.option('--ui', 'Show only UI agents (equivalent to --label ui=true)')
|
||||||
.option('--json', 'Output in JSON format')
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action((options, command) => {
|
.action(withOutput(runLsCommand))
|
||||||
if (options.json) {
|
|
||||||
command.parent.parent.opts().format = 'json'
|
|
||||||
}
|
|
||||||
return withOutput(runLsCommand)(options, command)
|
|
||||||
})
|
|
||||||
|
|
||||||
agent
|
agent
|
||||||
.command('run')
|
.command('run')
|
||||||
@@ -48,6 +43,7 @@ export function createAgentCommand(): Command {
|
|||||||
.option('--cwd <path>', 'Working directory (default: current)')
|
.option('--cwd <path>', 'Working directory (default: current)')
|
||||||
.option('--label <key=value>', 'Add label(s) to the agent (can be used multiple times)', collectMultiple, [])
|
.option('--label <key=value>', 'Add label(s) to the agent (can be used multiple times)', collectMultiple, [])
|
||||||
.option('--ui', 'Mark as UI agent (equivalent to --label ui=true)')
|
.option('--ui', 'Mark as UI agent (equivalent to --label ui=true)')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runRunCommand))
|
.action(withOutput(runRunCommand))
|
||||||
|
|
||||||
@@ -74,6 +70,7 @@ export function createAgentCommand(): Command {
|
|||||||
.argument('[id]', 'Agent ID (or prefix) - optional if --all or --cwd specified')
|
.argument('[id]', 'Agent ID (or prefix) - optional if --all or --cwd specified')
|
||||||
.option('--all', 'Stop all agents')
|
.option('--all', 'Stop all agents')
|
||||||
.option('--cwd <path>', 'Stop all agents in directory')
|
.option('--cwd <path>', 'Stop all agents in directory')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runStopCommand))
|
.action(withOutput(runStopCommand))
|
||||||
|
|
||||||
@@ -83,6 +80,7 @@ export function createAgentCommand(): Command {
|
|||||||
.argument('<id>', 'Agent ID (or prefix)')
|
.argument('<id>', 'Agent ID (or prefix)')
|
||||||
.argument('<prompt>', 'The message to send')
|
.argument('<prompt>', 'The message to send')
|
||||||
.option('--no-wait', 'Return immediately without waiting for completion')
|
.option('--no-wait', 'Return immediately without waiting for completion')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runSendCommand))
|
.action(withOutput(runSendCommand))
|
||||||
|
|
||||||
@@ -90,6 +88,7 @@ export function createAgentCommand(): Command {
|
|||||||
.command('inspect')
|
.command('inspect')
|
||||||
.description('Show detailed information about an agent')
|
.description('Show detailed information about an agent')
|
||||||
.argument('<id>', 'Agent ID (or prefix)')
|
.argument('<id>', 'Agent ID (or prefix)')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runInspectCommand))
|
.action(withOutput(runInspectCommand))
|
||||||
|
|
||||||
@@ -98,6 +97,7 @@ export function createAgentCommand(): Command {
|
|||||||
.description('Wait for an agent to become idle')
|
.description('Wait for an agent to become idle')
|
||||||
.argument('<id>', 'Agent ID (or prefix)')
|
.argument('<id>', 'Agent ID (or prefix)')
|
||||||
.option('--timeout <seconds>', 'Maximum wait time (default: 600)')
|
.option('--timeout <seconds>', 'Maximum wait time (default: 600)')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runWaitCommand))
|
.action(withOutput(runWaitCommand))
|
||||||
|
|
||||||
@@ -108,6 +108,7 @@ export function createAgentCommand(): Command {
|
|||||||
.argument('<id>', 'Agent ID (or prefix)')
|
.argument('<id>', 'Agent ID (or prefix)')
|
||||||
.argument('[mode]', 'Mode to set (required unless --list)')
|
.argument('[mode]', 'Mode to set (required unless --list)')
|
||||||
.option('--list', 'List available modes for this agent')
|
.option('--list', 'List available modes for this agent')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runModeCommand))
|
.action(withOutput(runModeCommand))
|
||||||
|
|
||||||
@@ -116,6 +117,7 @@ export function createAgentCommand(): Command {
|
|||||||
.description('Archive an agent (soft-delete)')
|
.description('Archive an agent (soft-delete)')
|
||||||
.argument('<id>', 'Agent ID (or prefix)')
|
.argument('<id>', 'Agent ID (or prefix)')
|
||||||
.option('--force', 'Force archive running agent')
|
.option('--force', 'Force archive running agent')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runArchiveCommand))
|
.action(withOutput(runArchiveCommand))
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ export interface AgentLogsOptions extends CommandOptions {
|
|||||||
export type AgentLogsResult = void
|
export type AgentLogsResult = void
|
||||||
|
|
||||||
|
|
||||||
|
function parseTailCount(raw: string | undefined): number | undefined {
|
||||||
|
if (raw === undefined) return undefined
|
||||||
|
const parsed = Number.parseInt(raw, 10)
|
||||||
|
if (Number.isNaN(parsed) || parsed < 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a timeline item matches the filter type
|
* Check if a timeline item matches the filter type
|
||||||
*/
|
*/
|
||||||
@@ -110,6 +119,12 @@ export async function runLogsCommand(
|
|||||||
|
|
||||||
// For follow mode, we stream events continuously
|
// For follow mode, we stream events continuously
|
||||||
if (options.follow) {
|
if (options.follow) {
|
||||||
|
if (options.tail !== undefined && parseTailCount(options.tail) === undefined) {
|
||||||
|
console.error(`Error: Invalid --tail value: ${options.tail}`)
|
||||||
|
console.error('Usage: --tail <n> (where n is >= 0)')
|
||||||
|
await client.close().catch(() => {})
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
await runFollowMode(client, resolvedId, options)
|
await runFollowMode(client, resolvedId, options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -159,18 +174,22 @@ export async function runLogsCommand(
|
|||||||
timelineItems = timelineItems.filter((item) => matchesFilter(item, options.filter))
|
timelineItems = timelineItems.filter((item) => matchesFilter(item, options.filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply tail limit
|
const tailCount = parseTailCount(options.tail)
|
||||||
if (options.tail) {
|
if (options.tail !== undefined && tailCount === undefined) {
|
||||||
const tailCount = parseInt(options.tail, 10)
|
console.error(`Error: Invalid --tail value: ${options.tail}`)
|
||||||
if (!isNaN(tailCount) && tailCount > 0) {
|
console.error('Usage: --tail <n> (where n is >= 0)')
|
||||||
timelineItems = timelineItems.slice(-tailCount)
|
await client.close().catch(() => {})
|
||||||
}
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
// Use curateAgentActivity to format the transcript
|
// Use curateAgentActivity to format the transcript
|
||||||
const transcript = curateAgentActivity(timelineItems)
|
if (tailCount === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const transcript = curateAgentActivity(timelineItems, tailCount !== undefined ? { maxItems: tailCount } : undefined)
|
||||||
console.log(transcript)
|
console.log(transcript)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
@@ -188,6 +207,9 @@ async function runFollowMode(
|
|||||||
agentId: string,
|
agentId: string,
|
||||||
options: AgentLogsOptions
|
options: AgentLogsOptions
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const DEFAULT_FOLLOW_TAIL = 10
|
||||||
|
const tailCount = parseTailCount(options.tail) ?? DEFAULT_FOLLOW_TAIL
|
||||||
|
|
||||||
// First, get existing timeline
|
// First, get existing timeline
|
||||||
const snapshotPromise = new Promise<AgentTimelineItem[]>((resolve) => {
|
const snapshotPromise = new Promise<AgentTimelineItem[]>((resolve) => {
|
||||||
const timeout = setTimeout(() => resolve([]), 10000)
|
const timeout = setTimeout(() => resolve([]), 10000)
|
||||||
@@ -218,22 +240,17 @@ async function runFollowMode(
|
|||||||
existingItems = existingItems.filter((item) => matchesFilter(item, options.filter))
|
existingItems = existingItems.filter((item) => matchesFilter(item, options.filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply tail to existing items
|
// Print existing transcript (tail-like behavior)
|
||||||
if (options.tail) {
|
if (tailCount > 0) {
|
||||||
const tailCount = parseInt(options.tail, 10)
|
const existingTranscript = curateAgentActivity(existingItems, { maxItems: tailCount })
|
||||||
if (!isNaN(tailCount) && tailCount > 0) {
|
if (existingTranscript !== 'No activity to display.') {
|
||||||
existingItems = existingItems.slice(-tailCount)
|
console.log(existingTranscript)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print existing transcript
|
|
||||||
const existingTranscript = curateAgentActivity(existingItems)
|
|
||||||
if (existingTranscript !== 'No activity to display.') {
|
|
||||||
console.log(existingTranscript)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to new events
|
// Subscribe to new events
|
||||||
console.log('\n--- Following logs (Ctrl+C to stop) ---\n')
|
const tailLabel = tailCount === 0 ? 'no history' : `last ${tailCount} entr${tailCount === 1 ? 'y' : 'ies'}`
|
||||||
|
console.log(`\n--- Following logs (${tailLabel}; Ctrl+C to stop) ---\n`)
|
||||||
|
|
||||||
const unsubscribe = client.on('agent_stream', (msg: unknown) => {
|
const unsubscribe = client.on('agent_stream', (msg: unknown) => {
|
||||||
const message = msg as AgentStreamMessage
|
const message = msg as AgentStreamMessage
|
||||||
|
|||||||
@@ -15,22 +15,19 @@ export function createDaemonCommand(): Command {
|
|||||||
.description('Show daemon status')
|
.description('Show daemon status')
|
||||||
.option('--json', 'Output in JSON format')
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action((options, command) => {
|
.action(withOutput(runStatusCommand))
|
||||||
if (options.json) {
|
|
||||||
command.parent.parent.opts().format = 'json'
|
|
||||||
}
|
|
||||||
return withOutput(runStatusCommand)(options, command)
|
|
||||||
})
|
|
||||||
|
|
||||||
daemon
|
daemon
|
||||||
.command('stop')
|
.command('stop')
|
||||||
.description('Stop the daemon')
|
.description('Stop the daemon')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runStopCommand))
|
.action(withOutput(runStopCommand))
|
||||||
|
|
||||||
daemon
|
daemon
|
||||||
.command('restart')
|
.command('restart')
|
||||||
.description('Restart the daemon')
|
.description('Restart the daemon')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runRestartCommand))
|
.action(withOutput(runRestartCommand))
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,7 @@ export function createPermitCommand(): Command {
|
|||||||
.description('List all pending permissions')
|
.description('List all pending permissions')
|
||||||
.option('--json', 'Output in JSON format')
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action((options, command) => {
|
.action(withOutput(runLsCommand))
|
||||||
if (options.json) {
|
|
||||||
command.parent.parent.opts().format = 'json'
|
|
||||||
}
|
|
||||||
return withOutput(runLsCommand)(options, command)
|
|
||||||
})
|
|
||||||
|
|
||||||
permit
|
permit
|
||||||
.command('allow')
|
.command('allow')
|
||||||
@@ -26,6 +21,7 @@ export function createPermitCommand(): Command {
|
|||||||
.argument('[req_id]', 'Permission request ID (optional if --all)')
|
.argument('[req_id]', 'Permission request ID (optional if --all)')
|
||||||
.option('--all', 'Allow all pending permissions for this agent')
|
.option('--all', 'Allow all pending permissions for this agent')
|
||||||
.option('--input <json>', 'Modified input parameters (JSON)')
|
.option('--input <json>', 'Modified input parameters (JSON)')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runAllowCommand))
|
.action(withOutput(runAllowCommand))
|
||||||
|
|
||||||
@@ -37,6 +33,7 @@ export function createPermitCommand(): Command {
|
|||||||
.option('--all', 'Deny all pending permissions for this agent')
|
.option('--all', 'Deny all pending permissions for this agent')
|
||||||
.option('--message <msg>', 'Denial reason message')
|
.option('--message <msg>', 'Denial reason message')
|
||||||
.option('--interrupt', 'Stop agent after denial')
|
.option('--interrupt', 'Stop agent after denial')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runDenyCommand))
|
.action(withOutput(runDenyCommand))
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,7 @@ export function createProviderCommand(): Command {
|
|||||||
.description('List available providers and status')
|
.description('List available providers and status')
|
||||||
.option('--json', 'Output in JSON format')
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action((options, command) => {
|
.action(withOutput(runLsCommand))
|
||||||
if (options.json) {
|
|
||||||
command.parent.parent.opts().format = 'json'
|
|
||||||
}
|
|
||||||
return withOutput(runLsCommand)(options, command)
|
|
||||||
})
|
|
||||||
|
|
||||||
provider
|
provider
|
||||||
.command('models')
|
.command('models')
|
||||||
@@ -24,12 +19,7 @@ export function createProviderCommand(): Command {
|
|||||||
.argument('<provider>', 'Provider name (claude, codex, opencode)')
|
.argument('<provider>', 'Provider name (claude, codex, opencode)')
|
||||||
.option('--json', 'Output in JSON format')
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action((provider, options, command) => {
|
.action(withOutput(runModelsCommand))
|
||||||
if (options.json) {
|
|
||||||
command.parent.parent.opts().format = 'json'
|
|
||||||
}
|
|
||||||
return withOutput(runModelsCommand)(provider, options, command)
|
|
||||||
})
|
|
||||||
|
|
||||||
return provider
|
return provider
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,17 +11,13 @@ export function createWorktreeCommand(): Command {
|
|||||||
.description('List Paseo-managed git worktrees')
|
.description('List Paseo-managed git worktrees')
|
||||||
.option('--json', 'Output in JSON format')
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action((options, command) => {
|
.action(withOutput(runLsCommand))
|
||||||
if (options.json) {
|
|
||||||
command.parent.parent.opts().format = 'json'
|
|
||||||
}
|
|
||||||
return withOutput(runLsCommand)(options, command)
|
|
||||||
})
|
|
||||||
|
|
||||||
worktree
|
worktree
|
||||||
.command('archive')
|
.command('archive')
|
||||||
.description('Archive a worktree (removes worktree and associated branch)')
|
.description('Archive a worktree (removes worktree and associated branch)')
|
||||||
.argument('<name>', 'Worktree name or branch name')
|
.argument('<name>', 'Worktree name or branch name')
|
||||||
|
.option('--json', 'Output in JSON format')
|
||||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||||
.action(withOutput(runArchiveCommand))
|
.action(withOutput(runArchiveCommand))
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Command } from 'commander'
|
import type { Command } from 'commander'
|
||||||
import type { AnyCommandResult, OutputOptions } from './types.js'
|
import type { AnyCommandResult, CommandError, OutputOptions } from './types.js'
|
||||||
import { render, renderError, toCommandError, defaultOutputOptions } from './render.js'
|
import { render, renderError, toCommandError, defaultOutputOptions } from './render.js'
|
||||||
|
|
||||||
/** Options that include output settings from global options */
|
/** Options that include output settings from global options */
|
||||||
@@ -13,10 +13,26 @@ export interface CommandOptions extends Partial<OutputOptions> {
|
|||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeFormat(raw: unknown): OutputOptions['format'] {
|
||||||
|
const value = typeof raw === 'string' ? raw.trim().toLowerCase() : ''
|
||||||
|
|
||||||
|
// Common user expectation: "cli" means "table/human"
|
||||||
|
if (value === 'cli') return 'table'
|
||||||
|
|
||||||
|
if (value === 'table' || value === 'json' || value === 'yaml') return value
|
||||||
|
|
||||||
|
const error: CommandError = {
|
||||||
|
code: 'INVALID_FORMAT',
|
||||||
|
message: `Unsupported output format: ${String(raw)}`,
|
||||||
|
details: 'Supported formats: table, json, yaml',
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
/** Extract output options from command options */
|
/** Extract output options from command options */
|
||||||
function extractOutputOptions(options: CommandOptions): OutputOptions {
|
function extractOutputOptions(options: CommandOptions): OutputOptions {
|
||||||
return {
|
return {
|
||||||
format: (options.format as OutputOptions['format']) ?? defaultOutputOptions.format,
|
format: options.json ? 'json' : normalizeFormat(options.format ?? defaultOutputOptions.format),
|
||||||
quiet: options.quiet ?? defaultOutputOptions.quiet,
|
quiet: options.quiet ?? defaultOutputOptions.quiet,
|
||||||
noHeaders: options.headers === false, // Commander uses --no-headers -> headers: false
|
noHeaders: options.headers === false, // Commander uses --no-headers -> headers: false
|
||||||
noColor: options.color === false, // Commander uses --no-color -> color: false
|
noColor: options.color === false, // Commander uses --no-color -> color: false
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* Tests:
|
* Tests:
|
||||||
* - daemon --help shows subcommands
|
* - daemon --help shows subcommands
|
||||||
* - daemon status fails gracefully when daemon not running
|
* - daemon status fails gracefully when daemon not running
|
||||||
* - daemon status --format json outputs valid JSON (even for errors)
|
* - daemon status --json outputs valid JSON (even for errors)
|
||||||
* - daemon stop handles daemon not running gracefully
|
* - daemon stop handles daemon not running gracefully
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -56,11 +56,11 @@ try {
|
|||||||
console.log('✓ daemon status fails gracefully when not running\n')
|
console.log('✓ daemon status fails gracefully when not running\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 3: daemon status --format json outputs valid JSON (even for errors)
|
// Test 3: daemon status --json outputs valid JSON (even for errors)
|
||||||
{
|
{
|
||||||
console.log('Test 3: daemon status --format json outputs JSON')
|
console.log('Test 3: daemon status --json outputs JSON')
|
||||||
const result =
|
const result =
|
||||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo daemon status --format json`.nothrow()
|
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo daemon status --json`.nothrow()
|
||||||
// Should still fail (daemon not running)
|
// Should still fail (daemon not running)
|
||||||
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
|
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
|
||||||
// But output should be valid JSON
|
// But output should be valid JSON
|
||||||
@@ -68,14 +68,14 @@ try {
|
|||||||
if (output.length > 0) {
|
if (output.length > 0) {
|
||||||
try {
|
try {
|
||||||
JSON.parse(output)
|
JSON.parse(output)
|
||||||
console.log('✓ daemon status --format json outputs valid JSON\n')
|
console.log('✓ daemon status --json outputs valid JSON\n')
|
||||||
} catch {
|
} catch {
|
||||||
// If stdout is empty, check if stderr has the error (acceptable for now)
|
// If stdout is empty, check if stderr has the error (acceptable for now)
|
||||||
console.log('✓ daemon status --format json handled error (output may be in stderr)\n')
|
console.log('✓ daemon status --json handled error (output may be in stderr)\n')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Empty stdout is acceptable if error is in stderr
|
// Empty stdout is acceptable if error is in stderr
|
||||||
console.log('✓ daemon status --format json handled error gracefully\n')
|
console.log('✓ daemon status --json handled error gracefully\n')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* - paseo --help shows ls command
|
* - paseo --help shows ls command
|
||||||
* - paseo ls --help shows options
|
* - paseo ls --help shows options
|
||||||
* - paseo ls returns empty list or error when no daemon
|
* - paseo ls returns empty list or error when no daemon
|
||||||
* - paseo ls --format json returns valid JSON (or error)
|
* - paseo ls --json returns valid JSON (or error)
|
||||||
* - paseo ls -a flag is accepted
|
* - paseo ls -a flag is accepted
|
||||||
* - paseo ls -g flag is accepted
|
* - paseo ls -g flag is accepted
|
||||||
*/
|
*/
|
||||||
@@ -71,11 +71,11 @@ try {
|
|||||||
console.log('✓ paseo ls handles daemon not running\n')
|
console.log('✓ paseo ls handles daemon not running\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 4: paseo ls --format json returns valid JSON error
|
// Test 4: paseo ls --json returns valid JSON error
|
||||||
{
|
{
|
||||||
console.log('Test 4: paseo ls --format json handles errors')
|
console.log('Test 4: paseo ls --json handles errors')
|
||||||
const result =
|
const result =
|
||||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo ls --format json`.nothrow()
|
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo ls --json`.nothrow()
|
||||||
// Should still fail (daemon not running)
|
// Should still fail (daemon not running)
|
||||||
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
|
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
|
||||||
// But output should be valid JSON if present
|
// But output should be valid JSON if present
|
||||||
@@ -83,13 +83,13 @@ try {
|
|||||||
if (output.length > 0) {
|
if (output.length > 0) {
|
||||||
try {
|
try {
|
||||||
JSON.parse(output)
|
JSON.parse(output)
|
||||||
console.log('✓ paseo ls --format json outputs valid JSON error\n')
|
console.log('✓ paseo ls --json outputs valid JSON error\n')
|
||||||
} catch {
|
} catch {
|
||||||
// Empty or stderr-only output is acceptable
|
// Empty or stderr-only output is acceptable
|
||||||
console.log('✓ paseo ls --format json handled error (output may be in stderr)\n')
|
console.log('✓ paseo ls --json handled error (output may be in stderr)\n')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('✓ paseo ls --format json handled error gracefully\n')
|
console.log('✓ paseo ls --json handled error gracefully\n')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,15 +98,15 @@ try {
|
|||||||
console.log('-q (quiet) flag is accepted with inspect\n')
|
console.log('-q (quiet) flag is accepted with inspect\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 6: --format json flag is accepted with inspect
|
// Test 6: --json flag is accepted with inspect
|
||||||
{
|
{
|
||||||
console.log('Test 6: --format json flag is accepted with inspect')
|
console.log('Test 6: --json flag is accepted with inspect')
|
||||||
const result =
|
const result =
|
||||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo --format json inspect abc123`.nothrow()
|
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo inspect abc123 --json`.nothrow()
|
||||||
const output = result.stdout + result.stderr
|
const output = result.stdout + result.stderr
|
||||||
assert(!output.includes('unknown option'), 'should accept --format json flag')
|
assert(!output.includes('unknown option'), 'should accept --json flag')
|
||||||
assert(!output.includes('error: option'), 'should not have option parsing error')
|
assert(!output.includes('error: option'), 'should not have option parsing error')
|
||||||
console.log('--format json flag is accepted with inspect\n')
|
console.log('--json flag is accepted with inspect\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 7: --format yaml flag is accepted with inspect
|
// Test 7: --format yaml flag is accepted with inspect
|
||||||
|
|||||||
@@ -112,15 +112,15 @@ try {
|
|||||||
console.log('-q (quiet) flag is accepted with wait\n')
|
console.log('-q (quiet) flag is accepted with wait\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 7: --format json flag is accepted with wait
|
// Test 7: --json flag is accepted with wait
|
||||||
{
|
{
|
||||||
console.log('Test 7: --format json flag is accepted with wait')
|
console.log('Test 7: --json flag is accepted with wait')
|
||||||
const result =
|
const result =
|
||||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo --format json wait abc123`.nothrow()
|
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo wait abc123 --json`.nothrow()
|
||||||
const output = result.stdout + result.stderr
|
const output = result.stdout + result.stderr
|
||||||
assert(!output.includes('unknown option'), 'should accept --format json flag')
|
assert(!output.includes('unknown option'), 'should accept --json flag')
|
||||||
assert(!output.includes('error: option'), 'should not have option parsing error')
|
assert(!output.includes('error: option'), 'should not have option parsing error')
|
||||||
console.log('--format json flag is accepted with wait\n')
|
console.log('--json flag is accepted with wait\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 8: --format yaml flag is accepted with wait
|
// Test 8: --format yaml flag is accepted with wait
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* - permit --help shows subcommands
|
* - permit --help shows subcommands
|
||||||
* - permit ls --help shows options
|
* - permit ls --help shows options
|
||||||
* - permit ls returns error when no daemon
|
* - permit ls returns error when no daemon
|
||||||
* - permit ls --format json handles errors
|
* - permit ls --json handles errors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import assert from 'node:assert'
|
import assert from 'node:assert'
|
||||||
@@ -67,11 +67,11 @@ try {
|
|||||||
console.log('✓ permit ls handles daemon not running\n')
|
console.log('✓ permit ls handles daemon not running\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 4: permit ls --format json handles errors
|
// Test 4: permit ls --json handles errors
|
||||||
{
|
{
|
||||||
console.log('Test 4: permit ls --format json handles errors')
|
console.log('Test 4: permit ls --json handles errors')
|
||||||
const result =
|
const result =
|
||||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit ls --format json`.nothrow()
|
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo permit ls --json`.nothrow()
|
||||||
// Should still fail (daemon not running)
|
// Should still fail (daemon not running)
|
||||||
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
|
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
|
||||||
// But output should be valid JSON if present
|
// But output should be valid JSON if present
|
||||||
@@ -79,13 +79,13 @@ try {
|
|||||||
if (output.length > 0) {
|
if (output.length > 0) {
|
||||||
try {
|
try {
|
||||||
JSON.parse(output)
|
JSON.parse(output)
|
||||||
console.log('✓ permit ls --format json outputs valid JSON error\n')
|
console.log('✓ permit ls --json outputs valid JSON error\n')
|
||||||
} catch {
|
} catch {
|
||||||
// Empty or stderr-only output is acceptable
|
// Empty or stderr-only output is acceptable
|
||||||
console.log('✓ permit ls --format json handled error (output may be in stderr)\n')
|
console.log('✓ permit ls --json handled error (output may be in stderr)\n')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('✓ permit ls --format json handled error gracefully\n')
|
console.log('✓ permit ls --json handled error gracefully\n')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,15 +142,15 @@ try {
|
|||||||
console.log('✓ -q (quiet) flag is accepted with worktree ls\n')
|
console.log('✓ -q (quiet) flag is accepted with worktree ls\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 10: -f json flag is accepted with worktree ls
|
// Test 10: --json flag is accepted with worktree ls
|
||||||
{
|
{
|
||||||
console.log('Test 10: -f json flag is accepted with worktree ls')
|
console.log('Test 10: --json flag is accepted with worktree ls')
|
||||||
const result =
|
const result =
|
||||||
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo -f json worktree ls`.nothrow()
|
await $`PASEO_HOST=localhost:${port} PASEO_HOME=${paseoHome} npx paseo worktree ls --json`.nothrow()
|
||||||
const output = result.stdout + result.stderr
|
const output = result.stdout + result.stderr
|
||||||
assert(!output.includes('unknown option'), 'should accept -f json flag')
|
assert(!output.includes('unknown option'), 'should accept --json flag')
|
||||||
assert(!output.includes('error: option'), 'should not have option parsing error')
|
assert(!output.includes('error: option'), 'should not have option parsing error')
|
||||||
console.log('✓ -f json flag is accepted with worktree ls\n')
|
console.log('✓ --json flag is accepted with worktree ls\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 11: paseo --help shows worktree subcommand
|
// Test 11: paseo --help shows worktree subcommand
|
||||||
|
|||||||
@@ -9,13 +9,13 @@
|
|||||||
* Tests:
|
* Tests:
|
||||||
* - provider --help shows subcommands
|
* - provider --help shows subcommands
|
||||||
* - provider ls lists all providers
|
* - provider ls lists all providers
|
||||||
* - provider ls --format json outputs valid JSON
|
* - provider ls --json outputs valid JSON
|
||||||
* - provider ls --quiet outputs provider names only
|
* - provider ls --quiet outputs provider names only
|
||||||
* - provider models claude lists claude models
|
* - provider models claude lists claude models
|
||||||
* - provider models codex lists codex models
|
* - provider models codex lists codex models
|
||||||
* - provider models opencode lists opencode models
|
* - provider models opencode lists opencode models
|
||||||
* - provider models unknown fails with error
|
* - provider models unknown fails with error
|
||||||
* - provider models --format json outputs valid JSON
|
* - provider models --json outputs valid JSON
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import assert from 'node:assert'
|
import assert from 'node:assert'
|
||||||
@@ -47,10 +47,10 @@ console.log('=== Provider Commands ===\n')
|
|||||||
console.log('✓ provider ls lists all providers\n')
|
console.log('✓ provider ls lists all providers\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 3: provider ls --format json outputs valid JSON
|
// Test 3: provider ls --json outputs valid JSON
|
||||||
{
|
{
|
||||||
console.log('Test 3: provider ls --format json outputs valid JSON')
|
console.log('Test 3: provider ls --json outputs valid JSON')
|
||||||
const result = await $`npx paseo provider ls --format json`.nothrow()
|
const result = await $`npx paseo provider ls --json`.nothrow()
|
||||||
assert.strictEqual(result.exitCode, 0, 'should exit 0')
|
assert.strictEqual(result.exitCode, 0, 'should exit 0')
|
||||||
const data = JSON.parse(result.stdout.trim())
|
const data = JSON.parse(result.stdout.trim())
|
||||||
assert(Array.isArray(data), 'output should be an array')
|
assert(Array.isArray(data), 'output should be an array')
|
||||||
@@ -58,7 +58,7 @@ console.log('=== Provider Commands ===\n')
|
|||||||
assert(data.some((p: { provider: string }) => p.provider === 'claude'), 'should include claude')
|
assert(data.some((p: { provider: string }) => p.provider === 'claude'), 'should include claude')
|
||||||
assert(data.some((p: { provider: string }) => p.provider === 'codex'), 'should include codex')
|
assert(data.some((p: { provider: string }) => p.provider === 'codex'), 'should include codex')
|
||||||
assert(data.some((p: { provider: string }) => p.provider === 'opencode'), 'should include opencode')
|
assert(data.some((p: { provider: string }) => p.provider === 'opencode'), 'should include opencode')
|
||||||
console.log('✓ provider ls --format json outputs valid JSON\n')
|
console.log('✓ provider ls --json outputs valid JSON\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 4: provider ls --quiet outputs provider names only
|
// Test 4: provider ls --quiet outputs provider names only
|
||||||
@@ -119,16 +119,16 @@ console.log('=== Provider Commands ===\n')
|
|||||||
console.log('✓ provider models unknown fails with error\n')
|
console.log('✓ provider models unknown fails with error\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 9: provider models --format json outputs valid JSON
|
// Test 9: provider models --json outputs valid JSON
|
||||||
{
|
{
|
||||||
console.log('Test 9: provider models --format json outputs valid JSON')
|
console.log('Test 9: provider models --json outputs valid JSON')
|
||||||
const result = await $`npx paseo provider models claude --format json`.nothrow()
|
const result = await $`npx paseo provider models claude --json`.nothrow()
|
||||||
assert.strictEqual(result.exitCode, 0, 'should exit 0')
|
assert.strictEqual(result.exitCode, 0, 'should exit 0')
|
||||||
const data = JSON.parse(result.stdout.trim())
|
const data = JSON.parse(result.stdout.trim())
|
||||||
assert(Array.isArray(data), 'output should be an array')
|
assert(Array.isArray(data), 'output should be an array')
|
||||||
assert.strictEqual(data.length, 3, 'should have 3 models for claude')
|
assert.strictEqual(data.length, 3, 'should have 3 models for claude')
|
||||||
assert(data.every((m: { model: string; id: string }) => m.model && m.id), 'each model should have name and id')
|
assert(data.every((m: { model: string; id: string }) => m.model && m.id), 'each model should have name and id')
|
||||||
console.log('✓ provider models --format json outputs valid JSON\n')
|
console.log('✓ provider models --json outputs valid JSON\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test 10: provider models --quiet outputs model IDs only
|
// Test 10: provider models --quiet outputs model IDs only
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ async function cleanup(): Promise<void> {
|
|||||||
async function test_agent_ls_empty(): Promise<void> {
|
async function test_agent_ls_empty(): Promise<void> {
|
||||||
console.log('\n--- Test: agent ls with empty list ---')
|
console.log('\n--- Test: agent ls with empty list ---')
|
||||||
|
|
||||||
const result = await ctx.paseo(['ls', '--format', 'json'])
|
const result = await ctx.paseo(['ls', '--json'])
|
||||||
|
|
||||||
console.log('Exit code:', result.exitCode)
|
console.log('Exit code:', result.exitCode)
|
||||||
console.log('Stdout:', result.stdout)
|
console.log('Stdout:', result.stdout)
|
||||||
@@ -116,7 +116,7 @@ async function test_agent_run_detached(): Promise<string> {
|
|||||||
async function test_agent_ls_shows_agent(agentId: string): Promise<void> {
|
async function test_agent_ls_shows_agent(agentId: string): Promise<void> {
|
||||||
console.log('\n--- Test: agent ls shows created agent ---')
|
console.log('\n--- Test: agent ls shows created agent ---')
|
||||||
|
|
||||||
const result = await ctx.paseo(['ls', '--format', 'json'])
|
const result = await ctx.paseo(['ls', '--json'])
|
||||||
|
|
||||||
console.log('Exit code:', result.exitCode)
|
console.log('Exit code:', result.exitCode)
|
||||||
console.log('Stdout:', result.stdout)
|
console.log('Stdout:', result.stdout)
|
||||||
@@ -198,7 +198,7 @@ async function test_agent_stop(agentId: string): Promise<void> {
|
|||||||
assert.strictEqual(result.exitCode, 0, 'agent stop should succeed')
|
assert.strictEqual(result.exitCode, 0, 'agent stop should succeed')
|
||||||
|
|
||||||
// Verify agent is no longer in running list
|
// Verify agent is no longer in running list
|
||||||
const psResult = await ctx.paseo(['ls', '--format', 'json'])
|
const psResult = await ctx.paseo(['ls', '--json'])
|
||||||
const agents = JSON.parse(psResult.stdout.trim()) as Array<{ id: string; status?: string }>
|
const agents = JSON.parse(psResult.stdout.trim()) as Array<{ id: string; status?: string }>
|
||||||
|
|
||||||
// Agent might still be in list but should not be running
|
// Agent might still be in list but should not be running
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ async function test_wait_for_permission_request(agentId: string): Promise<void>
|
|||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
while (Date.now() - startTime < maxWait) {
|
while (Date.now() - startTime < maxWait) {
|
||||||
const result = await ctx.paseo(['permit', 'ls', '--format', 'json'])
|
const result = await ctx.paseo(['permit', 'ls', '--json'])
|
||||||
|
|
||||||
if (result.exitCode === 0) {
|
if (result.exitCode === 0) {
|
||||||
try {
|
try {
|
||||||
@@ -148,7 +148,7 @@ async function test_wait_for_permission_request(agentId: string): Promise<void>
|
|||||||
async function test_permit_ls(): Promise<{ agentShortId: string; requestId: string }> {
|
async function test_permit_ls(): Promise<{ agentShortId: string; requestId: string }> {
|
||||||
console.log('\n--- Test: List pending permissions with permit ls ---')
|
console.log('\n--- Test: List pending permissions with permit ls ---')
|
||||||
|
|
||||||
const result = await ctx.paseo(['permit', 'ls', '--format', 'json'])
|
const result = await ctx.paseo(['permit', 'ls', '--json'])
|
||||||
|
|
||||||
console.log('Exit code:', result.exitCode)
|
console.log('Exit code:', result.exitCode)
|
||||||
console.log('Stdout:', result.stdout)
|
console.log('Stdout:', result.stdout)
|
||||||
|
|||||||
@@ -4,6 +4,44 @@ export type AgentProvider = string;
|
|||||||
|
|
||||||
export type AgentMetadata = { [key: string]: unknown };
|
export type AgentMetadata = { [key: string]: unknown };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stdio-based MCP server (spawns a subprocess).
|
||||||
|
*/
|
||||||
|
export interface McpStdioServerConfig {
|
||||||
|
type: "stdio";
|
||||||
|
command: string;
|
||||||
|
args?: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP-based MCP server.
|
||||||
|
*/
|
||||||
|
export interface McpHttpServerConfig {
|
||||||
|
type: "http";
|
||||||
|
url: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE-based MCP server (Server-Sent Events over HTTP).
|
||||||
|
*/
|
||||||
|
export interface McpSseServerConfig {
|
||||||
|
type: "sse";
|
||||||
|
url: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonical MCP server configuration.
|
||||||
|
* Discriminated union by `type` field.
|
||||||
|
* Each provider normalizes this to their expected format.
|
||||||
|
*/
|
||||||
|
export type McpServerConfig =
|
||||||
|
| McpStdioServerConfig
|
||||||
|
| McpHttpServerConfig
|
||||||
|
| McpSseServerConfig;
|
||||||
|
|
||||||
export type AgentMode = {
|
export type AgentMode = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -208,7 +246,7 @@ export type AgentSessionConfig = {
|
|||||||
codex?: AgentMetadata;
|
codex?: AgentMetadata;
|
||||||
claude?: Partial<ClaudeAgentOptions>;
|
claude?: Partial<ClaudeAgentOptions>;
|
||||||
};
|
};
|
||||||
mcpServers?: AgentMetadata;
|
mcpServers?: Record<string, McpServerConfig>;
|
||||||
/**
|
/**
|
||||||
* Internal agents are hidden from listings and don't trigger notifications.
|
* Internal agents are hidden from listings and don't trigger notifications.
|
||||||
* They are used for ephemeral system tasks like commit/PR generation.
|
* They are used for ephemeral system tasks like commit/PR generation.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
query,
|
query,
|
||||||
type AgentDefinition,
|
type AgentDefinition,
|
||||||
type CanUseTool,
|
type CanUseTool,
|
||||||
type McpServerConfig,
|
type McpServerConfig as ClaudeSdkMcpServerConfig,
|
||||||
type ModelInfo,
|
type ModelInfo,
|
||||||
type Options,
|
type Options,
|
||||||
type PermissionMode,
|
type PermissionMode,
|
||||||
@@ -45,6 +45,7 @@ import type {
|
|||||||
AgentRuntimeInfo,
|
AgentRuntimeInfo,
|
||||||
ListModelsOptions,
|
ListModelsOptions,
|
||||||
ListPersistedAgentsOptions,
|
ListPersistedAgentsOptions,
|
||||||
|
McpServerConfig,
|
||||||
PersistedAgentDescriptor,
|
PersistedAgentDescriptor,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
||||||
@@ -102,7 +103,6 @@ type ClaudeAgentConfig = AgentSessionConfig & { provider: "claude" };
|
|||||||
|
|
||||||
export type ClaudeContentChunk = { type: string; [key: string]: any };
|
export type ClaudeContentChunk = { type: string; [key: string]: any };
|
||||||
|
|
||||||
type ClaudeMcpServerConfig = McpServerConfig;
|
|
||||||
type ClaudeOptions = Options;
|
type ClaudeOptions = Options;
|
||||||
|
|
||||||
type ClaudeAgentClientOptions = {
|
type ClaudeAgentClientOptions = {
|
||||||
@@ -178,6 +178,32 @@ function isMetadata(value: unknown): value is AgentMetadata {
|
|||||||
return typeof value === "object" && value !== null;
|
return typeof value === "object" && value !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMcpServerConfig(value: unknown): value is McpServerConfig {
|
||||||
|
if (!isMetadata(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const type = value.type;
|
||||||
|
if (type === "stdio") {
|
||||||
|
return typeof value.command === "string";
|
||||||
|
}
|
||||||
|
if (type === "http" || type === "sse") {
|
||||||
|
return typeof value.url === "string";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMcpServersRecord(value: unknown): value is Record<string, McpServerConfig> {
|
||||||
|
if (!isMetadata(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const config of Object.values(value)) {
|
||||||
|
if (!isMcpServerConfig(config)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function isPermissionMode(value: string | undefined): value is PermissionMode {
|
function isPermissionMode(value: string | undefined): value is PermissionMode {
|
||||||
return typeof value === "string" && VALID_CLAUDE_MODES.has(value);
|
return typeof value === "string" && VALID_CLAUDE_MODES.has(value);
|
||||||
}
|
}
|
||||||
@@ -230,18 +256,37 @@ function coerceSessionMetadata(metadata: AgentMetadata | undefined): Partial<Age
|
|||||||
result.extra = extra;
|
result.extra = extra;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isMetadata(metadata.mcpServers)) {
|
if (isMcpServersRecord(metadata.mcpServers)) {
|
||||||
result.mcpServers = metadata.mcpServers;
|
result.mcpServers = metadata.mcpServers;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isClaudeMcpServerConfig(value: unknown): value is ClaudeMcpServerConfig {
|
function toClaudeSdkMcpConfig(
|
||||||
if (!isMetadata(value)) {
|
config: McpServerConfig
|
||||||
return false;
|
): ClaudeSdkMcpServerConfig {
|
||||||
|
switch (config.type) {
|
||||||
|
case "stdio":
|
||||||
|
return {
|
||||||
|
type: "stdio",
|
||||||
|
command: config.command,
|
||||||
|
args: config.args,
|
||||||
|
env: config.env,
|
||||||
|
};
|
||||||
|
case "http":
|
||||||
|
return {
|
||||||
|
type: "http",
|
||||||
|
url: config.url,
|
||||||
|
headers: config.headers,
|
||||||
|
};
|
||||||
|
case "sse":
|
||||||
|
return {
|
||||||
|
type: "sse",
|
||||||
|
url: config.url,
|
||||||
|
headers: config.headers,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return typeof value.type === "string" || typeof value.command === "string" || typeof value.url === "string";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isClaudeContentChunk(value: unknown): value is ClaudeContentChunk {
|
function isClaudeContentChunk(value: unknown): value is ClaudeContentChunk {
|
||||||
@@ -783,10 +828,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (this.config.mcpServers) {
|
if (this.config.mcpServers) {
|
||||||
const normalizedUserServers = this.normalizeMcpServers(this.config.mcpServers);
|
base.mcpServers = this.normalizeMcpServers(this.config.mcpServers);
|
||||||
if (normalizedUserServers) {
|
|
||||||
base.mcpServers = normalizedUserServers;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.model) {
|
if (this.config.model) {
|
||||||
@@ -800,18 +842,13 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private normalizeMcpServers(
|
private normalizeMcpServers(
|
||||||
servers: ClaudeAgentConfig["mcpServers"]
|
servers: Record<string, McpServerConfig>
|
||||||
): Record<string, ClaudeMcpServerConfig> | undefined {
|
): Record<string, ClaudeSdkMcpServerConfig> {
|
||||||
if (!servers) {
|
const result: Record<string, ClaudeSdkMcpServerConfig> = {};
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const result: Record<string, ClaudeMcpServerConfig> = {};
|
|
||||||
for (const [name, config] of Object.entries(servers)) {
|
for (const [name, config] of Object.entries(servers)) {
|
||||||
if (isClaudeMcpServerConfig(config)) {
|
result[name] = toClaudeSdkMcpConfig(config);
|
||||||
result[name] = config;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Object.keys(result).length ? result : undefined;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private toSdkUserMessage(prompt: AgentPromptInput): SDKUserMessage {
|
private toSdkUserMessage(prompt: AgentPromptInput): SDKUserMessage {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import type {
|
|||||||
AgentRuntimeInfo,
|
AgentRuntimeInfo,
|
||||||
ListModelsOptions,
|
ListModelsOptions,
|
||||||
ListPersistedAgentsOptions,
|
ListPersistedAgentsOptions,
|
||||||
|
McpServerConfig,
|
||||||
PersistedAgentDescriptor,
|
PersistedAgentDescriptor,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
import { curateAgentActivity } from "../activity-curator.js";
|
import { curateAgentActivity } from "../activity-curator.js";
|
||||||
@@ -1866,6 +1867,31 @@ const AgentSessionExtraSchema = z.object({
|
|||||||
claude: z.custom<AgentSessionExtra["claude"]>().optional(),
|
claude: z.custom<AgentSessionExtra["claude"]>().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const McpStdioServerConfigSchema = z.object({
|
||||||
|
type: z.literal("stdio"),
|
||||||
|
command: z.string(),
|
||||||
|
args: z.array(z.string()).optional(),
|
||||||
|
env: z.record(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const McpHttpServerConfigSchema = z.object({
|
||||||
|
type: z.literal("http"),
|
||||||
|
url: z.string(),
|
||||||
|
headers: z.record(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const McpSseServerConfigSchema = z.object({
|
||||||
|
type: z.literal("sse"),
|
||||||
|
url: z.string(),
|
||||||
|
headers: z.record(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const McpServerConfigSchema = z.discriminatedUnion("type", [
|
||||||
|
McpStdioServerConfigSchema,
|
||||||
|
McpHttpServerConfigSchema,
|
||||||
|
McpSseServerConfigSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
const AgentSessionConfigSchema = z
|
const AgentSessionConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
provider: z.string(),
|
provider: z.string(),
|
||||||
@@ -1879,7 +1905,7 @@ const AgentSessionConfigSchema = z
|
|||||||
webSearch: z.boolean().optional(),
|
webSearch: z.boolean().optional(),
|
||||||
reasoningEffort: z.string().optional(),
|
reasoningEffort: z.string().optional(),
|
||||||
extra: AgentSessionExtraSchema.optional(),
|
extra: AgentSessionExtraSchema.optional(),
|
||||||
mcpServers: z.record(z.unknown()).optional(),
|
mcpServers: z.record(McpServerConfigSchema).optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
@@ -2693,14 +2719,35 @@ function getCodexMcpCommand(): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodexMcpServerConfig = {
|
interface CodexMcpServerConfig {
|
||||||
url?: string;
|
url?: string;
|
||||||
http_headers?: Record<string, string>; // Static HTTP headers for HTTP servers
|
http_headers?: Record<string, string>;
|
||||||
command?: string;
|
command?: string;
|
||||||
args?: string[];
|
args?: string[];
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
tool_timeout_sec?: number; // Override the default 60s per-tool timeout
|
tool_timeout_sec?: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function toCodexMcpConfig(config: McpServerConfig): CodexMcpServerConfig {
|
||||||
|
switch (config.type) {
|
||||||
|
case "stdio":
|
||||||
|
return {
|
||||||
|
command: config.command,
|
||||||
|
args: config.args,
|
||||||
|
env: config.env,
|
||||||
|
};
|
||||||
|
case "http":
|
||||||
|
return {
|
||||||
|
url: config.url,
|
||||||
|
http_headers: config.headers,
|
||||||
|
};
|
||||||
|
case "sse":
|
||||||
|
return {
|
||||||
|
url: config.url,
|
||||||
|
http_headers: config.headers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type CodexConfigPayload = {
|
type CodexConfigPayload = {
|
||||||
mcp_servers?: Record<string, CodexMcpServerConfig>;
|
mcp_servers?: Record<string, CodexMcpServerConfig>;
|
||||||
@@ -2754,22 +2801,10 @@ function buildCodexMcpConfig(
|
|||||||
// Build MCP servers configuration
|
// Build MCP servers configuration
|
||||||
const mcpServers: Record<string, CodexMcpServerConfig> = {};
|
const mcpServers: Record<string, CodexMcpServerConfig> = {};
|
||||||
|
|
||||||
// Merge MCP servers from extra.codex.mcp_servers (legacy location)
|
// Merge user-provided MCP servers
|
||||||
const extraCodex = config.extra?.codex as Record<string, unknown> | undefined;
|
|
||||||
if (extraCodex?.mcp_servers && typeof extraCodex.mcp_servers === "object") {
|
|
||||||
for (const [name, serverConfig] of Object.entries(extraCodex.mcp_servers as Record<string, unknown>)) {
|
|
||||||
if (typeof serverConfig === "object" && serverConfig !== null) {
|
|
||||||
mcpServers[name] = serverConfig as CodexMcpServerConfig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge user-provided MCP servers (they take highest precedence)
|
|
||||||
if (config.mcpServers) {
|
if (config.mcpServers) {
|
||||||
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
||||||
if (typeof serverConfig === "object" && serverConfig !== null) {
|
mcpServers[name] = toCodexMcpConfig(serverConfig);
|
||||||
mcpServers[name] = serverConfig as CodexMcpServerConfig;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,31 @@ const AgentUsageSchema: z.ZodType<AgentUsage> = z.object({
|
|||||||
totalCostUsd: z.number().optional(),
|
totalCostUsd: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const McpStdioServerConfigSchema = z.object({
|
||||||
|
type: z.literal("stdio"),
|
||||||
|
command: z.string(),
|
||||||
|
args: z.array(z.string()).optional(),
|
||||||
|
env: z.record(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const McpHttpServerConfigSchema = z.object({
|
||||||
|
type: z.literal("http"),
|
||||||
|
url: z.string(),
|
||||||
|
headers: z.record(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const McpSseServerConfigSchema = z.object({
|
||||||
|
type: z.literal("sse"),
|
||||||
|
url: z.string(),
|
||||||
|
headers: z.record(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const McpServerConfigSchema = z.discriminatedUnion("type", [
|
||||||
|
McpStdioServerConfigSchema,
|
||||||
|
McpHttpServerConfigSchema,
|
||||||
|
McpSseServerConfigSchema,
|
||||||
|
]);
|
||||||
|
|
||||||
const AgentSessionConfigSchema = z.object({
|
const AgentSessionConfigSchema = z.object({
|
||||||
provider: AgentProviderSchema,
|
provider: AgentProviderSchema,
|
||||||
cwd: z.string(),
|
cwd: z.string(),
|
||||||
@@ -70,7 +95,7 @@ const AgentSessionConfigSchema = z.object({
|
|||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.optional(),
|
.optional(),
|
||||||
mcpServers: z.record(z.unknown()).optional(),
|
mcpServers: z.record(McpServerConfigSchema).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const AgentPermissionUpdateSchema = z.record(z.unknown());
|
const AgentPermissionUpdateSchema = z.record(z.unknown());
|
||||||
|
|||||||
Reference in New Issue
Block a user