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
|
||||
|
||||
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"}
|
||||
@@ -451,8 +451,8 @@ E2E tests can verify both structured data (for correctness) and formatted output
|
||||
|
||||
```typescript
|
||||
// Verify JSON output is valid and contains expected data
|
||||
test('agent list --format json', async () => {
|
||||
const output = await ctx.paseo('agent list --format json')
|
||||
test('agent list --json', async () => {
|
||||
const output = await ctx.paseo('agent list --json')
|
||||
const data = JSON.parse(output.stdout)
|
||||
|
||||
expect(data).toBeInstanceOf(Array)
|
||||
@@ -475,7 +475,8 @@ Add output options to the root command:
|
||||
|
||||
```typescript
|
||||
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('--no-headers', 'Omit table headers')
|
||||
.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')
|
||||
.version(VERSION, '-v, --version', 'output the version number')
|
||||
// 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('--no-headers', 'omit table headers')
|
||||
.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('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action((options, command) => {
|
||||
if (options.json) {
|
||||
command.parent.opts().format = 'json'
|
||||
}
|
||||
return withOutput(runLsCommand)(options, command)
|
||||
})
|
||||
.action(withOutput(runLsCommand))
|
||||
|
||||
program
|
||||
.command('run')
|
||||
@@ -69,6 +65,7 @@ export function createCli(): Command {
|
||||
.option('--cwd <path>', 'Working directory (default: current)')
|
||||
.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('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runRunCommand))
|
||||
|
||||
@@ -96,6 +93,7 @@ export function createCli(): Command {
|
||||
.argument('[id]', 'Agent ID (or prefix) - optional if --all or --cwd specified')
|
||||
.option('--all', 'Stop all agents')
|
||||
.option('--cwd <path>', 'Stop all agents in directory')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runStopCommand))
|
||||
|
||||
@@ -106,6 +104,7 @@ export function createCli(): Command {
|
||||
.argument('<prompt>', 'The message to send')
|
||||
.option('--no-wait', 'Return immediately without waiting for completion')
|
||||
.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)')
|
||||
.action(withOutput(runSendCommand))
|
||||
|
||||
@@ -115,18 +114,14 @@ export function createCli(): Command {
|
||||
.argument('<id>', 'Agent ID (or prefix)')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action((id, options, command) => {
|
||||
if (options.json) {
|
||||
command.parent.opts().format = 'json'
|
||||
}
|
||||
return withOutput(runInspectCommand)(id, options, command)
|
||||
})
|
||||
.action(withOutput(runInspectCommand))
|
||||
|
||||
program
|
||||
.command('wait')
|
||||
.description('Wait for an agent to become idle')
|
||||
.argument('<id>', 'Agent ID (or prefix)')
|
||||
.option('--timeout <seconds>', 'Maximum wait time (default: 600)')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runWaitCommand))
|
||||
|
||||
|
||||
@@ -29,12 +29,7 @@ export function createAgentCommand(): Command {
|
||||
.option('--ui', 'Show only UI agents (equivalent to --label ui=true)')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action((options, command) => {
|
||||
if (options.json) {
|
||||
command.parent.parent.opts().format = 'json'
|
||||
}
|
||||
return withOutput(runLsCommand)(options, command)
|
||||
})
|
||||
.action(withOutput(runLsCommand))
|
||||
|
||||
agent
|
||||
.command('run')
|
||||
@@ -48,6 +43,7 @@ export function createAgentCommand(): Command {
|
||||
.option('--cwd <path>', 'Working directory (default: current)')
|
||||
.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('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runRunCommand))
|
||||
|
||||
@@ -74,6 +70,7 @@ export function createAgentCommand(): Command {
|
||||
.argument('[id]', 'Agent ID (or prefix) - optional if --all or --cwd specified')
|
||||
.option('--all', 'Stop all agents')
|
||||
.option('--cwd <path>', 'Stop all agents in directory')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runStopCommand))
|
||||
|
||||
@@ -83,6 +80,7 @@ export function createAgentCommand(): Command {
|
||||
.argument('<id>', 'Agent ID (or prefix)')
|
||||
.argument('<prompt>', 'The message to send')
|
||||
.option('--no-wait', 'Return immediately without waiting for completion')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runSendCommand))
|
||||
|
||||
@@ -90,6 +88,7 @@ export function createAgentCommand(): Command {
|
||||
.command('inspect')
|
||||
.description('Show detailed information about an agent')
|
||||
.argument('<id>', 'Agent ID (or prefix)')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runInspectCommand))
|
||||
|
||||
@@ -98,6 +97,7 @@ export function createAgentCommand(): Command {
|
||||
.description('Wait for an agent to become idle')
|
||||
.argument('<id>', 'Agent ID (or prefix)')
|
||||
.option('--timeout <seconds>', 'Maximum wait time (default: 600)')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runWaitCommand))
|
||||
|
||||
@@ -108,6 +108,7 @@ export function createAgentCommand(): Command {
|
||||
.argument('<id>', 'Agent ID (or prefix)')
|
||||
.argument('[mode]', 'Mode to set (required unless --list)')
|
||||
.option('--list', 'List available modes for this agent')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runModeCommand))
|
||||
|
||||
@@ -116,6 +117,7 @@ export function createAgentCommand(): Command {
|
||||
.description('Archive an agent (soft-delete)')
|
||||
.argument('<id>', 'Agent ID (or prefix)')
|
||||
.option('--force', 'Force archive running agent')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runArchiveCommand))
|
||||
|
||||
|
||||
@@ -20,6 +20,15 @@ export interface AgentLogsOptions extends CommandOptions {
|
||||
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
|
||||
*/
|
||||
@@ -110,6 +119,12 @@ export async function runLogsCommand(
|
||||
|
||||
// For follow mode, we stream events continuously
|
||||
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)
|
||||
return
|
||||
}
|
||||
@@ -159,18 +174,22 @@ export async function runLogsCommand(
|
||||
timelineItems = timelineItems.filter((item) => matchesFilter(item, options.filter))
|
||||
}
|
||||
|
||||
// Apply tail limit
|
||||
if (options.tail) {
|
||||
const tailCount = parseInt(options.tail, 10)
|
||||
if (!isNaN(tailCount) && tailCount > 0) {
|
||||
timelineItems = timelineItems.slice(-tailCount)
|
||||
}
|
||||
const tailCount = parseTailCount(options.tail)
|
||||
if (options.tail !== undefined && tailCount === 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 client.close()
|
||||
|
||||
// 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)
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
@@ -188,6 +207,9 @@ async function runFollowMode(
|
||||
agentId: string,
|
||||
options: AgentLogsOptions
|
||||
): Promise<void> {
|
||||
const DEFAULT_FOLLOW_TAIL = 10
|
||||
const tailCount = parseTailCount(options.tail) ?? DEFAULT_FOLLOW_TAIL
|
||||
|
||||
// First, get existing timeline
|
||||
const snapshotPromise = new Promise<AgentTimelineItem[]>((resolve) => {
|
||||
const timeout = setTimeout(() => resolve([]), 10000)
|
||||
@@ -218,22 +240,17 @@ async function runFollowMode(
|
||||
existingItems = existingItems.filter((item) => matchesFilter(item, options.filter))
|
||||
}
|
||||
|
||||
// Apply tail to existing items
|
||||
if (options.tail) {
|
||||
const tailCount = parseInt(options.tail, 10)
|
||||
if (!isNaN(tailCount) && tailCount > 0) {
|
||||
existingItems = existingItems.slice(-tailCount)
|
||||
// Print existing transcript (tail-like behavior)
|
||||
if (tailCount > 0) {
|
||||
const existingTranscript = curateAgentActivity(existingItems, { maxItems: tailCount })
|
||||
if (existingTranscript !== 'No activity to display.') {
|
||||
console.log(existingTranscript)
|
||||
}
|
||||
}
|
||||
|
||||
// Print existing transcript
|
||||
const existingTranscript = curateAgentActivity(existingItems)
|
||||
if (existingTranscript !== 'No activity to display.') {
|
||||
console.log(existingTranscript)
|
||||
}
|
||||
|
||||
// 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 message = msg as AgentStreamMessage
|
||||
|
||||
@@ -15,22 +15,19 @@ export function createDaemonCommand(): Command {
|
||||
.description('Show daemon status')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action((options, command) => {
|
||||
if (options.json) {
|
||||
command.parent.parent.opts().format = 'json'
|
||||
}
|
||||
return withOutput(runStatusCommand)(options, command)
|
||||
})
|
||||
.action(withOutput(runStatusCommand))
|
||||
|
||||
daemon
|
||||
.command('stop')
|
||||
.description('Stop the daemon')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runStopCommand))
|
||||
|
||||
daemon
|
||||
.command('restart')
|
||||
.description('Restart the daemon')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runRestartCommand))
|
||||
|
||||
|
||||
@@ -12,12 +12,7 @@ export function createPermitCommand(): Command {
|
||||
.description('List all pending permissions')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action((options, command) => {
|
||||
if (options.json) {
|
||||
command.parent.parent.opts().format = 'json'
|
||||
}
|
||||
return withOutput(runLsCommand)(options, command)
|
||||
})
|
||||
.action(withOutput(runLsCommand))
|
||||
|
||||
permit
|
||||
.command('allow')
|
||||
@@ -26,6 +21,7 @@ export function createPermitCommand(): Command {
|
||||
.argument('[req_id]', 'Permission request ID (optional if --all)')
|
||||
.option('--all', 'Allow all pending permissions for this agent')
|
||||
.option('--input <json>', 'Modified input parameters (JSON)')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runAllowCommand))
|
||||
|
||||
@@ -37,6 +33,7 @@ export function createPermitCommand(): Command {
|
||||
.option('--all', 'Deny all pending permissions for this agent')
|
||||
.option('--message <msg>', 'Denial reason message')
|
||||
.option('--interrupt', 'Stop agent after denial')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runDenyCommand))
|
||||
|
||||
|
||||
@@ -11,12 +11,7 @@ export function createProviderCommand(): Command {
|
||||
.description('List available providers and status')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action((options, command) => {
|
||||
if (options.json) {
|
||||
command.parent.parent.opts().format = 'json'
|
||||
}
|
||||
return withOutput(runLsCommand)(options, command)
|
||||
})
|
||||
.action(withOutput(runLsCommand))
|
||||
|
||||
provider
|
||||
.command('models')
|
||||
@@ -24,12 +19,7 @@ export function createProviderCommand(): Command {
|
||||
.argument('<provider>', 'Provider name (claude, codex, opencode)')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action((provider, options, command) => {
|
||||
if (options.json) {
|
||||
command.parent.parent.opts().format = 'json'
|
||||
}
|
||||
return withOutput(runModelsCommand)(provider, options, command)
|
||||
})
|
||||
.action(withOutput(runModelsCommand))
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
@@ -11,17 +11,13 @@ export function createWorktreeCommand(): Command {
|
||||
.description('List Paseo-managed git worktrees')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action((options, command) => {
|
||||
if (options.json) {
|
||||
command.parent.parent.opts().format = 'json'
|
||||
}
|
||||
return withOutput(runLsCommand)(options, command)
|
||||
})
|
||||
.action(withOutput(runLsCommand))
|
||||
|
||||
worktree
|
||||
.command('archive')
|
||||
.description('Archive a worktree (removes worktree and associated branch)')
|
||||
.argument('<name>', 'Worktree name or branch name')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
|
||||
.action(withOutput(runArchiveCommand))
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
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'
|
||||
|
||||
/** Options that include output settings from global options */
|
||||
@@ -13,10 +13,26 @@ export interface CommandOptions extends Partial<OutputOptions> {
|
||||
[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 */
|
||||
function extractOutputOptions(options: CommandOptions): OutputOptions {
|
||||
return {
|
||||
format: (options.format as OutputOptions['format']) ?? defaultOutputOptions.format,
|
||||
format: options.json ? 'json' : normalizeFormat(options.format ?? defaultOutputOptions.format),
|
||||
quiet: options.quiet ?? defaultOutputOptions.quiet,
|
||||
noHeaders: options.headers === false, // Commander uses --no-headers -> headers: false
|
||||
noColor: options.color === false, // Commander uses --no-color -> color: false
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* Tests:
|
||||
* - daemon --help shows subcommands
|
||||
* - 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
|
||||
*/
|
||||
|
||||
@@ -56,11 +56,11 @@ try {
|
||||
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 =
|
||||
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)
|
||||
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
|
||||
// But output should be valid JSON
|
||||
@@ -68,14 +68,14 @@ try {
|
||||
if (output.length > 0) {
|
||||
try {
|
||||
JSON.parse(output)
|
||||
console.log('✓ daemon status --format json outputs valid JSON\n')
|
||||
console.log('✓ daemon status --json outputs valid JSON\n')
|
||||
} catch {
|
||||
// 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 {
|
||||
// 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 ls --help shows options
|
||||
* - 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 -g flag is accepted
|
||||
*/
|
||||
@@ -71,11 +71,11 @@ try {
|
||||
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 =
|
||||
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)
|
||||
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
|
||||
// But output should be valid JSON if present
|
||||
@@ -83,13 +83,13 @@ try {
|
||||
if (output.length > 0) {
|
||||
try {
|
||||
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 {
|
||||
// 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 {
|
||||
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')
|
||||
}
|
||||
|
||||
// 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 =
|
||||
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
|
||||
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')
|
||||
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
|
||||
|
||||
@@ -112,15 +112,15 @@ try {
|
||||
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 =
|
||||
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
|
||||
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')
|
||||
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
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* - permit --help shows subcommands
|
||||
* - permit ls --help shows options
|
||||
* - permit ls returns error when no daemon
|
||||
* - permit ls --format json handles errors
|
||||
* - permit ls --json handles errors
|
||||
*/
|
||||
|
||||
import assert from 'node:assert'
|
||||
@@ -67,11 +67,11 @@ try {
|
||||
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 =
|
||||
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)
|
||||
assert.notStrictEqual(result.exitCode, 0, 'should fail when daemon not running')
|
||||
// But output should be valid JSON if present
|
||||
@@ -79,13 +79,13 @@ try {
|
||||
if (output.length > 0) {
|
||||
try {
|
||||
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 {
|
||||
// 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 {
|
||||
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')
|
||||
}
|
||||
|
||||
// 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 =
|
||||
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
|
||||
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')
|
||||
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
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
* Tests:
|
||||
* - provider --help shows subcommands
|
||||
* - 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 models claude lists claude models
|
||||
* - provider models codex lists codex models
|
||||
* - provider models opencode lists opencode models
|
||||
* - provider models unknown fails with error
|
||||
* - provider models --format json outputs valid JSON
|
||||
* - provider models --json outputs valid JSON
|
||||
*/
|
||||
|
||||
import assert from 'node:assert'
|
||||
@@ -47,10 +47,10 @@ console.log('=== Provider Commands ===\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')
|
||||
const result = await $`npx paseo provider ls --format json`.nothrow()
|
||||
console.log('Test 3: provider ls --json outputs valid JSON')
|
||||
const result = await $`npx paseo provider ls --json`.nothrow()
|
||||
assert.strictEqual(result.exitCode, 0, 'should exit 0')
|
||||
const data = JSON.parse(result.stdout.trim())
|
||||
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 === 'codex'), 'should include codex')
|
||||
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
|
||||
@@ -119,16 +119,16 @@ console.log('=== Provider Commands ===\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')
|
||||
const result = await $`npx paseo provider models claude --format json`.nothrow()
|
||||
console.log('Test 9: provider models --json outputs valid JSON')
|
||||
const result = await $`npx paseo provider models claude --json`.nothrow()
|
||||
assert.strictEqual(result.exitCode, 0, 'should exit 0')
|
||||
const data = JSON.parse(result.stdout.trim())
|
||||
assert(Array.isArray(data), 'output should be an array')
|
||||
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')
|
||||
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
|
||||
|
||||
@@ -60,7 +60,7 @@ async function cleanup(): Promise<void> {
|
||||
async function test_agent_ls_empty(): Promise<void> {
|
||||
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('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> {
|
||||
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('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')
|
||||
|
||||
// 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 }>
|
||||
|
||||
// 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()
|
||||
|
||||
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) {
|
||||
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 }> {
|
||||
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('Stdout:', result.stdout)
|
||||
|
||||
@@ -4,6 +4,44 @@ export type AgentProvider = string;
|
||||
|
||||
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 = {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -208,7 +246,7 @@ export type AgentSessionConfig = {
|
||||
codex?: AgentMetadata;
|
||||
claude?: Partial<ClaudeAgentOptions>;
|
||||
};
|
||||
mcpServers?: AgentMetadata;
|
||||
mcpServers?: Record<string, McpServerConfig>;
|
||||
/**
|
||||
* Internal agents are hidden from listings and don't trigger notifications.
|
||||
* They are used for ephemeral system tasks like commit/PR generation.
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
query,
|
||||
type AgentDefinition,
|
||||
type CanUseTool,
|
||||
type McpServerConfig,
|
||||
type McpServerConfig as ClaudeSdkMcpServerConfig,
|
||||
type ModelInfo,
|
||||
type Options,
|
||||
type PermissionMode,
|
||||
@@ -45,6 +45,7 @@ import type {
|
||||
AgentRuntimeInfo,
|
||||
ListModelsOptions,
|
||||
ListPersistedAgentsOptions,
|
||||
McpServerConfig,
|
||||
PersistedAgentDescriptor,
|
||||
} from "../agent-sdk-types.js";
|
||||
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
||||
@@ -102,7 +103,6 @@ type ClaudeAgentConfig = AgentSessionConfig & { provider: "claude" };
|
||||
|
||||
export type ClaudeContentChunk = { type: string; [key: string]: any };
|
||||
|
||||
type ClaudeMcpServerConfig = McpServerConfig;
|
||||
type ClaudeOptions = Options;
|
||||
|
||||
type ClaudeAgentClientOptions = {
|
||||
@@ -178,6 +178,32 @@ function isMetadata(value: unknown): value is AgentMetadata {
|
||||
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 {
|
||||
return typeof value === "string" && VALID_CLAUDE_MODES.has(value);
|
||||
}
|
||||
@@ -230,18 +256,37 @@ function coerceSessionMetadata(metadata: AgentMetadata | undefined): Partial<Age
|
||||
result.extra = extra;
|
||||
}
|
||||
}
|
||||
if (isMetadata(metadata.mcpServers)) {
|
||||
if (isMcpServersRecord(metadata.mcpServers)) {
|
||||
result.mcpServers = metadata.mcpServers;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function isClaudeMcpServerConfig(value: unknown): value is ClaudeMcpServerConfig {
|
||||
if (!isMetadata(value)) {
|
||||
return false;
|
||||
function toClaudeSdkMcpConfig(
|
||||
config: McpServerConfig
|
||||
): 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 {
|
||||
@@ -783,10 +828,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
};
|
||||
|
||||
if (this.config.mcpServers) {
|
||||
const normalizedUserServers = this.normalizeMcpServers(this.config.mcpServers);
|
||||
if (normalizedUserServers) {
|
||||
base.mcpServers = normalizedUserServers;
|
||||
}
|
||||
base.mcpServers = this.normalizeMcpServers(this.config.mcpServers);
|
||||
}
|
||||
|
||||
if (this.config.model) {
|
||||
@@ -800,18 +842,13 @@ class ClaudeAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
private normalizeMcpServers(
|
||||
servers: ClaudeAgentConfig["mcpServers"]
|
||||
): Record<string, ClaudeMcpServerConfig> | undefined {
|
||||
if (!servers) {
|
||||
return undefined;
|
||||
}
|
||||
const result: Record<string, ClaudeMcpServerConfig> = {};
|
||||
servers: Record<string, McpServerConfig>
|
||||
): Record<string, ClaudeSdkMcpServerConfig> {
|
||||
const result: Record<string, ClaudeSdkMcpServerConfig> = {};
|
||||
for (const [name, config] of Object.entries(servers)) {
|
||||
if (isClaudeMcpServerConfig(config)) {
|
||||
result[name] = config;
|
||||
}
|
||||
result[name] = toClaudeSdkMcpConfig(config);
|
||||
}
|
||||
return Object.keys(result).length ? result : undefined;
|
||||
return result;
|
||||
}
|
||||
|
||||
private toSdkUserMessage(prompt: AgentPromptInput): SDKUserMessage {
|
||||
|
||||
@@ -37,6 +37,7 @@ import type {
|
||||
AgentRuntimeInfo,
|
||||
ListModelsOptions,
|
||||
ListPersistedAgentsOptions,
|
||||
McpServerConfig,
|
||||
PersistedAgentDescriptor,
|
||||
} from "../agent-sdk-types.js";
|
||||
import { curateAgentActivity } from "../activity-curator.js";
|
||||
@@ -1866,6 +1867,31 @@ const AgentSessionExtraSchema = z.object({
|
||||
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
|
||||
.object({
|
||||
provider: z.string(),
|
||||
@@ -1879,7 +1905,7 @@ const AgentSessionConfigSchema = z
|
||||
webSearch: z.boolean().optional(),
|
||||
reasoningEffort: z.string().optional(),
|
||||
extra: AgentSessionExtraSchema.optional(),
|
||||
mcpServers: z.record(z.unknown()).optional(),
|
||||
mcpServers: z.record(McpServerConfigSchema).optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
@@ -2693,14 +2719,35 @@ function getCodexMcpCommand(): string {
|
||||
}
|
||||
}
|
||||
|
||||
type CodexMcpServerConfig = {
|
||||
interface CodexMcpServerConfig {
|
||||
url?: string;
|
||||
http_headers?: Record<string, string>; // Static HTTP headers for HTTP servers
|
||||
http_headers?: Record<string, string>;
|
||||
command?: string;
|
||||
args?: 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 = {
|
||||
mcp_servers?: Record<string, CodexMcpServerConfig>;
|
||||
@@ -2754,22 +2801,10 @@ function buildCodexMcpConfig(
|
||||
// Build MCP servers configuration
|
||||
const mcpServers: Record<string, CodexMcpServerConfig> = {};
|
||||
|
||||
// Merge MCP servers from extra.codex.mcp_servers (legacy location)
|
||||
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)
|
||||
// Merge user-provided MCP servers
|
||||
if (config.mcpServers) {
|
||||
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
||||
if (typeof serverConfig === "object" && serverConfig !== null) {
|
||||
mcpServers[name] = serverConfig as CodexMcpServerConfig;
|
||||
}
|
||||
mcpServers[name] = toCodexMcpConfig(serverConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,31 @@ const AgentUsageSchema: z.ZodType<AgentUsage> = z.object({
|
||||
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({
|
||||
provider: AgentProviderSchema,
|
||||
cwd: z.string(),
|
||||
@@ -70,7 +95,7 @@ const AgentSessionConfigSchema = z.object({
|
||||
})
|
||||
.partial()
|
||||
.optional(),
|
||||
mcpServers: z.record(z.unknown()).optional(),
|
||||
mcpServers: z.record(McpServerConfigSchema).optional(),
|
||||
});
|
||||
|
||||
const AgentPermissionUpdateSchema = z.record(z.unknown());
|
||||
|
||||
Reference in New Issue
Block a user