feat(cli): add agent update command for name and labels

This commit is contained in:
Mohamed Boudra
2026-02-12 14:15:33 +07:00
parent f55d0cd25f
commit fba7c73c8b
8 changed files with 498 additions and 0 deletions
+16
View File
@@ -16,6 +16,7 @@ import { runSendCommand } from './commands/agent/send.js'
import { runInspectCommand } from './commands/agent/inspect.js'
import { runWaitCommand } from './commands/agent/wait.js'
import { runAttachCommand } from './commands/agent/attach.js'
import { runUpdateCommand } from './commands/agent/update.js'
import { withOutput } from './output/index.js'
const VERSION = '0.1.0'
@@ -125,6 +126,21 @@ export function createCli(): Command {
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runWaitCommand))
program
.command('update')
.description('Update an agent (alias for "paseo agent update")')
.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)',
collectMultiple,
[]
)
.option('--json', 'Output in JSON format')
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runUpdateCommand))
// Top-level local daemon shortcuts
program.addCommand(daemonStartCommand())
+16
View File
@@ -9,6 +9,7 @@ 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'
export function createAgentCommand(): Command {
@@ -120,5 +121,20 @@ export function createAgentCommand(): Command {
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runArchiveCommand))
agent
.command('update')
.description("Update an agent's metadata")
.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)',
collectMultiple,
[]
)
.option('--json', 'Output in JSON format')
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runUpdateCommand))
return agent
}
+177
View File
@@ -0,0 +1,177 @@
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
}
/** Schema for update command output */
export const updateSchema: OutputSchema<AgentUpdateResult> = {
idField: 'agentId',
columns: [
{ header: 'AGENT ID', field: 'agentId' },
{ header: 'NAME', field: 'name' },
{ header: 'LABELS', field: 'labels' },
],
}
export interface AgentUpdateOptions extends CommandOptions {
name?: string
label?: string[]
host?: string
}
export type AgentUpdateCommandResult = SingleResult<AgentUpdateResult>
function parseLabelOptions(labels: string[] | undefined): Record<string, string> {
const parsed: Record<string, string> = {}
if (!labels) {
return parsed
}
for (const rawLabel of labels) {
for (const segment of rawLabel.split(',')) {
const label = segment.trim()
if (!label) {
continue
}
const eqIndex = label.indexOf('=')
if (eqIndex === -1) {
const error: CommandError = {
code: 'INVALID_LABEL',
message: `Invalid label format: ${label}`,
details: 'Labels must be in key=value format',
}
throw error
}
const key = label.slice(0, eqIndex).trim()
const value = label.slice(eqIndex + 1)
if (!key) {
const error: CommandError = {
code: 'INVALID_LABEL',
message: `Invalid label format: ${label}`,
details: 'Labels must include a non-empty key in key=value format',
}
throw error
}
parsed[key] = value
}
}
return parsed
}
function formatLabels(labels: Record<string, string>): string {
const entries = Object.entries(labels)
if (entries.length === 0) {
return '-'
}
return entries.map(([key, value]) => `${key}=${value}`).join(',')
}
export async function runUpdateCommand(
agentIdArg: string,
options: AgentUpdateOptions,
_command: Command
): Promise<AgentUpdateCommandResult> {
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
}
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
}
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
}
let client
try {
client = await connectToDaemon({ host: options.host as string | undefined })
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const error: CommandError = {
code: 'DAEMON_NOT_RUNNING',
message: `Cannot connect to daemon at ${host}: ${message}`,
details: 'Start the daemon with: paseo daemon start',
}
throw error
}
try {
const agent = await client.fetchAgent(agentIdArg)
if (!agent) {
const error: CommandError = {
code: 'AGENT_NOT_FOUND',
message: `Agent not found: ${agentIdArg}`,
details: 'Use "paseo ls" to list available agents',
}
throw error
}
const agentId = agent.id
await client.updateAgent(agentId, {
...(name ? { name } : {}),
...(Object.keys(labels).length > 0 ? { labels } : {}),
})
const updated = await client.fetchAgent(agentId)
if (!updated) {
throw new Error(`Agent not found after update: ${agentId}`)
}
await client.close()
return {
type: 'single',
data: {
agentId,
name: updated.title,
labels: formatLabels(updated.labels),
},
schema: updateSchema,
}
} catch (err) {
await client.close().catch(() => {})
// Re-throw CommandError as-is
if (err && typeof err === 'object' && 'code' in err) {
throw err
}
const message = err instanceof Error ? err.message : String(err)
const error: CommandError = {
code: 'UPDATE_FAILED',
message: `Failed to update agent: ${message}`,
}
throw error
}
}