Switch to double quotes and reformat codebase with Biome
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user