Switch to double quotes and reformat codebase with Biome

This commit is contained in:
Mohamed Boudra
2026-03-21 20:22:15 +07:00
parent ab40786882
commit f6579dbeb4
805 changed files with 38132 additions and 40175 deletions
+61 -56
View File
@@ -1,126 +1,131 @@
import type { Command } from 'commander'
import { connectToDaemon, getDaemonHost, resolveAgentId } from '../../utils/client.js'
import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js'
import type { Command } from "commander";
import { connectToDaemon, getDaemonHost, resolveAgentId } from "../../utils/client.js";
import type {
CommandOptions,
SingleResult,
OutputSchema,
CommandError,
} from "../../output/index.js";
/** Result type for agent archive command */
export interface AgentArchiveResult {
agentId: string
status: 'archived'
archivedAt: string
agentId: string;
status: "archived";
archivedAt: string;
}
/** Schema for archive command output */
export const archiveSchema: OutputSchema<AgentArchiveResult> = {
idField: 'agentId',
idField: "agentId",
columns: [
{ header: 'AGENT ID', field: 'agentId' },
{ header: 'STATUS', field: 'status' },
{ header: 'ARCHIVED AT', field: 'archivedAt' },
{ header: "AGENT ID", field: "agentId" },
{ header: "STATUS", field: "status" },
{ header: "ARCHIVED AT", field: "archivedAt" },
],
}
};
export interface AgentArchiveOptions extends CommandOptions {
force?: boolean
host?: string
force?: boolean;
host?: string;
}
export type AgentArchiveCommandResult = SingleResult<AgentArchiveResult>
export type AgentArchiveCommandResult = SingleResult<AgentArchiveResult>;
export async function runArchiveCommand(
agentIdArg: string,
options: AgentArchiveOptions,
_command: Command
_command: Command,
): Promise<AgentArchiveCommandResult> {
const host = getDaemonHost({ host: options.host as string | undefined })
const host = getDaemonHost({ host: options.host as string | undefined });
// Validate arguments
if (!agentIdArg || agentIdArg.trim().length === 0) {
const error: CommandError = {
code: 'MISSING_AGENT_ID',
message: 'Agent ID is required',
details: 'Usage: paseo agent archive <id-or-name>',
}
throw error
code: "MISSING_AGENT_ID",
message: "Agent ID is required",
details: "Usage: paseo agent archive <id-or-name>",
};
throw error;
}
let client
let client;
try {
client = await connectToDaemon({ host: options.host as string | undefined })
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'DAEMON_NOT_RUNNING',
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: 'Start the daemon with: paseo daemon start',
}
throw error
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
try {
const agentsPayload = await client.fetchAgents({ filter: { includeArchived: true } })
const agents = agentsPayload.entries.map((entry) => entry.agent)
const agentId = resolveAgentId(agentIdArg, agents)
const agentsPayload = await client.fetchAgents({ filter: { includeArchived: true } });
const agents = agentsPayload.entries.map((entry) => entry.agent);
const agentId = resolveAgentId(agentIdArg, agents);
if (!agentId) {
const error: CommandError = {
code: 'AGENT_NOT_FOUND',
code: "AGENT_NOT_FOUND",
message: `Agent not found: ${agentIdArg}`,
details: 'Use "paseo ls" to list available agents',
}
throw error
};
throw error;
}
const agent = agents.find((entry) => entry.id === agentId)
const agent = agents.find((entry) => entry.id === agentId);
if (!agent) {
throw new Error(`Resolved agent missing from fetched agents: ${agentId}`)
throw new Error(`Resolved agent missing from fetched agents: ${agentId}`);
}
// Check if agent is already archived
if (agent.archivedAt) {
const error: CommandError = {
code: 'AGENT_ALREADY_ARCHIVED',
code: "AGENT_ALREADY_ARCHIVED",
message: `Agent ${agentId.slice(0, 7)} is already archived`,
details: `Archived at: ${agent.archivedAt}`,
}
throw error
};
throw error;
}
// Check if agent is running and reject unless --force is set
if (agent.status === 'running' && !options.force) {
if (agent.status === "running" && !options.force) {
const error: CommandError = {
code: 'AGENT_RUNNING',
code: "AGENT_RUNNING",
message: `Agent ${agentId.slice(0, 7)} is currently running`,
details:
'Use --force to archive a running agent (it will interrupt the active run), or stop it first with: paseo agent stop. Use paseo agent delete to hard-delete it.',
}
throw error
"Use --force to archive a running agent (it will interrupt the active run), or stop it first with: paseo agent stop. Use paseo agent delete to hard-delete it.",
};
throw error;
}
// Archive the agent
const result = await client.archiveAgent(agentId)
const result = await client.archiveAgent(agentId);
await client.close()
await client.close();
return {
type: 'single',
type: "single",
data: {
agentId,
status: 'archived',
status: "archived",
archivedAt: result.archivedAt,
},
schema: archiveSchema,
}
};
} catch (err) {
await client.close().catch(() => {})
await client.close().catch(() => {});
// Re-throw CommandError as-is
if (err && typeof err === 'object' && 'code' in err) {
throw err
if (err && typeof err === "object" && "code" in err) {
throw err;
}
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'ARCHIVE_FAILED',
code: "ARCHIVE_FAILED",
message: `Failed to archive agent: ${message}`,
}
throw error
};
throw error;
}
}
+90 -90
View File
@@ -1,16 +1,16 @@
import type { Command } from 'commander'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import { fetchProjectedTimelineItems } from '../../utils/timeline.js'
import type { Command } from "commander";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import { fetchProjectedTimelineItems } from "../../utils/timeline.js";
import type {
DaemonClient,
AgentStreamMessage,
AgentStreamEventPayload,
AgentTimelineItem,
} from '@getpaseo/server'
} from "@getpaseo/server";
export interface AgentAttachOptions {
host?: string
[key: string]: unknown
host?: string;
[key: string]: unknown;
}
/**
@@ -18,41 +18,41 @@ export interface AgentAttachOptions {
*/
function printTimelineItem(item: AgentTimelineItem): void {
switch (item.type) {
case 'assistant_message':
case "assistant_message":
// Print assistant text directly
process.stdout.write(item.text)
break
process.stdout.write(item.text);
break;
case 'reasoning':
case "reasoning":
// Print reasoning in a muted color if available
console.log(`\n[Reasoning] ${item.text}`)
break
console.log(`\n[Reasoning] ${item.text}`);
break;
case 'tool_call': {
const toolName = item.name
const status = item.status ?? 'started'
console.log(`\n[Tool: ${toolName}] ${status}`)
break
case "tool_call": {
const toolName = item.name;
const status = item.status ?? "started";
console.log(`\n[Tool: ${toolName}] ${status}`);
break;
}
case 'todo': {
const completed = item.items.filter((i) => i.completed).length
const total = item.items.length
console.log(`\n[Todo] ${completed}/${total} completed`)
break
case "todo": {
const completed = item.items.filter((i) => i.completed).length;
const total = item.items.length;
console.log(`\n[Todo] ${completed}/${total} completed`);
break;
}
case 'error':
console.error(`\n[Error] ${item.message}`)
break
case "error":
console.error(`\n[Error] ${item.message}`);
break;
case 'user_message':
console.log(`\n[User] ${item.text}`)
break
case "user_message":
console.log(`\n[User] ${item.text}`);
break;
default:
// Unknown item type, skip
break
break;
}
}
@@ -61,33 +61,33 @@ function printTimelineItem(item: AgentTimelineItem): void {
*/
function printStreamEvent(event: AgentStreamEventPayload): void {
switch (event.type) {
case 'timeline':
case "timeline":
// Print the timeline item
printTimelineItem(event.item)
break
printTimelineItem(event.item);
break;
case 'permission_requested':
console.log(`\n[Permission Required] ${event.request.name}`)
case "permission_requested":
console.log(`\n[Permission Required] ${event.request.name}`);
if (event.request.description) {
console.log(` ${event.request.description}`)
console.log(` ${event.request.description}`);
}
break
break;
case 'permission_resolved':
console.log(`\n[Permission ${event.resolution.behavior}]`)
break
case "permission_resolved":
console.log(`\n[Permission ${event.resolution.behavior}]`);
break;
case 'turn_failed':
console.error(`\n[Turn Failed] ${event.error}`)
break
case "turn_failed":
console.error(`\n[Turn Failed] ${event.error}`);
break;
case 'attention_required':
console.log(`\n[Attention Required: ${event.reason}]`)
break
case "attention_required":
console.log(`\n[Attention Required: ${event.reason}]`);
break;
default:
// Other event types are internal
break
break;
}
}
@@ -97,91 +97,91 @@ function printStreamEvent(event: AgentStreamEventPayload): void {
export async function runAttachCommand(
id: string,
options: AgentAttachOptions,
_command: Command
_command: Command,
): Promise<void> {
const host = getDaemonHost({ host: options.host as string | undefined })
const host = getDaemonHost({ host: options.host as string | undefined });
if (!id) {
console.error('Error: Agent ID required')
console.error('Usage: paseo attach <id>')
process.exit(1)
console.error("Error: Agent ID required");
console.error("Usage: paseo attach <id>");
process.exit(1);
}
let client: DaemonClient
let client: DaemonClient;
try {
client = await connectToDaemon({ host: options.host as string | undefined })
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
console.error(`Error: Cannot connect to daemon at ${host}: ${message}`)
console.error('Start the daemon with: paseo daemon start')
process.exit(1)
const message = err instanceof Error ? err.message : String(err);
console.error(`Error: Cannot connect to daemon at ${host}: ${message}`);
console.error("Start the daemon with: paseo daemon start");
process.exit(1);
}
try {
const fetchResult = await client.fetchAgent(id)
const fetchResult = await client.fetchAgent(id);
if (!fetchResult) {
console.error(`Error: No agent found matching: ${id}`)
console.error('Use `paseo ls` to list available agents')
await client.close()
process.exit(1)
console.error(`Error: No agent found matching: ${id}`);
console.error("Use `paseo ls` to list available agents");
await client.close();
process.exit(1);
}
const resolvedId = fetchResult.agent.id
const resolvedId = fetchResult.agent.id;
// Print header
console.log(`Attaching to agent ${resolvedId.substring(0, 7)}...`)
console.log(`(Press Ctrl+C to detach)\n`)
console.log(`Attaching to agent ${resolvedId.substring(0, 7)}...`);
console.log(`(Press Ctrl+C to detach)\n`);
// Print existing output from timeline fetch.
try {
const timelineItems = await fetchProjectedTimelineItems({
client,
agentId: resolvedId,
})
});
for (const item of timelineItems) {
printTimelineItem(item)
printTimelineItem(item);
}
} catch (error) {
console.warn('Warning: failed to fetch existing timeline', error)
console.warn("Warning: failed to fetch existing timeline", error);
}
// Subscribe to new events
const unsubscribe = client.on('agent_stream', (msg: unknown) => {
const message = msg as AgentStreamMessage
if (message.type !== 'agent_stream') return
if (message.payload.agentId !== resolvedId) return
const unsubscribe = client.on("agent_stream", (msg: unknown) => {
const message = msg as AgentStreamMessage;
if (message.type !== "agent_stream") return;
if (message.payload.agentId !== resolvedId) return;
printStreamEvent(message.payload.event)
})
printStreamEvent(message.payload.event);
});
// Handle Ctrl+C to detach gracefully
let detached = false
let detached = false;
const detach = () => {
if (detached) return
detached = true
if (detached) return;
detached = true;
console.log('\n\nDetaching from agent...')
unsubscribe()
console.log("\n\nDetaching from agent...");
unsubscribe();
client
.close()
.then(() => {
process.exit(0)
process.exit(0);
})
.catch(() => {
process.exit(1)
})
}
process.exit(1);
});
};
process.on('SIGINT', detach)
process.on('SIGTERM', detach)
process.on("SIGINT", detach);
process.on("SIGTERM", detach);
// Keep the process alive
await new Promise(() => {
// Wait indefinitely until interrupted
})
});
} catch (err) {
await client.close().catch(() => {})
const message = err instanceof Error ? err.message : String(err)
console.error(`Error: Failed to attach to agent: ${message}`)
process.exit(1)
await client.close().catch(() => {});
const message = err instanceof Error ? err.message : String(err);
console.error(`Error: Failed to attach to agent: ${message}`);
process.exit(1);
}
}
+62 -57
View File
@@ -1,114 +1,119 @@
import type { Command } from 'commander'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js'
import type { Command } from "commander";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type {
CommandOptions,
SingleResult,
OutputSchema,
CommandError,
} from "../../output/index.js";
export interface DeleteResult {
deletedCount: number
agentIds: string[]
deletedCount: number;
agentIds: string[];
}
export const deleteSchema: OutputSchema<DeleteResult> = {
idField: (item) => item.agentIds.join('\n'),
columns: [{ header: 'DELETED', field: 'deletedCount' }],
}
idField: (item) => item.agentIds.join("\n"),
columns: [{ header: "DELETED", field: "deletedCount" }],
};
export interface AgentDeleteOptions extends CommandOptions {
all?: boolean
cwd?: string
all?: boolean;
cwd?: string;
}
export type AgentDeleteResult = SingleResult<DeleteResult>
export type AgentDeleteResult = SingleResult<DeleteResult>;
export async function runDeleteCommand(
id: string | undefined,
options: AgentDeleteOptions,
_command: Command
_command: Command,
): Promise<AgentDeleteResult> {
const host = getDaemonHost({ host: options.host as string | undefined })
const host = getDaemonHost({ host: options.host as string | undefined });
if (!id && !options.all && !options.cwd) {
const error: CommandError = {
code: 'MISSING_ARGUMENT',
message: 'Agent ID required unless --all or --cwd is specified',
details: 'Usage: paseo agent delete <id> | --all | --cwd <path>',
}
throw error
code: "MISSING_ARGUMENT",
message: "Agent ID required unless --all or --cwd is specified",
details: "Usage: paseo agent delete <id> | --all | --cwd <path>",
};
throw error;
}
let client
let client;
try {
client = await connectToDaemon({ host: options.host as string | undefined })
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'DAEMON_NOT_RUNNING',
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: 'Start the daemon with: paseo daemon start',
}
throw error
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
try {
const fetchPayload = await client.fetchAgents({ filter: { includeArchived: true } })
let agents = fetchPayload.entries.map((entry) => entry.agent)
const deletedIds: string[] = []
const fetchPayload = await client.fetchAgents({ filter: { includeArchived: true } });
let agents = fetchPayload.entries.map((entry) => entry.agent);
const deletedIds: string[] = [];
if (options.all) {
agents = agents.filter((a) => !a.archivedAt)
agents = agents.filter((a) => !a.archivedAt);
} else if (options.cwd) {
const filterCwd = options.cwd
const filterCwd = options.cwd;
agents = agents.filter((a) => {
if (a.archivedAt) return false
const agentCwd = a.cwd.replace(/\/$/, '')
const targetCwd = filterCwd.replace(/\/$/, '')
return agentCwd === targetCwd || agentCwd.startsWith(targetCwd + '/')
})
if (a.archivedAt) return false;
const agentCwd = a.cwd.replace(/\/$/, "");
const targetCwd = filterCwd.replace(/\/$/, "");
return agentCwd === targetCwd || agentCwd.startsWith(targetCwd + "/");
});
} else if (id) {
const fetchResult = await client.fetchAgent(id)
const fetchResult = await client.fetchAgent(id);
if (!fetchResult) {
const error: CommandError = {
code: 'AGENT_NOT_FOUND',
code: "AGENT_NOT_FOUND",
message: `No agent found matching: ${id}`,
details: 'Use `paseo ls` to list available agents',
}
throw error
details: "Use `paseo ls` to list available agents",
};
throw error;
}
agents = [fetchResult.agent]
agents = [fetchResult.agent];
}
for (const agent of agents) {
try {
if (agent.status === 'running') {
await client.cancelAgent(agent.id)
if (agent.status === "running") {
await client.cancelAgent(agent.id);
}
await client.deleteAgent(agent.id)
deletedIds.push(agent.id)
await client.deleteAgent(agent.id);
deletedIds.push(agent.id);
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
console.error(`Warning: Failed to delete agent ${agent.id.slice(0, 7)}: ${message}`)
const message = err instanceof Error ? err.message : String(err);
console.error(`Warning: Failed to delete agent ${agent.id.slice(0, 7)}: ${message}`);
}
}
await client.close()
await client.close();
return {
type: 'single',
type: "single",
data: {
deletedCount: deletedIds.length,
agentIds: deletedIds,
},
schema: deleteSchema,
}
};
} catch (err) {
await client.close().catch(() => {})
if (err && typeof err === 'object' && 'code' in err) {
throw err
await client.close().catch(() => {});
if (err && typeof err === "object" && "code" in err) {
throw err;
}
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'DELETE_AGENT_FAILED',
code: "DELETE_AGENT_FAILED",
message: `Failed to delete agent(s): ${message}`,
}
throw error
};
throw error;
}
}
+119 -99
View File
@@ -1,149 +1,169 @@
import { Command } from 'commander'
import { runModeCommand } from './mode.js'
import { runArchiveCommand } from './archive.js'
import { runDeleteCommand } from './delete.js'
import { runLsCommand } from './ls.js'
import { runRunCommand } from './run.js'
import { runLogsCommand } from './logs.js'
import { runStopCommand } from './stop.js'
import { runSendCommand } from './send.js'
import { runInspectCommand } from './inspect.js'
import { runWaitCommand } from './wait.js'
import { runAttachCommand } from './attach.js'
import { runUpdateCommand } from './update.js'
import { withOutput } from '../../output/index.js'
import { Command } from "commander";
import { runModeCommand } from "./mode.js";
import { runArchiveCommand } from "./archive.js";
import { runDeleteCommand } from "./delete.js";
import { runLsCommand } from "./ls.js";
import { runRunCommand } from "./run.js";
import { runLogsCommand } from "./logs.js";
import { runStopCommand } from "./stop.js";
import { runSendCommand } from "./send.js";
import { runInspectCommand } from "./inspect.js";
import { runWaitCommand } from "./wait.js";
import { runAttachCommand } from "./attach.js";
import { runUpdateCommand } from "./update.js";
import { withOutput } from "../../output/index.js";
import {
addDaemonHostOption,
addJsonAndDaemonHostOptions,
collectMultiple,
} from '../../utils/command-options.js'
} from "../../utils/command-options.js";
export function createAgentCommand(): Command {
const agent = new Command('agent').description('Manage agents (advanced operations)')
const agent = new Command("agent").description("Manage agents (advanced operations)");
// Primary agent commands (same as top-level)
addJsonAndDaemonHostOptions(
agent
.command('ls')
.description('List agents. By default excludes archived agents.')
.option('-a, --all', 'Include archived agents')
.option('-g, --global', 'Legacy no-op (kept for compatibility)')
.option('--label <key=value>', 'Filter by label (can be used multiple times)', collectMultiple, [])
.option('--thinking <id>', 'Filter by thinking option ID')
).action(withOutput(runLsCommand))
addJsonAndDaemonHostOptions(
agent
.command('run')
.description('Create and start an agent with a task')
.argument('<prompt>', 'The task/prompt for the agent')
.option('-d, --detach', 'Run in background (detached)')
.option('--name <name>', 'Assign a name/title to the agent')
.option('--provider <provider>', 'Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)', 'claude')
.option('--model <model>', 'Model to use (e.g., claude-sonnet-4-20250514, claude-3-5-haiku-20241022)')
.option('--thinking <id>', 'Thinking option ID to use for this run')
.option('--mode <mode>', 'Provider-specific mode (e.g., plan, default, bypass)')
.option('--cwd <path>', 'Working directory (default: current)')
.option('--label <key=value>', 'Add label(s) to the agent (can be used multiple times)', collectMultiple, [])
.command("ls")
.description("List agents. By default excludes archived agents.")
.option("-a, --all", "Include archived agents")
.option("-g, --global", "Legacy no-op (kept for compatibility)")
.option(
'--wait-timeout <duration>',
'Maximum time to wait for agent to finish (e.g., 30s, 5m, 1h). Default: no limit'
"--label <key=value>",
"Filter by label (can be used multiple times)",
collectMultiple,
[],
)
.option('--output-schema <schema>', 'Output JSON matching the provided schema file path or inline JSON schema')
).action(withOutput(runRunCommand))
.option("--thinking <id>", "Filter by thinking option ID"),
).action(withOutput(runLsCommand));
addJsonAndDaemonHostOptions(
agent
.command("run")
.description("Create and start an agent with a task")
.argument("<prompt>", "The task/prompt for the agent")
.option("-d, --detach", "Run in background (detached)")
.option("--name <name>", "Assign a name/title to the agent")
.option(
"--provider <provider>",
"Agent provider, or provider/model (e.g. codex or codex/gpt-5.4)",
"claude",
)
.option(
"--model <model>",
"Model to use (e.g., claude-sonnet-4-20250514, claude-3-5-haiku-20241022)",
)
.option("--thinking <id>", "Thinking option ID to use for this run")
.option("--mode <mode>", "Provider-specific mode (e.g., plan, default, bypass)")
.option("--cwd <path>", "Working directory (default: current)")
.option(
"--label <key=value>",
"Add label(s) to the agent (can be used multiple times)",
collectMultiple,
[],
)
.option(
"--wait-timeout <duration>",
"Maximum time to wait for agent to finish (e.g., 30s, 5m, 1h). Default: no limit",
)
.option(
"--output-schema <schema>",
"Output JSON matching the provided schema file path or inline JSON schema",
),
).action(withOutput(runRunCommand));
addDaemonHostOption(
agent
.command('attach')
.command("attach")
.description("Attach to a running agent's output stream")
.argument('<id>', 'Agent ID (or prefix)')
).action(runAttachCommand)
.argument("<id>", "Agent ID (or prefix)"),
).action(runAttachCommand);
addDaemonHostOption(
agent
.command('logs')
.description('View agent activity/timeline')
.argument('<id>', 'Agent ID (or prefix)')
.option('-f, --follow', 'Follow log output (streaming)')
.option('--tail <n>', 'Show last n entries')
.option('--filter <type>', 'Filter by event type (tools, text, errors, permissions)')
).action(runLogsCommand)
.command("logs")
.description("View agent activity/timeline")
.argument("<id>", "Agent ID (or prefix)")
.option("-f, --follow", "Follow log output (streaming)")
.option("--tail <n>", "Show last n entries")
.option("--filter <type>", "Filter by event type (tools, text, errors, permissions)"),
).action(runLogsCommand);
addJsonAndDaemonHostOptions(
agent
.command('stop')
.description('Interrupt an agent if it is running (no-op for idle agents)')
.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')
).action(withOutput(runStopCommand))
.command("stop")
.description("Interrupt an agent if it is running (no-op for idle agents)")
.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"),
).action(withOutput(runStopCommand));
addJsonAndDaemonHostOptions(
agent
.command('delete')
.description('Delete an agent (interrupt if running, then hard-delete)')
.argument('[id]', 'Agent ID (or prefix) - optional if --all or --cwd specified')
.option('--all', 'Delete all agents')
.option('--cwd <path>', 'Delete all agents in directory')
).action(withOutput(runDeleteCommand))
.command("delete")
.description("Delete an agent (interrupt if running, then hard-delete)")
.argument("[id]", "Agent ID (or prefix) - optional if --all or --cwd specified")
.option("--all", "Delete all agents")
.option("--cwd <path>", "Delete all agents in directory"),
).action(withOutput(runDeleteCommand));
addJsonAndDaemonHostOptions(
agent
.command('send')
.description('Send a message/task to an existing agent')
.argument('<id>', 'Agent ID (or prefix)')
.argument('[prompt]', 'The message to send')
.option('--prompt <text>', 'Provide the message inline as a flag')
.option('--prompt-file <path>', 'Read the message from a UTF-8 text file')
.option('--no-wait', 'Return immediately without waiting for completion')
).action(withOutput(runSendCommand))
.command("send")
.description("Send a message/task to an existing agent")
.argument("<id>", "Agent ID (or prefix)")
.argument("[prompt]", "The message to send")
.option("--prompt <text>", "Provide the message inline as a flag")
.option("--prompt-file <path>", "Read the message from a UTF-8 text file")
.option("--no-wait", "Return immediately without waiting for completion"),
).action(withOutput(runSendCommand));
addJsonAndDaemonHostOptions(
agent
.command('inspect')
.description('Show detailed information about an agent')
.argument('<id>', 'Agent ID (or prefix)')
).action(withOutput(runInspectCommand))
.command("inspect")
.description("Show detailed information about an agent")
.argument("<id>", "Agent ID (or prefix)"),
).action(withOutput(runInspectCommand));
addJsonAndDaemonHostOptions(
agent
.command('wait')
.description('Wait for an agent to become idle')
.argument('<id>', 'Agent ID (or prefix)')
.option('--timeout <seconds>', 'Maximum wait time (default: no limit)')
).action(withOutput(runWaitCommand))
.command("wait")
.description("Wait for an agent to become idle")
.argument("<id>", "Agent ID (or prefix)")
.option("--timeout <seconds>", "Maximum wait time (default: no limit)"),
).action(withOutput(runWaitCommand));
// Advanced agent commands (less common operations)
addJsonAndDaemonHostOptions(
agent
.command('mode')
.command("mode")
.description("Change an agent's operational mode")
.argument('<id>', 'Agent ID (or prefix)')
.argument('[mode]', 'Mode to set (required unless --list)')
.option('--list', 'List available modes for this agent')
).action(withOutput(runModeCommand))
.argument("<id>", "Agent ID (or prefix)")
.argument("[mode]", "Mode to set (required unless --list)")
.option("--list", "List available modes for this agent"),
).action(withOutput(runModeCommand));
addJsonAndDaemonHostOptions(
agent
.command('archive')
.description('Archive an agent (soft-delete)')
.argument('<id>', 'Agent ID, prefix, or name')
.option('--force', 'Force archive running agent (interrupts active run first)')
).action(withOutput(runArchiveCommand))
.command("archive")
.description("Archive an agent (soft-delete)")
.argument("<id>", "Agent ID, prefix, or name")
.option("--force", "Force archive running agent (interrupts active run first)"),
).action(withOutput(runArchiveCommand));
addJsonAndDaemonHostOptions(
agent
.command('update')
.command("update")
.description("Update an agent's metadata")
.argument('<id>', 'Agent ID (or prefix)')
.option('--name <name>', "Update the agent's display name")
.argument("<id>", "Agent ID (or prefix)")
.option("--name <name>", "Update the agent's display name")
.option(
'--label <label>',
'Add/set label(s) on the agent (can be used multiple times or comma-separated)',
"--label <label>",
"Add/set label(s) on the agent (can be used multiple times or comma-separated)",
collectMultiple,
[]
)
).action(withOutput(runUpdateCommand))
[],
),
).action(withOutput(runUpdateCommand));
return agent
return agent;
}
+127 -126
View File
@@ -1,101 +1,101 @@
import type { Command } from 'commander'
import type { AgentSnapshotPayload } from '@getpaseo/server'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions, ListResult, OutputSchema, CommandError } from '../../output/index.js'
import type { Command } from "commander";
import type { AgentSnapshotPayload } from "@getpaseo/server";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type { CommandOptions, ListResult, OutputSchema, CommandError } from "../../output/index.js";
/** Agent inspect data for display (matches CLI spec format) */
interface AgentInspect {
Id: string
Name: string
Provider: string
Model: string
Thinking: string
Status: string
Archived: boolean
ArchivedAt: string | null
Mode: string
Cwd: string
CreatedAt: string
UpdatedAt: string
Id: string;
Name: string;
Provider: string;
Model: string;
Thinking: string;
Status: string;
Archived: boolean;
ArchivedAt: string | null;
Mode: string;
Cwd: string;
CreatedAt: string;
UpdatedAt: string;
LastUsage: {
InputTokens: number
OutputTokens: number
CachedTokens: number
CostUsd: number
} | null
InputTokens: number;
OutputTokens: number;
CachedTokens: number;
CostUsd: number;
} | null;
Capabilities: {
Streaming: boolean
Persistence: boolean
DynamicModes: boolean
McpServers: boolean
} | null
Streaming: boolean;
Persistence: boolean;
DynamicModes: boolean;
McpServers: boolean;
} | null;
AvailableModes: Array<{
id: string
label: string
}> | null
id: string;
label: string;
}> | null;
PendingPermissions: Array<{
id: string
tool: string
}>
Worktree: string | null
ParentAgentId: string | null
id: string;
tool: string;
}>;
Worktree: string | null;
ParentAgentId: string | null;
}
/** Key-value row for table display */
interface InspectRow {
key: string
value: string
key: string;
value: string;
}
/** Schema for key-value display with custom serialization for JSON/YAML */
function createInspectSchema(agent: AgentInspect): OutputSchema<InspectRow> {
return {
idField: 'key',
idField: "key",
columns: [
{ header: 'KEY', field: 'key' },
{ header: "KEY", field: "key" },
{
header: 'VALUE',
field: 'value',
header: "VALUE",
field: "value",
color: (_, item) => {
if (item.key === 'Status') {
if (item.value === 'running') return 'green'
if (item.value === 'idle') return 'yellow'
if (item.value === 'error') return 'red'
if (item.key === "Status") {
if (item.value === "running") return "green";
if (item.value === "idle") return "yellow";
if (item.value === "error") return "red";
}
return undefined
return undefined;
},
},
],
// For JSON/YAML, return the structured agent object
serialize: (_item) => agent,
}
};
}
/** Shorten home directory in path */
function shortenPath(path: string): string {
const home = process.env.HOME
const home = process.env.HOME;
if (home && path.startsWith(home)) {
return '~' + path.slice(home.length)
return "~" + path.slice(home.length);
}
return path
return path;
}
/** Format cost in USD */
function formatCost(costUsd: number): string {
if (costUsd === 0) return '$0.00'
if (costUsd < 0.01) return `$${costUsd.toFixed(4)}`
return `$${costUsd.toFixed(2)}`
if (costUsd === 0) return "$0.00";
if (costUsd < 0.01) return `$${costUsd.toFixed(4)}`;
return `$${costUsd.toFixed(2)}`;
}
function normalizeModelId(value: string | null | undefined): string | null {
if (typeof value !== 'string') return null
const normalized = value.trim()
if (!normalized || normalized.toLowerCase() === 'default') return null
return normalized
if (typeof value !== "string") return null;
const normalized = value.trim();
if (!normalized || normalized.toLowerCase() === "default") return null;
return normalized;
}
function resolveModel(snapshot: AgentSnapshotPayload): string | null {
return normalizeModelId(snapshot.runtimeInfo?.model) ?? normalizeModelId(snapshot.model)
return normalizeModelId(snapshot.runtimeInfo?.model) ?? normalizeModelId(snapshot.model);
}
/** Convert agent snapshot to inspection data */
@@ -107,7 +107,7 @@ function toInspectData(snapshot: AgentSnapshotPayload): AgentInspect {
CachedTokens: snapshot.lastUsage.cachedInputTokens ?? 0,
CostUsd: snapshot.lastUsage.totalCostUsd ?? 0,
}
: null
: null;
const capabilities = snapshot.capabilities
? {
@@ -116,22 +116,22 @@ function toInspectData(snapshot: AgentSnapshotPayload): AgentInspect {
DynamicModes: snapshot.capabilities.supportsDynamicModes ?? false,
McpServers: snapshot.capabilities.supportsMcpServers ?? false,
}
: null
: null;
// Extract worktree and parentAgentId from labels if they exist
const worktree = snapshot.labels?.['paseo.worktree'] ?? null
const parentAgentId = snapshot.labels?.['paseo.parent-agent-id'] ?? null
const worktree = snapshot.labels?.["paseo.worktree"] ?? null;
const parentAgentId = snapshot.labels?.["paseo.parent-agent-id"] ?? null;
return {
Id: snapshot.id,
Name: snapshot.title ?? '-',
Name: snapshot.title ?? "-",
Provider: snapshot.provider,
Model: resolveModel(snapshot) ?? '-',
Thinking: snapshot.effectiveThinkingOptionId ?? 'auto',
Model: resolveModel(snapshot) ?? "-",
Thinking: snapshot.effectiveThinkingOptionId ?? "auto",
Status: snapshot.status,
Archived: snapshot.archivedAt != null,
ArchivedAt: snapshot.archivedAt ?? null,
Mode: snapshot.currentModeId ?? 'default',
Mode: snapshot.currentModeId ?? "default",
Cwd: snapshot.cwd,
CreatedAt: snapshot.createdAt,
UpdatedAt: snapshot.updatedAt,
@@ -142,133 +142,134 @@ function toInspectData(snapshot: AgentSnapshotPayload): AgentInspect {
: null,
PendingPermissions: (snapshot.pendingPermissions ?? []).map((p) => ({
id: p.id,
tool: p.name ?? 'unknown',
tool: p.name ?? "unknown",
})),
Worktree: worktree,
ParentAgentId: parentAgentId,
}
};
}
/** Convert agent to key-value rows for table display */
function toInspectRows(agent: AgentInspect): InspectRow[] {
const rows: InspectRow[] = [
{ key: 'Id', value: agent.Id },
{ key: 'Name', value: agent.Name },
{ key: 'Provider', value: agent.Provider },
{ key: 'Model', value: agent.Model },
{ key: 'Thinking', value: agent.Thinking },
{ key: 'Status', value: agent.Status },
{ key: 'Archived', value: String(agent.Archived) },
{ key: 'ArchivedAt', value: agent.ArchivedAt ?? 'null' },
{ key: 'Mode', value: agent.Mode },
{ key: 'Cwd', value: shortenPath(agent.Cwd) },
{ key: 'CreatedAt', value: agent.CreatedAt },
{ key: 'UpdatedAt', value: agent.UpdatedAt },
]
{ key: "Id", value: agent.Id },
{ key: "Name", value: agent.Name },
{ key: "Provider", value: agent.Provider },
{ key: "Model", value: agent.Model },
{ key: "Thinking", value: agent.Thinking },
{ key: "Status", value: agent.Status },
{ key: "Archived", value: String(agent.Archived) },
{ key: "ArchivedAt", value: agent.ArchivedAt ?? "null" },
{ key: "Mode", value: agent.Mode },
{ key: "Cwd", value: shortenPath(agent.Cwd) },
{ key: "CreatedAt", value: agent.CreatedAt },
{ key: "UpdatedAt", value: agent.UpdatedAt },
];
if (agent.LastUsage) {
rows.push({
key: 'LastUsage',
key: "LastUsage",
value: `InputTokens: ${agent.LastUsage.InputTokens}, OutputTokens: ${agent.LastUsage.OutputTokens}, CachedTokens: ${agent.LastUsage.CachedTokens}, CostUsd: ${formatCost(agent.LastUsage.CostUsd)}`,
})
});
}
if (agent.Capabilities) {
rows.push({
key: 'Capabilities',
key: "Capabilities",
value: `Streaming: ${agent.Capabilities.Streaming}, Persistence: ${agent.Capabilities.Persistence}, DynamicModes: ${agent.Capabilities.DynamicModes}, McpServers: ${agent.Capabilities.McpServers}`,
})
});
}
if (agent.AvailableModes && agent.AvailableModes.length > 0) {
rows.push({
key: 'AvailableModes',
value: agent.AvailableModes.map((m) => `${m.id} (${m.label})`).join(', '),
})
key: "AvailableModes",
value: agent.AvailableModes.map((m) => `${m.id} (${m.label})`).join(", "),
});
}
rows.push({
key: 'PendingPermissions',
value: agent.PendingPermissions.length > 0
? agent.PendingPermissions.map(p => `${p.id} (${p.tool})`).join(', ')
: '[]'
})
key: "PendingPermissions",
value:
agent.PendingPermissions.length > 0
? agent.PendingPermissions.map((p) => `${p.id} (${p.tool})`).join(", ")
: "[]",
});
rows.push({ key: 'Worktree', value: agent.Worktree ?? 'null' })
rows.push({ key: 'ParentAgentId', value: agent.ParentAgentId ?? 'null' })
rows.push({ key: "Worktree", value: agent.Worktree ?? "null" });
rows.push({ key: "ParentAgentId", value: agent.ParentAgentId ?? "null" });
return rows
return rows;
}
export type AgentInspectResult = ListResult<InspectRow>
export type AgentInspectResult = ListResult<InspectRow>;
export interface AgentInspectOptions extends CommandOptions {
host?: string
host?: string;
}
export async function runInspectCommand(
agentIdArg: string,
options: AgentInspectOptions,
_command: Command
_command: Command,
): Promise<AgentInspectResult> {
const host = getDaemonHost({ host: options.host as string | undefined })
const host = getDaemonHost({ host: options.host as string | undefined });
// Validate arguments
if (!agentIdArg || agentIdArg.trim().length === 0) {
const error: CommandError = {
code: 'MISSING_AGENT_ID',
message: 'Agent ID is required',
details: 'Usage: paseo agent inspect <id>',
}
throw error
code: "MISSING_AGENT_ID",
message: "Agent ID is required",
details: "Usage: paseo agent inspect <id>",
};
throw error;
}
let client
let client;
try {
client = await connectToDaemon({ host: options.host as string | undefined })
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'DAEMON_NOT_RUNNING',
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: 'Start the daemon with: paseo daemon start',
}
throw error
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
try {
const fetchResult = await client.fetchAgent(agentIdArg)
const fetchResult = await client.fetchAgent(agentIdArg);
if (!fetchResult) {
const error: CommandError = {
code: 'AGENT_NOT_FOUND',
code: "AGENT_NOT_FOUND",
message: `Agent not found: ${agentIdArg}`,
details: 'Use "paseo ls" to list available agents',
}
throw error
};
throw error;
}
await client.close()
await client.close();
const inspectData = toInspectData(fetchResult.agent)
const inspectData = toInspectData(fetchResult.agent);
return {
type: 'list',
type: "list",
data: toInspectRows(inspectData),
schema: createInspectSchema(inspectData),
}
};
} catch (err) {
await client.close().catch(() => {})
await client.close().catch(() => {});
// Re-throw CommandError as-is
if (err && typeof err === 'object' && 'code' in err) {
throw err
if (err && typeof err === "object" && "code" in err) {
throw err;
}
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'INSPECT_FAILED',
code: "INSPECT_FAILED",
message: `Failed to inspect agent: ${message}`,
}
throw error
};
throw error;
}
}
+99 -102
View File
@@ -1,155 +1,151 @@
import type { Command } from 'commander'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions } from '../../output/index.js'
import { fetchProjectedTimelineItems } from '../../utils/timeline.js'
import type {
DaemonClient,
AgentStreamMessage,
AgentTimelineItem,
} from '@getpaseo/server'
import { curateAgentActivity } from '@getpaseo/server'
import type { Command } from "commander";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type { CommandOptions } from "../../output/index.js";
import { fetchProjectedTimelineItems } from "../../utils/timeline.js";
import type { DaemonClient, AgentStreamMessage, AgentTimelineItem } from "@getpaseo/server";
import { curateAgentActivity } from "@getpaseo/server";
export interface AgentLogsOptions extends CommandOptions {
follow?: boolean
tail?: string
filter?: string
since?: string
follow?: boolean;
tail?: string;
filter?: string;
since?: string;
}
// Logs command returns void - it outputs directly to console
export type AgentLogsResult = void
export type AgentLogsResult = void;
export const NO_ACTIVITY_MESSAGE = 'No activity to display.'
export const NO_ACTIVITY_MESSAGE = "No activity to display.";
export async function fetchAgentTimelineItems(
client: DaemonClient,
agentId: string
agentId: string,
): Promise<AgentTimelineItem[]> {
return fetchProjectedTimelineItems({ client, agentId })
return fetchProjectedTimelineItems({ client, agentId });
}
export function formatAgentActivityTranscript(
timelineItems: AgentTimelineItem[],
tailCount?: number
tailCount?: number,
): string {
if (tailCount === 0) {
return ''
return "";
}
return curateAgentActivity(
timelineItems,
tailCount !== undefined ? { maxItems: tailCount } : undefined
)
tailCount !== undefined ? { maxItems: tailCount } : undefined,
);
}
function parseTailCount(raw: string | undefined): number | undefined {
if (raw === undefined) return undefined
const parsed = Number.parseInt(raw, 10)
if (raw === undefined) return undefined;
const parsed = Number.parseInt(raw, 10);
if (Number.isNaN(parsed) || parsed < 0) {
return undefined
return undefined;
}
return parsed
return parsed;
}
/**
* Check if a timeline item matches the filter type
*/
function matchesFilter(item: AgentTimelineItem, filter?: string): boolean {
if (!filter) return true
if (!filter) return true;
const filterLower = filter.toLowerCase()
const type = item.type.toLowerCase()
const filterLower = filter.toLowerCase();
const type = item.type.toLowerCase();
switch (filterLower) {
case 'tools':
return type === 'tool_call'
case 'text':
return type === 'user_message' || type === 'assistant_message' || type === 'reasoning'
case 'errors':
return type === 'error'
case 'permissions':
case "tools":
return type === "tool_call";
case "text":
return type === "user_message" || type === "assistant_message" || type === "reasoning";
case "errors":
return type === "error";
case "permissions":
// Permissions might be in tool_call status or a separate event type
return type.includes('permission')
return type.includes("permission");
default:
// If filter doesn't match predefined types, match against the actual type
return type.includes(filterLower)
return type.includes(filterLower);
}
}
export async function runLogsCommand(
id: string,
options: AgentLogsOptions,
_command: Command
_command: Command,
): Promise<AgentLogsResult> {
const host = getDaemonHost({ host: options.host as string | undefined })
const host = getDaemonHost({ host: options.host as string | undefined });
if (!id) {
console.error('Error: Agent ID required')
console.error('Usage: paseo agent logs <id>')
process.exit(1)
console.error("Error: Agent ID required");
console.error("Usage: paseo agent logs <id>");
process.exit(1);
}
let client: DaemonClient
let client: DaemonClient;
try {
client = await connectToDaemon({ host: options.host as string | undefined })
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
console.error(`Error: Cannot connect to daemon at ${host}: ${message}`)
console.error('Start the daemon with: paseo daemon start')
process.exit(1)
const message = err instanceof Error ? err.message : String(err);
console.error(`Error: Cannot connect to daemon at ${host}: ${message}`);
console.error("Start the daemon with: paseo daemon start");
process.exit(1);
}
try {
const fetchResult = await client.fetchAgent(id)
const fetchResult = await client.fetchAgent(id);
if (!fetchResult) {
console.error(`Error: No agent found matching: ${id}`)
console.error('Use `paseo ls` to list available agents')
await client.close()
process.exit(1)
console.error(`Error: No agent found matching: ${id}`);
console.error("Use `paseo ls` to list available agents");
await client.close();
process.exit(1);
}
const resolvedId = fetchResult.agent.id
const resolvedId = fetchResult.agent.id;
// 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)
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
await runFollowMode(client, resolvedId, options);
return;
}
// Fetch timeline directly via cursor RPC.
let timelineItems = await fetchAgentTimelineItems(client, resolvedId)
let timelineItems = await fetchAgentTimelineItems(client, resolvedId);
// Apply filter
if (options.filter) {
timelineItems = timelineItems.filter((item) => matchesFilter(item, options.filter))
timelineItems = timelineItems.filter((item) => matchesFilter(item, options.filter));
}
const tailCount = parseTailCount(options.tail)
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)
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()
await client.close();
// Use curateAgentActivity to format the transcript
if (tailCount === 0) {
return
return;
}
const transcript = formatAgentActivityTranscript(timelineItems, tailCount)
console.log(transcript)
const transcript = formatAgentActivityTranscript(timelineItems, tailCount);
console.log(transcript);
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
console.error(`Error: Failed to get logs: ${message}`)
await client.close().catch(() => {})
process.exit(1)
const message = err instanceof Error ? err.message : String(err);
console.error(`Error: Failed to get logs: ${message}`);
await client.close().catch(() => {});
process.exit(1);
}
}
@@ -159,60 +155,61 @@ export async function runLogsCommand(
async function runFollowMode(
client: DaemonClient,
agentId: string,
options: AgentLogsOptions
options: AgentLogsOptions,
): Promise<void> {
const DEFAULT_FOLLOW_TAIL = 10
const tailCount = parseTailCount(options.tail) ?? DEFAULT_FOLLOW_TAIL
const DEFAULT_FOLLOW_TAIL = 10;
const tailCount = parseTailCount(options.tail) ?? DEFAULT_FOLLOW_TAIL;
// First, get existing timeline.
let existingItems = await fetchAgentTimelineItems(client, agentId)
let existingItems = await fetchAgentTimelineItems(client, agentId);
// Apply filter to existing items
if (options.filter) {
existingItems = existingItems.filter((item) => matchesFilter(item, options.filter))
existingItems = existingItems.filter((item) => matchesFilter(item, options.filter));
}
// Print existing transcript (tail-like behavior)
if (tailCount > 0) {
const existingTranscript = formatAgentActivityTranscript(existingItems, tailCount)
const existingTranscript = formatAgentActivityTranscript(existingItems, tailCount);
if (existingTranscript !== NO_ACTIVITY_MESSAGE) {
console.log(existingTranscript)
console.log(existingTranscript);
}
}
// Subscribe to new events
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 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
if (message.type !== 'agent_stream') return
if (message.payload.agentId !== agentId) return
const unsubscribe = client.on("agent_stream", (msg: unknown) => {
const message = msg as AgentStreamMessage;
if (message.type !== "agent_stream") return;
if (message.payload.agentId !== agentId) return;
if (message.payload.event.type === 'timeline') {
const item = message.payload.event.item
if (message.payload.event.type === "timeline") {
const item = message.payload.event.item;
// Apply filter
if (options.filter && !matchesFilter(item, options.filter)) {
return
return;
}
// Print each timeline item as it arrives using the curator format
const transcript = formatAgentActivityTranscript([item])
const transcript = formatAgentActivityTranscript([item]);
if (transcript !== NO_ACTIVITY_MESSAGE) {
console.log(transcript)
console.log(transcript);
}
}
})
});
// Wait for interrupt
await new Promise<void>((resolve) => {
const cleanup = () => {
unsubscribe()
resolve()
}
unsubscribe();
resolve();
};
process.on('SIGINT', cleanup)
process.on('SIGTERM', cleanup)
})
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
});
await client.close()
await client.close();
}
+106 -106
View File
@@ -1,102 +1,102 @@
import type { Command } from 'commander'
import type { AgentSnapshotPayload } from '@getpaseo/server'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions, ListResult, OutputSchema, CommandError } from '../../output/index.js'
import type { Command } from "commander";
import type { AgentSnapshotPayload } from "@getpaseo/server";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type { CommandOptions, ListResult, OutputSchema, CommandError } from "../../output/index.js";
/** Agent list item for display */
export interface AgentListItem {
id: string
shortId: string
name: string
provider: string
thinking: string
status: string
cwd: string
created: string
id: string;
shortId: string;
name: string;
provider: string;
thinking: string;
status: string;
cwd: string;
created: string;
}
/** Helper to get relative time string */
function relativeTime(date: Date | string): string {
const now = Date.now()
const then = new Date(date).getTime()
const seconds = Math.floor((now - then) / 1000)
const now = Date.now();
const then = new Date(date).getTime();
const seconds = Math.floor((now - then) / 1000);
if (seconds < 60) return 'just now'
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`
return `${Math.floor(seconds / 86400)} days ago`
if (seconds < 60) return "just now";
if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`;
return `${Math.floor(seconds / 86400)} days ago`;
}
/** Shorten home directory in path */
function shortenPath(path: string): string {
const home = process.env.HOME
const home = process.env.HOME;
if (home && path.startsWith(home)) {
return '~' + path.slice(home.length)
return "~" + path.slice(home.length);
}
return path
return path;
}
function normalizeModelId(modelId: string | null | undefined): string | null {
if (typeof modelId !== 'string') return null
const normalized = modelId.trim()
if (!normalized || normalized.toLowerCase() === 'default') return null
return normalized
if (typeof modelId !== "string") return null;
const normalized = modelId.trim();
if (!normalized || normalized.toLowerCase() === "default") return null;
return normalized;
}
/** Schema for agent ls output */
export const agentLsSchema: OutputSchema<AgentListItem> = {
idField: 'shortId',
idField: "shortId",
columns: [
{ header: 'AGENT ID', field: 'shortId', width: 12 },
{ header: 'NAME', field: 'name', width: 20 },
{ header: 'PROVIDER', field: 'provider', width: 15 },
{ header: 'THINKING', field: 'thinking', width: 12 },
{ header: "AGENT ID", field: "shortId", width: 12 },
{ header: "NAME", field: "name", width: 20 },
{ header: "PROVIDER", field: "provider", width: 15 },
{ header: "THINKING", field: "thinking", width: 12 },
{
header: 'STATUS',
field: 'status',
header: "STATUS",
field: "status",
width: 10,
color: (value) => {
if (value === 'running') return 'green'
if (value === 'idle') return 'yellow'
if (value === 'error') return 'red'
return undefined
if (value === "running") return "green";
if (value === "idle") return "yellow";
if (value === "error") return "red";
return undefined;
},
},
{ header: 'CWD', field: 'cwd', width: 30 },
{ header: 'CREATED', field: 'created', width: 15 },
{ header: "CWD", field: "cwd", width: 30 },
{ header: "CREATED", field: "created", width: 15 },
],
}
};
/** Transform agent snapshot to AgentListItem */
function toListItem(agent: AgentSnapshotPayload): AgentListItem {
const model = normalizeModelId(agent.runtimeInfo?.model) ?? normalizeModelId(agent.model)
const model = normalizeModelId(agent.runtimeInfo?.model) ?? normalizeModelId(agent.model);
return {
id: agent.id,
shortId: agent.id.slice(0, 7),
name: agent.title ?? '-',
name: agent.title ?? "-",
provider: model ? `${agent.provider}/${model}` : agent.provider,
thinking: agent.effectiveThinkingOptionId ?? 'auto',
thinking: agent.effectiveThinkingOptionId ?? "auto",
status: agent.status,
cwd: shortenPath(agent.cwd),
created: relativeTime(agent.createdAt),
}
};
}
export type AgentLsResult = ListResult<AgentListItem>
export type AgentLsResult = ListResult<AgentListItem>;
export interface AgentLsOptions extends CommandOptions {
/** -a: Include archived agents */
all?: boolean
all?: boolean;
/** Legacy flag retained for CLI compatibility */
global?: boolean
global?: boolean;
/** Filter by specific status */
status?: string
status?: string;
/** Filter by specific cwd */
cwd?: string
cwd?: string;
/** Filter by labels (key=value format) */
label?: string[]
label?: string[];
/** Filter by thinking option ID */
thinking?: string
thinking?: string;
}
/**
@@ -106,128 +106,128 @@ export interface AgentLsOptions extends CommandOptions {
*/
export async function runLsCommand(
options: AgentLsOptions,
_command: Command
_command: Command,
): Promise<AgentLsResult> {
const host = getDaemonHost({ host: options.host as string | undefined })
const host = getDaemonHost({ host: options.host as string | undefined });
let client
let client;
try {
client = await connectToDaemon({ host: options.host as string | undefined })
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'DAEMON_NOT_RUNNING',
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: 'Start the daemon with: paseo daemon start',
}
throw error
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
try {
const normalizedThinkingOptionId = options.thinking?.trim()
const normalizedThinkingOptionId = options.thinking?.trim();
if (options.thinking !== undefined && !normalizedThinkingOptionId) {
const error: CommandError = {
code: 'INVALID_THINKING_OPTION',
message: '--thinking cannot be empty',
}
throw error
code: "INVALID_THINKING_OPTION",
message: "--thinking cannot be empty",
};
throw error;
}
// Parse --label filters (key=value).
const labelFilters: Record<string, string> = {}
const labelFilters: Record<string, string> = {};
if (options.label) {
for (const labelStr of options.label) {
const eqIndex = labelStr.indexOf('=')
const eqIndex = labelStr.indexOf("=");
if (eqIndex !== -1) {
const key = labelStr.slice(0, eqIndex)
const value = labelStr.slice(eqIndex + 1)
labelFilters[key] = value
const key = labelStr.slice(0, eqIndex);
const value = labelStr.slice(eqIndex + 1);
labelFilters[key] = value;
}
}
}
const daemonFilter: {
includeArchived?: boolean
labels?: Record<string, string>
thinkingOptionId?: string
} = {}
includeArchived?: boolean;
labels?: Record<string, string>;
thinkingOptionId?: string;
} = {};
if (options.all) {
daemonFilter.includeArchived = true
daemonFilter.includeArchived = true;
}
if (Object.keys(labelFilters).length > 0) {
daemonFilter.labels = labelFilters
daemonFilter.labels = labelFilters;
}
if (normalizedThinkingOptionId) {
daemonFilter.thinkingOptionId = normalizedThinkingOptionId
daemonFilter.thinkingOptionId = normalizedThinkingOptionId;
}
const fetchPayload = await client.fetchAgents({
filter: Object.keys(daemonFilter).length > 0 ? daemonFilter : undefined,
})
let agents = fetchPayload.entries.map((entry) => entry.agent)
});
let agents = fetchPayload.entries.map((entry) => entry.agent);
// By default, exclude archived agents. `-a` includes them.
if (!options.all) {
agents = agents.filter((a) => !a.archivedAt)
agents = agents.filter((a) => !a.archivedAt);
}
// If explicit status filter is provided, apply it.
if (options.status) {
agents = agents.filter((a) => a.status === options.status)
agents = agents.filter((a) => a.status === options.status);
}
// Optional cwd filter.
if (options.cwd) {
const targetCwd = options.cwd.replace(/\/$/, '')
const targetCwd = options.cwd.replace(/\/$/, "");
agents = agents.filter((a) => {
const agentCwd = a.cwd.replace(/\/$/, '')
return agentCwd === targetCwd || agentCwd.startsWith(targetCwd + '/')
})
const agentCwd = a.cwd.replace(/\/$/, "");
return agentCwd === targetCwd || agentCwd.startsWith(targetCwd + "/");
});
}
// Apply label filtering only when explicitly requested.
if (Object.keys(labelFilters).length > 0) {
agents = agents.filter((a) => {
const agentLabels = a.labels
const agentLabels = a.labels;
for (const [key, value] of Object.entries(labelFilters)) {
if (agentLabels[key] !== value) {
return false
return false;
}
}
return true
})
return true;
});
}
await client.close()
await client.close();
// Sort agents: running first, then idle, then others; within each group, most recent first
const statusOrder = { running: 0, idle: 1 } as Record<string, number>
const statusOrder = { running: 0, idle: 1 } as Record<string, number>;
agents.sort((a, b) => {
// Primary sort: by status
const aOrder = statusOrder[a.status] ?? 999
const bOrder = statusOrder[b.status] ?? 999
if (aOrder !== bOrder) return aOrder - bOrder
const aOrder = statusOrder[a.status] ?? 999;
const bOrder = statusOrder[b.status] ?? 999;
if (aOrder !== bOrder) return aOrder - bOrder;
// Secondary sort: by creation time (most recent first)
const aTime = new Date(a.createdAt).getTime()
const bTime = new Date(b.createdAt).getTime()
return bTime - aTime
})
const aTime = new Date(a.createdAt).getTime();
const bTime = new Date(b.createdAt).getTime();
return bTime - aTime;
});
const items = agents.map(toListItem)
const items = agents.map(toListItem);
return {
type: 'list',
type: "list",
data: items,
schema: agentLsSchema,
}
};
} catch (err) {
await client.close().catch(() => {})
const message = err instanceof Error ? err.message : String(err)
await client.close().catch(() => {});
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'LIST_AGENTS_FAILED',
code: "LIST_AGENTS_FAILED",
message: `Failed to list agents: ${message}`,
}
throw error
};
throw error;
}
}
+55 -55
View File
@@ -1,139 +1,139 @@
import type { Command } from 'commander'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { Command } from "commander";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type {
CommandOptions,
OutputSchema,
CommandError,
AnyCommandResult,
} from '../../output/index.js'
import type { AgentMode } from '@getpaseo/server'
} from "../../output/index.js";
import type { AgentMode } from "@getpaseo/server";
/** Result for setting mode */
export interface SetModeResult {
agentId: string
mode: string
agentId: string;
mode: string;
}
/** Schema for mode list output */
export const modeListSchema: OutputSchema<AgentMode> = {
idField: 'id',
idField: "id",
columns: [
{ header: 'MODE', field: 'id', width: 15 },
{ header: 'LABEL', field: 'label', width: 25 },
{ header: 'DESCRIPTION', field: 'description', width: 40 },
{ header: "MODE", field: "id", width: 15 },
{ header: "LABEL", field: "label", width: 25 },
{ header: "DESCRIPTION", field: "description", width: 40 },
],
}
};
/** Schema for set mode output */
export const setModeSchema: OutputSchema<SetModeResult> = {
idField: 'agentId',
idField: "agentId",
columns: [
{ header: 'AGENT ID', field: 'agentId', width: 12 },
{ header: 'MODE', field: 'mode', width: 20 },
{ header: "AGENT ID", field: "agentId", width: 12 },
{ header: "MODE", field: "mode", width: 20 },
],
}
};
export interface AgentModeOptions extends CommandOptions {
list?: boolean
list?: boolean;
}
const missingModeError = (): CommandError => ({
code: 'MISSING_ARGUMENT',
message: 'Mode argument required unless --list is specified',
details: 'Usage: paseo agent mode <id> <mode> | paseo agent mode --list <id>',
})
code: "MISSING_ARGUMENT",
message: "Mode argument required unless --list is specified",
details: "Usage: paseo agent mode <id> <mode> | paseo agent mode --list <id>",
});
// This command returns two different data shapes (set result vs mode list).
// Keep `any` here to match the existing output wrapper generic contract.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AgentModeResult = AnyCommandResult<any>
export type AgentModeResult = AnyCommandResult<any>;
export async function runModeCommand(
id: string,
mode: string | undefined,
options: AgentModeOptions,
_command: Command
_command: Command,
): Promise<AgentModeResult> {
const normalizedMode = mode?.trim()
const host = getDaemonHost({ host: options.host as string | undefined })
const normalizedMode = mode?.trim();
const host = getDaemonHost({ host: options.host as string | undefined });
// Validate arguments
if (!options.list && !normalizedMode) {
throw missingModeError()
throw missingModeError();
}
let client: Awaited<ReturnType<typeof connectToDaemon>> | undefined
let client: Awaited<ReturnType<typeof connectToDaemon>> | undefined;
try {
client = await connectToDaemon({ host: options.host as string | undefined })
const fetchResult = await client.fetchAgent(id)
client = await connectToDaemon({ host: options.host as string | undefined });
const fetchResult = await client.fetchAgent(id);
if (!fetchResult) {
const error: CommandError = {
code: 'AGENT_NOT_FOUND',
code: "AGENT_NOT_FOUND",
message: `No agent found matching: ${id}`,
details: 'Use `paseo ls` to list available agents',
}
throw error
details: "Use `paseo ls` to list available agents",
};
throw error;
}
const agent = fetchResult.agent
const resolvedId = agent.id
const agent = fetchResult.agent;
const resolvedId = agent.id;
if (options.list) {
// List available modes for this agent
const availableModes = agent.availableModes ?? []
const availableModes = agent.availableModes ?? [];
const items: AgentMode[] = availableModes.map((m) => ({
id: m.id,
label: m.label,
description: m.description,
}))
}));
return {
type: 'list',
type: "list",
data: items,
schema: modeListSchema,
}
};
}
if (!normalizedMode) {
throw missingModeError()
throw missingModeError();
}
// Set the agent mode
await client.setAgentMode(resolvedId, normalizedMode)
await client.setAgentMode(resolvedId, normalizedMode);
return {
type: 'single',
type: "single",
data: {
agentId: resolvedId.slice(0, 7),
mode: normalizedMode,
},
schema: setModeSchema,
}
};
} catch (err) {
// Re-throw if it's already a CommandError
if (err && typeof err === 'object' && 'code' in err) {
throw err
if (err && typeof err === "object" && "code" in err) {
throw err;
}
if (!client) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'DAEMON_NOT_RUNNING',
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: 'Start the daemon with: paseo daemon start',
}
throw error
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'MODE_OPERATION_FAILED',
message: `Failed to ${options.list ? 'list modes' : 'set mode'}: ${message}`,
}
throw error
code: "MODE_OPERATION_FAILED",
message: `Failed to ${options.list ? "list modes" : "set mode"}: ${message}`,
};
throw error;
} finally {
if (client) {
await client.close().catch(() => {})
await client.close().catch(() => {});
}
}
}
+235 -229
View File
@@ -1,331 +1,338 @@
import type { Command } from 'commander'
import type { Command } from "commander";
import {
getStructuredAgentResponse,
StructuredAgentResponseError,
type AgentSnapshotPayload,
} from '@getpaseo/server'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { lookup } from 'mime-types'
import { parseDuration } from '../../utils/duration.js'
} from "@getpaseo/server";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type {
CommandOptions,
SingleResult,
OutputSchema,
CommandError,
} from "../../output/index.js";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { lookup } from "mime-types";
import { parseDuration } from "../../utils/duration.js";
/** Result type for agent run command */
export interface AgentRunResult {
agentId: string
status: 'created' | 'running' | 'completed' | 'timeout' | 'permission' | 'error'
provider: string
cwd: string
title: string | null
agentId: string;
status: "created" | "running" | "completed" | "timeout" | "permission" | "error";
provider: string;
cwd: string;
title: string | null;
}
/** Schema for agent run output */
export const agentRunSchema: OutputSchema<AgentRunResult> = {
idField: 'agentId',
idField: "agentId",
columns: [
{ header: 'AGENT ID', field: 'agentId', width: 12 },
{ header: 'STATUS', field: 'status', width: 10 },
{ header: 'PROVIDER', field: 'provider', width: 10 },
{ header: 'CWD', field: 'cwd', width: 30 },
{ header: 'TITLE', field: 'title', width: 20 },
{ header: "AGENT ID", field: "agentId", width: 12 },
{ header: "STATUS", field: "status", width: 10 },
{ header: "PROVIDER", field: "provider", width: 10 },
{ header: "CWD", field: "cwd", width: 30 },
{ header: "TITLE", field: "title", width: 20 },
],
}
};
export interface AgentRunOptions extends CommandOptions {
detach?: boolean
name?: string
provider?: string
model?: string
thinking?: string
mode?: string
worktree?: string
base?: string
image?: string[]
cwd?: string
label?: string[]
waitTimeout?: string
outputSchema?: string
detach?: boolean;
name?: string;
provider?: string;
model?: string;
thinking?: string;
mode?: string;
worktree?: string;
base?: string;
image?: string[];
cwd?: string;
label?: string[];
waitTimeout?: string;
outputSchema?: string;
}
interface ResolvedProviderModel {
provider: string
model: string | undefined
provider: string;
model: string | undefined;
}
function toRunResult(
agent: AgentSnapshotPayload,
statusOverride?: AgentRunResult['status']
statusOverride?: AgentRunResult["status"],
): AgentRunResult {
return {
agentId: agent.id,
status: statusOverride ?? (agent.status === 'running' ? 'running' : 'created'),
status: statusOverride ?? (agent.status === "running" ? "running" : "created"),
provider: agent.provider,
cwd: agent.cwd,
title: agent.title,
}
};
}
function loadOutputSchema(value: string): Record<string, unknown> {
const trimmed = value.trim()
const trimmed = value.trim();
if (!trimmed) {
const error: CommandError = {
code: 'INVALID_OUTPUT_SCHEMA',
message: '--output-schema cannot be empty',
details: 'Provide a JSON schema file path or inline JSON object',
}
throw error
code: "INVALID_OUTPUT_SCHEMA",
message: "--output-schema cannot be empty",
details: "Provide a JSON schema file path or inline JSON object",
};
throw error;
}
let source = trimmed
if (!trimmed.startsWith('{')) {
let source = trimmed;
if (!trimmed.startsWith("{")) {
try {
source = readFileSync(resolve(trimmed), 'utf8')
source = readFileSync(resolve(trimmed), "utf8");
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'INVALID_OUTPUT_SCHEMA',
code: "INVALID_OUTPUT_SCHEMA",
message: `Failed to read output schema file: ${trimmed}`,
details: message,
}
throw error
};
throw error;
}
}
let parsed: unknown
let parsed: unknown;
try {
parsed = JSON.parse(source)
parsed = JSON.parse(source);
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'INVALID_OUTPUT_SCHEMA',
message: 'Failed to parse output schema JSON',
code: "INVALID_OUTPUT_SCHEMA",
message: "Failed to parse output schema JSON",
details: message,
}
throw error
};
throw error;
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
const error: CommandError = {
code: 'INVALID_OUTPUT_SCHEMA',
message: 'Output schema must be a JSON object',
}
throw error
code: "INVALID_OUTPUT_SCHEMA",
message: "Output schema must be a JSON object",
};
throw error;
}
return parsed as Record<string, unknown>
return parsed as Record<string, unknown>;
}
class StructuredRunStatusError extends Error {
readonly kind: 'timeout' | 'permission' | 'error' | 'empty'
readonly kind: "timeout" | "permission" | "error" | "empty";
constructor(kind: 'timeout' | 'permission' | 'error' | 'empty', message: string) {
super(message)
this.name = 'StructuredRunStatusError'
this.kind = kind
constructor(kind: "timeout" | "permission" | "error" | "empty", message: string) {
super(message);
this.name = "StructuredRunStatusError";
this.kind = kind;
}
}
type ConnectedDaemonClient = Awaited<ReturnType<typeof connectToDaemon>>
type ConnectedDaemonClient = Awaited<ReturnType<typeof connectToDaemon>>;
export interface StructuredResponseTimelineClient {
fetchAgentTimeline: ConnectedDaemonClient['fetchAgentTimeline']
fetchAgentTimeline: ConnectedDaemonClient["fetchAgentTimeline"];
}
export async function resolveStructuredResponseMessage(options: {
client: StructuredResponseTimelineClient
agentId: string
lastMessage: string | null
client: StructuredResponseTimelineClient;
agentId: string;
lastMessage: string | null;
}): Promise<string | null> {
const direct = options.lastMessage?.trim()
const direct = options.lastMessage?.trim();
if (direct) {
return direct
return direct;
}
try {
const timeline = await options.client.fetchAgentTimeline(options.agentId, {
direction: 'tail',
projection: 'projected',
direction: "tail",
projection: "projected",
limit: 200,
})
});
for (let index = timeline.entries.length - 1; index >= 0; index -= 1) {
const entry = timeline.entries[index]
if (!entry || entry.item.type !== 'assistant_message') {
continue
const entry = timeline.entries[index];
if (!entry || entry.item.type !== "assistant_message") {
continue;
}
const text = entry.item.text.trim()
const text = entry.item.text.trim();
if (text.length > 0) {
return text
return text;
}
}
} catch {
// Leave empty; caller will surface a consistent structured-output failure message.
}
return null
return null;
}
function structuredRunSchema(output: Record<string, unknown>): OutputSchema<AgentRunResult> {
return {
...agentRunSchema,
serialize: () => output,
}
};
}
export function resolveProviderAndModel(options: Pick<AgentRunOptions, 'provider' | 'model'>): ResolvedProviderModel {
const providerInput = options.provider?.trim() || 'claude'
const modelInput = options.model?.trim()
export function resolveProviderAndModel(
options: Pick<AgentRunOptions, "provider" | "model">,
): ResolvedProviderModel {
const providerInput = options.provider?.trim() || "claude";
const modelInput = options.model?.trim();
if (options.model !== undefined && !modelInput) {
const error: CommandError = {
code: 'INVALID_MODEL',
message: '--model cannot be empty',
}
throw error
code: "INVALID_MODEL",
message: "--model cannot be empty",
};
throw error;
}
const slashIndex = providerInput.indexOf('/')
const slashIndex = providerInput.indexOf("/");
if (slashIndex === -1) {
return {
provider: providerInput,
model: modelInput,
}
};
}
const provider = providerInput.slice(0, slashIndex).trim()
const modelFromProvider = providerInput.slice(slashIndex + 1).trim()
const provider = providerInput.slice(0, slashIndex).trim();
const modelFromProvider = providerInput.slice(slashIndex + 1).trim();
if (!provider || !modelFromProvider) {
const error: CommandError = {
code: 'INVALID_PROVIDER',
message: 'Invalid --provider value',
details: 'Use --provider <provider> or --provider <provider>/<model>',
}
throw error
code: "INVALID_PROVIDER",
message: "Invalid --provider value",
details: "Use --provider <provider> or --provider <provider>/<model>",
};
throw error;
}
if (modelInput && modelInput !== modelFromProvider) {
const error: CommandError = {
code: 'CONFLICTING_MODEL_OPTIONS',
message: 'Conflicting model values provided',
code: "CONFLICTING_MODEL_OPTIONS",
message: "Conflicting model values provided",
details: `--provider specifies model ${modelFromProvider}, but --model specifies ${modelInput}`,
}
throw error
};
throw error;
}
return {
provider,
model: modelInput ?? modelFromProvider,
}
};
}
export async function runRunCommand(
prompt: string,
options: AgentRunOptions,
_command: Command
_command: Command,
): Promise<SingleResult<AgentRunResult>> {
const host = getDaemonHost({ host: options.host as string | undefined })
const outputSchema = options.outputSchema ? loadOutputSchema(options.outputSchema) : undefined
let waitTimeoutMs = 0
const host = getDaemonHost({ host: options.host as string | undefined });
const outputSchema = options.outputSchema ? loadOutputSchema(options.outputSchema) : undefined;
let waitTimeoutMs = 0;
// Validate prompt is provided
if (!prompt || prompt.trim().length === 0) {
const error: CommandError = {
code: 'MISSING_PROMPT',
message: 'A prompt is required',
details: 'Usage: paseo agent run [options] <prompt>',
}
throw error
code: "MISSING_PROMPT",
message: "A prompt is required",
details: "Usage: paseo agent run [options] <prompt>",
};
throw error;
}
// Validate --base is only used with --worktree
if (options.base && !options.worktree) {
const error: CommandError = {
code: 'INVALID_OPTIONS',
message: '--base can only be used with --worktree',
details: 'Usage: paseo agent run --worktree <name> --base <branch> <prompt>',
}
throw error
code: "INVALID_OPTIONS",
message: "--base can only be used with --worktree",
details: "Usage: paseo agent run --worktree <name> --base <branch> <prompt>",
};
throw error;
}
// --output-schema always runs in attached/wait mode
if (outputSchema && options.detach) {
const error: CommandError = {
code: 'INVALID_OPTIONS',
message: '--output-schema cannot be used with --detach',
details: 'Structured output requires waiting for the agent to finish',
}
throw error
code: "INVALID_OPTIONS",
message: "--output-schema cannot be used with --detach",
details: "Structured output requires waiting for the agent to finish",
};
throw error;
}
if (options.waitTimeout) {
try {
waitTimeoutMs = parseDuration(options.waitTimeout)
waitTimeoutMs = parseDuration(options.waitTimeout);
if (waitTimeoutMs <= 0) {
throw new Error('Timeout must be positive')
throw new Error("Timeout must be positive");
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'INVALID_TIMEOUT',
message: 'Invalid wait timeout value',
code: "INVALID_TIMEOUT",
message: "Invalid wait timeout value",
details: message,
}
throw error
};
throw error;
}
}
const resolvedProviderModel = resolveProviderAndModel(options)
const resolvedProviderModel = resolveProviderAndModel(options);
let client
let client;
try {
client = await connectToDaemon({ host: options.host as string | undefined })
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'DAEMON_NOT_RUNNING',
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: 'Start the daemon with: paseo daemon start',
}
throw error
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
try {
// Resolve working directory
const cwd = options.cwd ?? process.cwd()
const thinkingOptionId = options.thinking?.trim()
const cwd = options.cwd ?? process.cwd();
const thinkingOptionId = options.thinking?.trim();
if (options.thinking !== undefined && !thinkingOptionId) {
const error: CommandError = {
code: 'INVALID_THINKING_OPTION',
message: '--thinking cannot be empty',
code: "INVALID_THINKING_OPTION",
message: "--thinking cannot be empty",
details:
'Provide a thinking option ID. Use "paseo provider models <provider> --thinking" to list valid IDs.',
}
throw error
};
throw error;
}
// Process images if provided
let images: Array<{ data: string; mimeType: string }> | undefined
let images: Array<{ data: string; mimeType: string }> | undefined;
if (options.image && options.image.length > 0) {
images = options.image.map((imagePath) => {
const resolvedPath = resolve(imagePath)
const resolvedPath = resolve(imagePath);
try {
const imageData = readFileSync(resolvedPath)
const mimeType = lookup(resolvedPath) || 'application/octet-stream'
const imageData = readFileSync(resolvedPath);
const mimeType = lookup(resolvedPath) || "application/octet-stream";
// Verify it's an image MIME type
if (!mimeType.startsWith('image/')) {
throw new Error(`File is not an image: ${imagePath} (detected type: ${mimeType})`)
if (!mimeType.startsWith("image/")) {
throw new Error(`File is not an image: ${imagePath} (detected type: ${mimeType})`);
}
return {
data: imageData.toString('base64'),
data: imageData.toString("base64"),
mimeType,
}
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
throw new Error(`Failed to read image ${imagePath}: ${message}`)
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to read image ${imagePath}: ${message}`);
}
})
});
}
// Build git options if worktree is specified
@@ -335,34 +342,34 @@ export async function runRunCommand(
worktreeSlug: options.worktree,
baseBranch: options.base,
}
: undefined
: undefined;
// Build labels from --label flags
const labels: Record<string, string> = {}
const labels: Record<string, string> = {};
if (options.label) {
for (const labelStr of options.label) {
const eqIndex = labelStr.indexOf('=')
const eqIndex = labelStr.indexOf("=");
if (eqIndex === -1) {
const error: CommandError = {
code: 'INVALID_LABEL',
code: "INVALID_LABEL",
message: `Invalid label format: ${labelStr}`,
details: 'Labels must be in key=value format',
}
throw error
details: "Labels must be in key=value format",
};
throw error;
}
const key = labelStr.slice(0, eqIndex)
const value = labelStr.slice(eqIndex + 1)
labels[key] = value
const key = labelStr.slice(0, eqIndex);
const value = labelStr.slice(eqIndex + 1);
labels[key] = value;
}
}
if (outputSchema) {
let structuredAgent: AgentSnapshotPayload | null = null
let structuredAgent: AgentSnapshotPayload | null = null;
const callStructuredTurn = async (structuredPrompt: string): Promise<string> => {
if (!structuredAgent) {
structuredAgent = await client.createAgent({
provider: resolvedProviderModel.provider as 'claude' | 'codex' | 'opencode',
provider: resolvedProviderModel.provider as "claude" | "codex" | "opencode",
cwd,
title: options.name,
modeId: options.mode,
@@ -374,94 +381,94 @@ export async function runRunCommand(
git,
worktreeName: options.worktree,
labels: Object.keys(labels).length > 0 ? labels : undefined,
})
});
} else {
await client.sendMessage(structuredAgent.id, structuredPrompt)
await client.sendMessage(structuredAgent.id, structuredPrompt);
}
const state = await client.waitForFinish(structuredAgent.id, waitTimeoutMs)
if (state.status === 'timeout') {
throw new StructuredRunStatusError('timeout', 'Timed out waiting for structured output')
const state = await client.waitForFinish(structuredAgent.id, waitTimeoutMs);
if (state.status === "timeout") {
throw new StructuredRunStatusError("timeout", "Timed out waiting for structured output");
}
if (state.status === 'permission') {
if (state.status === "permission") {
throw new StructuredRunStatusError(
'permission',
'Agent is waiting for permission before producing structured output'
)
"permission",
"Agent is waiting for permission before producing structured output",
);
}
if (state.status === 'error') {
if (state.status === "error") {
throw new StructuredRunStatusError(
'error',
state.error ?? 'Agent failed before producing structured output'
)
"error",
state.error ?? "Agent failed before producing structured output",
);
}
const lastMessage = await resolveStructuredResponseMessage({
client,
agentId: structuredAgent.id,
lastMessage: state.lastMessage,
})
});
if (!lastMessage) {
throw new StructuredRunStatusError(
'empty',
'Agent finished without a structured output message'
)
"empty",
"Agent finished without a structured output message",
);
}
return lastMessage
}
return lastMessage;
};
let output: Record<string, unknown>
let output: Record<string, unknown>;
try {
output = await getStructuredAgentResponse<Record<string, unknown>>({
caller: callStructuredTurn,
prompt,
schema: outputSchema,
schemaName: 'RunOutput',
schemaName: "RunOutput",
maxRetries: 2,
})
});
} catch (err) {
if (err instanceof StructuredRunStatusError) {
const error: CommandError = {
code: 'OUTPUT_SCHEMA_FAILED',
code: "OUTPUT_SCHEMA_FAILED",
message: err.message,
}
throw error
};
throw error;
}
if (err instanceof StructuredAgentResponseError) {
const error: CommandError = {
code: 'OUTPUT_SCHEMA_FAILED',
message: 'Agent response did not match the required output schema',
code: "OUTPUT_SCHEMA_FAILED",
message: "Agent response did not match the required output schema",
details:
err.validationErrors.length > 0
? err.validationErrors.join('\n')
: err.lastResponse || 'No response',
}
throw error
? err.validationErrors.join("\n")
: err.lastResponse || "No response",
};
throw error;
}
throw err
throw err;
}
if (!structuredAgent) {
const error: CommandError = {
code: 'OUTPUT_SCHEMA_FAILED',
message: 'Agent finished without a structured output message',
}
throw error
code: "OUTPUT_SCHEMA_FAILED",
message: "Agent finished without a structured output message",
};
throw error;
}
await client.close()
await client.close();
return {
type: 'single',
data: toRunResult(structuredAgent, 'completed'),
type: "single",
data: toRunResult(structuredAgent, "completed"),
schema: structuredRunSchema(output),
}
};
}
// Create the agent
const agent = await client.createAgent({
provider: resolvedProviderModel.provider as 'claude' | 'codex' | 'opencode',
provider: resolvedProviderModel.provider as "claude" | "codex" | "opencode",
cwd,
title: options.name,
modeId: options.mode,
@@ -472,43 +479,42 @@ export async function runRunCommand(
git,
worktreeName: options.worktree,
labels: Object.keys(labels).length > 0 ? labels : undefined,
})
});
// Default run behavior is foreground: wait for completion unless --detach is set.
if (!options.detach) {
const state = await client.waitForFinish(agent.id, waitTimeoutMs)
await client.close()
const state = await client.waitForFinish(agent.id, waitTimeoutMs);
await client.close();
const finalAgent = state.final ?? agent
const status: AgentRunResult['status'] =
state.status === 'idle' ? 'completed' : state.status
const finalAgent = state.final ?? agent;
const status: AgentRunResult["status"] = state.status === "idle" ? "completed" : state.status;
return {
type: 'single',
type: "single",
data: toRunResult(finalAgent, status),
schema: agentRunSchema,
}
};
}
await client.close()
await client.close();
return {
type: 'single',
type: "single",
data: toRunResult(agent),
schema: agentRunSchema,
}
};
} catch (err) {
await client.close().catch(() => {})
await client.close().catch(() => {});
if (err && typeof err === 'object' && 'code' in err) {
throw err
if (err && typeof err === "object" && "code" in err) {
throw err;
}
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'AGENT_CREATE_FAILED',
code: "AGENT_CREATE_FAILED",
message: `Failed to create agent: ${message}`,
}
throw error
};
throw error;
}
}
+125 -118
View File
@@ -1,129 +1,137 @@
import type { Command } from 'commander'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js'
import { readFile } from 'node:fs/promises'
import { extname, resolve } from 'node:path'
import type { Command } from "commander";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type {
CommandOptions,
SingleResult,
OutputSchema,
CommandError,
} from "../../output/index.js";
import { readFile } from "node:fs/promises";
import { extname, resolve } from "node:path";
/** Result type for agent send command */
export interface AgentSendResult {
agentId: string
status: 'sent' | 'completed' | 'timeout' | 'permission' | 'error'
message: string
agentId: string;
status: "sent" | "completed" | "timeout" | "permission" | "error";
message: string;
}
/** Schema for agent send output */
export const agentSendSchema: OutputSchema<AgentSendResult> = {
idField: 'agentId',
idField: "agentId",
columns: [
{ header: 'AGENT ID', field: 'agentId', width: 12 },
{ header: 'STATUS', field: 'status', width: 12 },
{ header: 'MESSAGE', field: 'message', width: 40 },
{ header: "AGENT ID", field: "agentId", width: 12 },
{ header: "STATUS", field: "status", width: 12 },
{ header: "MESSAGE", field: "message", width: 40 },
],
}
};
export interface AgentSendOptions extends CommandOptions {
noWait?: boolean
image?: string[]
prompt?: string
promptFile?: string
noWait?: boolean;
image?: string[];
prompt?: string;
promptFile?: string;
}
/**
* Read image files and convert them to base64 data URIs
*/
async function readImageFiles(imagePaths: string[]): Promise<Array<{ data: string; mimeType: string }>> {
const images: Array<{ data: string; mimeType: string }> = []
async function readImageFiles(
imagePaths: string[],
): Promise<Array<{ data: string; mimeType: string }>> {
const images: Array<{ data: string; mimeType: string }> = [];
for (const path of imagePaths) {
try {
const buffer = await readFile(path)
const ext = extname(path).toLowerCase()
const buffer = await readFile(path);
const ext = extname(path).toLowerCase();
// Determine media type from extension
let mimeType = 'image/jpeg'
let mimeType = "image/jpeg";
switch (ext) {
case '.png':
mimeType = 'image/png'
break
case '.jpg':
case '.jpeg':
mimeType = 'image/jpeg'
break
case '.gif':
mimeType = 'image/gif'
break
case '.webp':
mimeType = 'image/webp'
break
case ".png":
mimeType = "image/png";
break;
case ".jpg":
case ".jpeg":
mimeType = "image/jpeg";
break;
case ".gif":
mimeType = "image/gif";
break;
case ".webp":
mimeType = "image/webp";
break;
default:
// Default to jpeg for unknown types
mimeType = 'image/jpeg'
mimeType = "image/jpeg";
}
const data = buffer.toString('base64')
const data = buffer.toString("base64");
images.push({
data,
mimeType,
})
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'IMAGE_READ_ERROR',
code: "IMAGE_READ_ERROR",
message: `Failed to read image file: ${path}`,
details: message,
}
throw error
};
throw error;
}
}
return images
return images;
}
async function resolvePromptInput(options: {
promptArgument: string | undefined
promptOption: string | undefined
promptFile: string | undefined
promptArgument: string | undefined;
promptOption: string | undefined;
promptFile: string | undefined;
}): Promise<string> {
const promptText = options.promptArgument?.trim()
const promptOptionText = options.promptOption?.trim()
const promptFilePath = options.promptFile?.trim()
const providedSourceCount = [promptText, promptOptionText, promptFilePath].filter(Boolean).length
const promptText = options.promptArgument?.trim();
const promptOptionText = options.promptOption?.trim();
const promptFilePath = options.promptFile?.trim();
const providedSourceCount = [promptText, promptOptionText, promptFilePath].filter(Boolean).length;
if (providedSourceCount > 1) {
const error: CommandError = {
code: 'CONFLICTING_PROMPT_INPUT',
message: 'Provide exactly one of prompt argument, --prompt, or --prompt-file',
}
throw error
code: "CONFLICTING_PROMPT_INPUT",
message: "Provide exactly one of prompt argument, --prompt, or --prompt-file",
};
throw error;
}
if (promptText) {
return options.promptArgument as string
return options.promptArgument as string;
}
if (promptOptionText) {
return options.promptOption as string
return options.promptOption as string;
}
if (!promptFilePath) {
const error: CommandError = {
code: 'MISSING_PROMPT',
message: 'A prompt is required',
details: 'Usage: paseo agent send [options] <id> [prompt] | --prompt <text> | --prompt-file <path>',
}
throw error
code: "MISSING_PROMPT",
message: "A prompt is required",
details:
"Usage: paseo agent send [options] <id> [prompt] | --prompt <text> | --prompt-file <path>",
};
throw error;
}
try {
return await readFile(resolve(promptFilePath), 'utf8')
return await readFile(resolve(promptFilePath), "utf8");
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'PROMPT_FILE_READ_ERROR',
code: "PROMPT_FILE_READ_ERROR",
message: `Failed to read prompt file: ${promptFilePath}`,
details: message,
}
throw error
};
throw error;
}
}
@@ -131,126 +139,125 @@ export async function runSendCommand(
agentIdArg: string,
prompt: string | undefined,
options: AgentSendOptions,
_command: Command
_command: Command,
): Promise<SingleResult<AgentSendResult>> {
const host = getDaemonHost({ host: options.host as string | undefined })
const host = getDaemonHost({ host: options.host as string | undefined });
// Validate arguments
if (!agentIdArg || agentIdArg.trim().length === 0) {
const error: CommandError = {
code: 'MISSING_AGENT_ID',
message: 'Agent ID is required',
details: 'Usage: paseo agent send [options] <id> [prompt]',
}
throw error
code: "MISSING_AGENT_ID",
message: "Agent ID is required",
details: "Usage: paseo agent send [options] <id> [prompt]",
};
throw error;
}
const promptInput = await resolvePromptInput({
promptArgument: prompt,
promptOption: options.prompt,
promptFile: options.promptFile,
})
});
let client
let client;
try {
client = await connectToDaemon({ host: options.host as string | undefined })
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'DAEMON_NOT_RUNNING',
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: 'Start the daemon with: paseo daemon start',
}
throw error
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
try {
// Read image files if provided
const images = options.image && options.image.length > 0
? await readImageFiles(options.image)
: undefined
const images =
options.image && options.image.length > 0 ? await readImageFiles(options.image) : undefined;
// Send the message
await client.sendAgentMessage(agentIdArg, promptInput, { images })
await client.sendAgentMessage(agentIdArg, promptInput, { images });
// If --no-wait, return immediately
if (options.noWait) {
await client.close()
await client.close();
return {
type: 'single',
type: "single",
data: {
agentId: agentIdArg,
status: 'sent',
message: 'Message sent, not waiting for completion',
status: "sent",
message: "Message sent, not waiting for completion",
},
schema: agentSendSchema,
}
};
}
// Wait for agent to finish
const state = await client.waitForFinish(agentIdArg, 600000) // 10 minute timeout
const state = await client.waitForFinish(agentIdArg, 600000); // 10 minute timeout
await client.close()
await client.close();
if (state.status === 'timeout') {
if (state.status === "timeout") {
return {
type: 'single',
type: "single",
data: {
agentId: state.final?.id ?? agentIdArg,
status: 'timeout',
message: 'Timed out waiting for agent to finish',
status: "timeout",
message: "Timed out waiting for agent to finish",
},
schema: agentSendSchema,
}
};
}
if (state.status === 'permission') {
if (state.status === "permission") {
return {
type: 'single',
type: "single",
data: {
agentId: state.final?.id ?? agentIdArg,
status: 'permission',
message: 'Agent is waiting for permission',
status: "permission",
message: "Agent is waiting for permission",
},
schema: agentSendSchema,
}
};
}
if (state.status === 'error') {
if (state.status === "error") {
return {
type: 'single',
type: "single",
data: {
agentId: state.final?.id ?? agentIdArg,
status: 'error',
message: state.error ?? 'Agent finished with error',
status: "error",
message: state.error ?? "Agent finished with error",
},
schema: agentSendSchema,
}
};
}
return {
type: 'single',
type: "single",
data: {
agentId: state.final?.id ?? agentIdArg,
status: 'completed',
message: 'Agent completed processing the message',
status: "completed",
message: "Agent completed processing the message",
},
schema: agentSendSchema,
}
};
} catch (err) {
await client.close().catch(() => {})
await client.close().catch(() => {});
// Re-throw CommandError as-is
if (err && typeof err === 'object' && 'code' in err) {
throw err
if (err && typeof err === "object" && "code" in err) {
throw err;
}
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'SEND_FAILED',
code: "SEND_FAILED",
message: `Failed to send message: ${message}`,
}
throw error
};
throw error;
}
}
+61 -56
View File
@@ -1,123 +1,128 @@
import type { Command } from 'commander'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js'
import type { Command } from "commander";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type {
CommandOptions,
SingleResult,
OutputSchema,
CommandError,
} from "../../output/index.js";
/** Result type for agent stop command */
export interface StopResult {
stoppedCount: number
agentIds: string[]
stoppedCount: number;
agentIds: string[];
}
/** Schema for stop command output */
export const stopSchema: OutputSchema<StopResult> = {
// For quiet mode, output the stopped agent IDs (one per line)
idField: (item) => item.agentIds.join('\n'),
columns: [{ header: 'INTERRUPTED', field: 'stoppedCount' }],
}
idField: (item) => item.agentIds.join("\n"),
columns: [{ header: "INTERRUPTED", field: "stoppedCount" }],
};
export interface AgentStopOptions extends CommandOptions {
all?: boolean
cwd?: string
all?: boolean;
cwd?: string;
}
export type AgentStopResult = SingleResult<StopResult>
export type AgentStopResult = SingleResult<StopResult>;
export async function runStopCommand(
id: string | undefined,
options: AgentStopOptions,
_command: Command
_command: Command,
): Promise<AgentStopResult> {
const host = getDaemonHost({ host: options.host as string | undefined })
const host = getDaemonHost({ host: options.host as string | undefined });
// Validate arguments - need either an id, --all, or --cwd
if (!id && !options.all && !options.cwd) {
const error: CommandError = {
code: 'MISSING_ARGUMENT',
message: 'Agent ID required unless --all or --cwd is specified',
details: 'Usage: paseo agent stop <id> | --all | --cwd <path>',
}
throw error
code: "MISSING_ARGUMENT",
message: "Agent ID required unless --all or --cwd is specified",
details: "Usage: paseo agent stop <id> | --all | --cwd <path>",
};
throw error;
}
let client
let client;
try {
client = await connectToDaemon({ host: options.host as string | undefined })
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'DAEMON_NOT_RUNNING',
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: 'Start the daemon with: paseo daemon start',
}
throw error
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
try {
const fetchPayload = await client.fetchAgents({ filter: { includeArchived: true } })
let agents = fetchPayload.entries.map((entry) => entry.agent)
const stoppedIds: string[] = []
const fetchPayload = await client.fetchAgents({ filter: { includeArchived: true } });
let agents = fetchPayload.entries.map((entry) => entry.agent);
const stoppedIds: string[] = [];
if (options.all) {
// Stop all agents (not archived)
agents = agents.filter((a) => !a.archivedAt)
agents = agents.filter((a) => !a.archivedAt);
} else if (options.cwd) {
// Stop agents in directory
const filterCwd = options.cwd
const filterCwd = options.cwd;
agents = agents.filter((a) => {
if (a.archivedAt) return false
const agentCwd = a.cwd.replace(/\/$/, '')
const targetCwd = filterCwd.replace(/\/$/, '')
return agentCwd === targetCwd || agentCwd.startsWith(targetCwd + '/')
})
if (a.archivedAt) return false;
const agentCwd = a.cwd.replace(/\/$/, "");
const targetCwd = filterCwd.replace(/\/$/, "");
return agentCwd === targetCwd || agentCwd.startsWith(targetCwd + "/");
});
} else if (id) {
// Stop specific agent
const fetchResult = await client.fetchAgent(id)
const fetchResult = await client.fetchAgent(id);
if (!fetchResult) {
const error: CommandError = {
code: 'AGENT_NOT_FOUND',
code: "AGENT_NOT_FOUND",
message: `No agent found matching: ${id}`,
details: 'Use `paseo ls` to list available agents',
}
throw error
details: "Use `paseo ls` to list available agents",
};
throw error;
}
agents = [fetchResult.agent]
agents = [fetchResult.agent];
}
// Interrupt each running agent. Idle agents are a no-op.
for (const agent of agents) {
try {
if (agent.status === 'running') {
await client.cancelAgent(agent.id)
stoppedIds.push(agent.id)
if (agent.status === "running") {
await client.cancelAgent(agent.id);
stoppedIds.push(agent.id);
}
} catch (err) {
// Continue interrupting other agents even if one fails
const message = err instanceof Error ? err.message : String(err)
console.error(`Warning: Failed to stop agent ${agent.id.slice(0, 7)}: ${message}`)
const message = err instanceof Error ? err.message : String(err);
console.error(`Warning: Failed to stop agent ${agent.id.slice(0, 7)}: ${message}`);
}
}
await client.close()
await client.close();
return {
type: 'single',
type: "single",
data: {
stoppedCount: stoppedIds.length,
agentIds: stoppedIds,
},
schema: stopSchema,
}
};
} catch (err) {
await client.close().catch(() => {})
await client.close().catch(() => {});
// Re-throw if it's already a CommandError
if (err && typeof err === 'object' && 'code' in err) {
throw err
if (err && typeof err === "object" && "code" in err) {
throw err;
}
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'STOP_AGENT_FAILED',
code: "STOP_AGENT_FAILED",
message: `Failed to stop agent(s): ${message}`,
}
throw error
};
throw error;
}
}
+85 -80
View File
@@ -1,177 +1,182 @@
import type { Command } from 'commander'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js'
import type { Command } from "commander";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type {
CommandOptions,
SingleResult,
OutputSchema,
CommandError,
} from "../../output/index.js";
/** Result type for agent update command */
export interface AgentUpdateResult {
agentId: string
name: string | null
labels: string
agentId: string;
name: string | null;
labels: string;
}
/** Schema for update command output */
export const updateSchema: OutputSchema<AgentUpdateResult> = {
idField: 'agentId',
idField: "agentId",
columns: [
{ header: 'AGENT ID', field: 'agentId' },
{ header: 'NAME', field: 'name' },
{ header: 'LABELS', field: 'labels' },
{ header: "AGENT ID", field: "agentId" },
{ header: "NAME", field: "name" },
{ header: "LABELS", field: "labels" },
],
}
};
export interface AgentUpdateOptions extends CommandOptions {
name?: string
label?: string[]
host?: string
name?: string;
label?: string[];
host?: string;
}
export type AgentUpdateCommandResult = SingleResult<AgentUpdateResult>
export type AgentUpdateCommandResult = SingleResult<AgentUpdateResult>;
function parseLabelOptions(labels: string[] | undefined): Record<string, string> {
const parsed: Record<string, string> = {}
const parsed: Record<string, string> = {};
if (!labels) {
return parsed
return parsed;
}
for (const rawLabel of labels) {
for (const segment of rawLabel.split(',')) {
const label = segment.trim()
for (const segment of rawLabel.split(",")) {
const label = segment.trim();
if (!label) {
continue
continue;
}
const eqIndex = label.indexOf('=')
const eqIndex = label.indexOf("=");
if (eqIndex === -1) {
const error: CommandError = {
code: 'INVALID_LABEL',
code: "INVALID_LABEL",
message: `Invalid label format: ${label}`,
details: 'Labels must be in key=value format',
}
throw error
details: "Labels must be in key=value format",
};
throw error;
}
const key = label.slice(0, eqIndex).trim()
const value = label.slice(eqIndex + 1)
const key = label.slice(0, eqIndex).trim();
const value = label.slice(eqIndex + 1);
if (!key) {
const error: CommandError = {
code: 'INVALID_LABEL',
code: "INVALID_LABEL",
message: `Invalid label format: ${label}`,
details: 'Labels must include a non-empty key in key=value format',
}
throw error
details: "Labels must include a non-empty key in key=value format",
};
throw error;
}
parsed[key] = value
parsed[key] = value;
}
}
return parsed
return parsed;
}
function formatLabels(labels: Record<string, string>): string {
const entries = Object.entries(labels)
const entries = Object.entries(labels);
if (entries.length === 0) {
return '-'
return "-";
}
return entries.map(([key, value]) => `${key}=${value}`).join(',')
return entries.map(([key, value]) => `${key}=${value}`).join(",");
}
export async function runUpdateCommand(
agentIdArg: string,
options: AgentUpdateOptions,
_command: Command
_command: Command,
): Promise<AgentUpdateCommandResult> {
const host = getDaemonHost({ host: options.host as string | undefined })
const host = getDaemonHost({ host: options.host as string | undefined });
// Validate arguments
if (!agentIdArg || agentIdArg.trim().length === 0) {
const error: CommandError = {
code: 'MISSING_AGENT_ID',
message: 'Agent ID is required',
details: 'Usage: paseo agent update <id> [--name <name>] [--label <key=value>]',
}
throw error
code: "MISSING_AGENT_ID",
message: "Agent ID is required",
details: "Usage: paseo agent update <id> [--name <name>] [--label <key=value>]",
};
throw error;
}
const name = options.name?.trim()
const name = options.name?.trim();
if (options.name !== undefined && !name) {
const error: CommandError = {
code: 'INVALID_NAME',
message: 'Name cannot be empty',
details: 'Use --name <name> with a non-empty value',
}
throw error
code: "INVALID_NAME",
message: "Name cannot be empty",
details: "Use --name <name> with a non-empty value",
};
throw error;
}
const labels = parseLabelOptions(options.label)
const labels = parseLabelOptions(options.label);
if (!name && Object.keys(labels).length === 0) {
const error: CommandError = {
code: 'NO_CHANGES_PROVIDED',
message: 'Nothing to update',
details: 'Provide at least one of: --name <name>, --label <key=value>',
}
throw error
code: "NO_CHANGES_PROVIDED",
message: "Nothing to update",
details: "Provide at least one of: --name <name>, --label <key=value>",
};
throw error;
}
let client
let client;
try {
client = await connectToDaemon({ host: options.host as string | undefined })
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'DAEMON_NOT_RUNNING',
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: 'Start the daemon with: paseo daemon start',
}
throw error
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
try {
const fetchResult = await client.fetchAgent(agentIdArg)
const fetchResult = await client.fetchAgent(agentIdArg);
if (!fetchResult) {
const error: CommandError = {
code: 'AGENT_NOT_FOUND',
code: "AGENT_NOT_FOUND",
message: `Agent not found: ${agentIdArg}`,
details: 'Use "paseo ls" to list available agents',
}
throw error
};
throw error;
}
const agentId = fetchResult.agent.id
const agentId = fetchResult.agent.id;
await client.updateAgent(agentId, {
...(name ? { name } : {}),
...(Object.keys(labels).length > 0 ? { labels } : {}),
})
});
const updatedResult = await client.fetchAgent(agentId)
const updatedResult = await client.fetchAgent(agentId);
if (!updatedResult) {
throw new Error(`Agent not found after update: ${agentId}`)
throw new Error(`Agent not found after update: ${agentId}`);
}
await client.close()
await client.close();
return {
type: 'single',
type: "single",
data: {
agentId,
name: updatedResult.agent.title,
labels: formatLabels(updatedResult.agent.labels),
},
schema: updateSchema,
}
};
} catch (err) {
await client.close().catch(() => {})
await client.close().catch(() => {});
// Re-throw CommandError as-is
if (err && typeof err === 'object' && 'code' in err) {
throw err
if (err && typeof err === "object" && "code" in err) {
throw err;
}
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'UPDATE_FAILED',
code: "UPDATE_FAILED",
message: `Failed to update agent: ${message}`,
}
throw error
};
throw error;
}
}
+89 -84
View File
@@ -1,193 +1,198 @@
import type { Command } from 'commander'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js'
import { fetchAgentTimelineItems, formatAgentActivityTranscript } from './logs.js'
import { parseDuration } from '../../utils/duration.js'
import type { Command } from "commander";
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
import type {
CommandOptions,
SingleResult,
OutputSchema,
CommandError,
} from "../../output/index.js";
import { fetchAgentTimelineItems, formatAgentActivityTranscript } from "./logs.js";
import { parseDuration } from "../../utils/duration.js";
/** Result type for agent wait command */
export interface AgentWaitResult {
agentId: string
status: 'idle' | 'timeout' | 'permission' | 'error'
message: string
agentId: string;
status: "idle" | "timeout" | "permission" | "error";
message: string;
}
/** Schema for agent wait output */
export const agentWaitSchema: OutputSchema<AgentWaitResult> = {
idField: 'agentId',
idField: "agentId",
columns: [
{ header: 'AGENT ID', field: 'agentId', width: 12 },
{ header: 'STATUS', field: 'status', width: 12 },
{ header: 'MESSAGE', field: 'message', width: 40 },
{ header: "AGENT ID", field: "agentId", width: 12 },
{ header: "STATUS", field: "status", width: 12 },
{ header: "MESSAGE", field: "message", width: 40 },
],
}
};
export interface AgentWaitOptions extends CommandOptions {
timeout?: string
host?: string
timeout?: string;
host?: string;
}
const WAIT_ACTIVITY_PREVIEW_COUNT = 5
const WAIT_ACTIVITY_PREVIEW_COUNT = 5;
function appendRecentActivity(message: string, transcript: string | null): string {
if (!transcript || transcript.trim().length === 0) {
return message
return message;
}
return `${message}\nLast ${WAIT_ACTIVITY_PREVIEW_COUNT} activity items:\n${transcript}`
return `${message}\nLast ${WAIT_ACTIVITY_PREVIEW_COUNT} activity items:\n${transcript}`;
}
async function getRecentActivityTranscript(
client: Awaited<ReturnType<typeof connectToDaemon>>,
agentId: string
agentId: string,
): Promise<string | null> {
try {
const timelineItems = await fetchAgentTimelineItems(client, agentId)
return formatAgentActivityTranscript(timelineItems, WAIT_ACTIVITY_PREVIEW_COUNT)
const timelineItems = await fetchAgentTimelineItems(client, agentId);
return formatAgentActivityTranscript(timelineItems, WAIT_ACTIVITY_PREVIEW_COUNT);
} catch {
return null
return null;
}
}
export async function runWaitCommand(
agentIdArg: string,
options: AgentWaitOptions,
_command: Command
_command: Command,
): Promise<SingleResult<AgentWaitResult>> {
const host = getDaemonHost({ host: options.host as string | undefined })
const host = getDaemonHost({ host: options.host as string | undefined });
// Validate arguments
if (!agentIdArg || agentIdArg.trim().length === 0) {
const error: CommandError = {
code: 'MISSING_AGENT_ID',
message: 'Agent ID is required',
details: 'Usage: paseo agent wait <id>',
}
throw error
code: "MISSING_AGENT_ID",
message: "Agent ID is required",
details: "Usage: paseo agent wait <id>",
};
throw error;
}
// Parse timeout (no limit unless explicitly provided)
let timeoutMs = 0
let timeoutLabel: string | null = null
let timeoutMs = 0;
let timeoutLabel: string | null = null;
if (options.timeout) {
try {
timeoutMs = parseDuration(options.timeout)
timeoutMs = parseDuration(options.timeout);
if (timeoutMs <= 0) {
throw new Error('Timeout must be positive')
throw new Error("Timeout must be positive");
}
const timeoutSeconds = Math.floor(timeoutMs / 1000)
timeoutLabel = `${timeoutSeconds} second${timeoutSeconds === 1 ? '' : 's'}`
const timeoutSeconds = Math.floor(timeoutMs / 1000);
timeoutLabel = `${timeoutSeconds} second${timeoutSeconds === 1 ? "" : "s"}`;
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'INVALID_TIMEOUT',
message: 'Invalid timeout value',
code: "INVALID_TIMEOUT",
message: "Invalid timeout value",
details: message,
}
throw error
};
throw error;
}
}
let client
let client;
try {
client = await connectToDaemon({ host: options.host as string | undefined })
client = await connectToDaemon({ host: options.host as string | undefined });
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'DAEMON_NOT_RUNNING',
code: "DAEMON_NOT_RUNNING",
message: `Cannot connect to daemon at ${host}: ${message}`,
details: 'Start the daemon with: paseo daemon start',
}
throw error
details: "Start the daemon with: paseo daemon start",
};
throw error;
}
try {
try {
const state = await client.waitForFinish(agentIdArg, timeoutMs)
const resolvedAgentId = state.final?.id ?? agentIdArg
const state = await client.waitForFinish(agentIdArg, timeoutMs);
const resolvedAgentId = state.final?.id ?? agentIdArg;
const recentActivity =
state.status === 'timeout' || state.status === 'idle'
state.status === "timeout" || state.status === "idle"
? await getRecentActivityTranscript(client, resolvedAgentId)
: null
: null;
await client.close()
await client.close();
if (state.status === 'timeout') {
if (state.status === "timeout") {
const timeoutMessage = timeoutLabel
? `Agent did not finish within ${timeoutLabel}. Run \`paseo wait ${resolvedAgentId}\` again to keep waiting.`
: `Agent wait timed out. Run \`paseo wait ${resolvedAgentId}\` again to keep waiting.`
: `Agent wait timed out. Run \`paseo wait ${resolvedAgentId}\` again to keep waiting.`;
return {
type: 'single',
type: "single",
data: {
agentId: resolvedAgentId,
status: 'timeout',
status: "timeout",
message: appendRecentActivity(timeoutMessage, recentActivity),
},
schema: agentWaitSchema,
}
};
}
if (state.status === 'permission') {
const permission = state.final?.pendingPermissions?.[0]
if (state.status === "permission") {
const permission = state.final?.pendingPermissions?.[0];
return {
type: 'single',
type: "single",
data: {
agentId: resolvedAgentId,
status: 'permission',
status: "permission",
message: permission
? `Agent is waiting for permission: ${permission.kind}`
: 'Agent is waiting for permission',
: "Agent is waiting for permission",
},
schema: agentWaitSchema,
}
};
}
if (state.status === 'error') {
if (state.status === "error") {
return {
type: 'single',
type: "single",
data: {
agentId: resolvedAgentId,
status: 'error',
message: state.error ?? 'Agent finished with error',
status: "error",
message: state.error ?? "Agent finished with error",
},
schema: agentWaitSchema,
}
};
}
// Agent is idle
return {
type: 'single',
type: "single",
data: {
agentId: resolvedAgentId,
status: 'idle',
message: appendRecentActivity('Agent is idle.', recentActivity),
status: "idle",
message: appendRecentActivity("Agent is idle.", recentActivity),
},
schema: agentWaitSchema,
}
};
} catch (waitErr) {
await client.close().catch(() => {})
await client.close().catch(() => {});
const waitMessage = waitErr instanceof Error ? waitErr.message : String(waitErr)
const waitMessage = waitErr instanceof Error ? waitErr.message : String(waitErr);
// Other errors
const error: CommandError = {
code: 'WAIT_FAILED',
code: "WAIT_FAILED",
message: `Failed to wait for agent: ${waitMessage}`,
}
throw error
};
throw error;
}
} catch (err) {
await client.close().catch(() => {})
await client.close().catch(() => {});
// Re-throw CommandError as-is
if (err && typeof err === 'object' && 'code' in err) {
throw err
if (err && typeof err === "object" && "code" in err) {
throw err;
}
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'WAIT_FAILED',
code: "WAIT_FAILED",
message: `Failed to wait for agent: ${message}`,
}
throw error
};
throw error;
}
}