refactor(cli): rename ps to ls and add top-level agent commands

This commit is contained in:
Mohamed Boudra
2026-01-29 10:41:19 +07:00
parent 7d4636ecff
commit dfda6daeed
46 changed files with 4473 additions and 542 deletions
+187
View File
@@ -0,0 +1,187 @@
import type { Command } from 'commander'
import type { AgentPermissionRequest } from '@paseo/server'
import { connectToDaemon, getDaemonHost, resolveAgentId } from '../../utils/client.js'
import type { CommandOptions, ListResult, OutputSchema, CommandError } from '../../output/index.js'
/** Permission response item for display */
export interface PermissionResponseItem {
requestId: string
agentId: string
agentShortId: string
name: string
result: string
}
/** Schema for permit allow/deny output */
export const permitResponseSchema: OutputSchema<PermissionResponseItem> = {
idField: 'requestId',
columns: [
{ header: 'REQUEST ID', field: 'requestId', width: 12 },
{ header: 'AGENT', field: 'agentShortId', width: 10 },
{ header: 'TOOL', field: 'name', width: 20 },
{
header: 'RESULT',
field: 'result',
width: 10,
color: (value) => {
if (value === 'allowed') return 'green'
if (value === 'denied') return 'red'
return undefined
},
},
],
}
export type PermitAllowResult = ListResult<PermissionResponseItem>
export interface PermitAllowOptions extends CommandOptions {
all?: boolean
input?: string
host?: string
}
export async function runAllowCommand(
agentIdOrPrefix: string,
reqId: string | undefined,
options: PermitAllowOptions,
_command: Command
): Promise<PermitAllowResult> {
const host = getDaemonHost({ host: options.host })
// Validate arguments
if (!options.all && !reqId) {
const error: CommandError = {
code: 'MISSING_ARGUMENT',
message: 'Request ID is required unless --all is specified',
details: 'Usage: paseo permit allow <agent> <req_id> or paseo permit allow <agent> --all',
}
throw error
}
// Parse input JSON if provided
let updatedInput: Record<string, unknown> | undefined
if (options.input) {
try {
updatedInput = JSON.parse(options.input)
} catch (err) {
const error: CommandError = {
code: 'INVALID_JSON',
message: `Invalid JSON for --input: ${err instanceof Error ? err.message : String(err)}`,
details: 'Provide valid JSON, e.g., --input \'{"key": "value"}\'',
}
throw error
}
}
let client
try {
client = await connectToDaemon({ host: options.host })
} 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 {
// Request session state to get agent information
client.requestSessionState()
await new Promise((resolve) => setTimeout(resolve, 500))
const agents = client.listAgents()
// Resolve agent ID
const resolvedAgentId = resolveAgentId(agentIdOrPrefix, agents)
if (!resolvedAgentId) {
await client.close()
const error: CommandError = {
code: 'AGENT_NOT_FOUND',
message: `Agent not found: ${agentIdOrPrefix}`,
details: 'Use "paseo ls" to list available agents',
}
throw error
}
// Find the agent
const agent = agents.find((a) => a.id === resolvedAgentId)
if (!agent) {
await client.close()
const error: CommandError = {
code: 'AGENT_NOT_FOUND',
message: `Agent not found: ${resolvedAgentId}`,
}
throw error
}
// Get pending permissions for this agent
const pendingPermissions = agent.pendingPermissions || []
if (pendingPermissions.length === 0) {
await client.close()
const error: CommandError = {
code: 'NO_PENDING_PERMISSIONS',
message: `No pending permissions for agent ${agent.id.slice(0, 7)}`,
}
throw error
}
// Determine which permissions to allow
let permissionsToAllow: AgentPermissionRequest[]
if (options.all) {
permissionsToAllow = pendingPermissions
} else {
// Find permission by ID prefix
const permission = pendingPermissions.find(
(p) => p.id === reqId || p.id.startsWith(reqId!)
)
if (!permission) {
await client.close()
const error: CommandError = {
code: 'PERMISSION_NOT_FOUND',
message: `Permission request not found: ${reqId}`,
details: `Available requests: ${pendingPermissions.map((p) => p.id.slice(0, 8)).join(', ')}`,
}
throw error
}
permissionsToAllow = [permission]
}
// Allow permissions
const results: PermissionResponseItem[] = []
for (const permission of permissionsToAllow) {
await client.respondToPermission(resolvedAgentId, permission.id, {
behavior: 'allow',
...(updatedInput ? { updatedInput } : {}),
})
results.push({
requestId: permission.id.slice(0, 8),
agentId: resolvedAgentId,
agentShortId: resolvedAgentId.slice(0, 7),
name: permission.name,
result: 'allowed',
})
}
await client.close()
return {
type: 'list',
data: results,
schema: permitResponseSchema,
}
} catch (err) {
await client.close().catch(() => {})
// Re-throw CommandErrors
if (err && typeof err === 'object' && 'code' in err) {
throw err
}
const message = err instanceof Error ? err.message : String(err)
const error: CommandError = {
code: 'ALLOW_PERMISSION_FAILED',
message: `Failed to allow permission: ${message}`,
}
throw error
}
}
+146
View File
@@ -0,0 +1,146 @@
import type { Command } from 'commander'
import type { AgentPermissionRequest } from '@paseo/server'
import { connectToDaemon, getDaemonHost, resolveAgentId } from '../../utils/client.js'
import type { CommandOptions, ListResult, CommandError } from '../../output/index.js'
import { permitResponseSchema, type PermissionResponseItem } from './allow.js'
export type PermitDenyResult = ListResult<PermissionResponseItem>
export interface PermitDenyOptions extends CommandOptions {
all?: boolean
message?: string
interrupt?: boolean
host?: string
}
export async function runDenyCommand(
agentIdOrPrefix: string,
reqId: string | undefined,
options: PermitDenyOptions,
_command: Command
): Promise<PermitDenyResult> {
const host = getDaemonHost({ host: options.host })
// Validate arguments
if (!options.all && !reqId) {
const error: CommandError = {
code: 'MISSING_ARGUMENT',
message: 'Request ID is required unless --all is specified',
details: 'Usage: paseo permit deny <agent> <req_id> or paseo permit deny <agent> --all',
}
throw error
}
let client
try {
client = await connectToDaemon({ host: options.host })
} 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 {
// Request session state to get agent information
client.requestSessionState()
await new Promise((resolve) => setTimeout(resolve, 500))
const agents = client.listAgents()
// Resolve agent ID
const resolvedAgentId = resolveAgentId(agentIdOrPrefix, agents)
if (!resolvedAgentId) {
await client.close()
const error: CommandError = {
code: 'AGENT_NOT_FOUND',
message: `Agent not found: ${agentIdOrPrefix}`,
details: 'Use "paseo ls" to list available agents',
}
throw error
}
// Find the agent
const agent = agents.find((a) => a.id === resolvedAgentId)
if (!agent) {
await client.close()
const error: CommandError = {
code: 'AGENT_NOT_FOUND',
message: `Agent not found: ${resolvedAgentId}`,
}
throw error
}
// Get pending permissions for this agent
const pendingPermissions = agent.pendingPermissions || []
if (pendingPermissions.length === 0) {
await client.close()
const error: CommandError = {
code: 'NO_PENDING_PERMISSIONS',
message: `No pending permissions for agent ${agent.id.slice(0, 7)}`,
}
throw error
}
// Determine which permissions to deny
let permissionsToDeny: AgentPermissionRequest[]
if (options.all) {
permissionsToDeny = pendingPermissions
} else {
// Find permission by ID prefix
const permission = pendingPermissions.find(
(p) => p.id === reqId || p.id.startsWith(reqId!)
)
if (!permission) {
await client.close()
const error: CommandError = {
code: 'PERMISSION_NOT_FOUND',
message: `Permission request not found: ${reqId}`,
details: `Available requests: ${pendingPermissions.map((p) => p.id.slice(0, 8)).join(', ')}`,
}
throw error
}
permissionsToDeny = [permission]
}
// Deny permissions
const results: PermissionResponseItem[] = []
for (const permission of permissionsToDeny) {
await client.respondToPermission(resolvedAgentId, permission.id, {
behavior: 'deny',
...(options.message ? { message: options.message } : {}),
...(options.interrupt ? { interrupt: true } : {}),
})
results.push({
requestId: permission.id.slice(0, 8),
agentId: resolvedAgentId,
agentShortId: resolvedAgentId.slice(0, 7),
name: permission.name,
result: 'denied',
})
}
await client.close()
return {
type: 'list',
data: results,
schema: permitResponseSchema,
}
} catch (err) {
await client.close().catch(() => {})
// Re-throw CommandErrors
if (err && typeof err === 'object' && 'code' in err) {
throw err
}
const message = err instanceof Error ? err.message : String(err)
const error: CommandError = {
code: 'DENY_PERMISSION_FAILED',
message: `Failed to deny permission: ${message}`,
}
throw error
}
}
+44
View File
@@ -0,0 +1,44 @@
import { Command } from 'commander'
import { runLsCommand } from './ls.js'
import { runAllowCommand } from './allow.js'
import { runDenyCommand } from './deny.js'
import { withOutput } from '../../output/index.js'
export function createPermitCommand(): Command {
const permit = new Command('permit').description('Manage permission requests')
permit
.command('ls')
.description('List all pending permissions')
.option('--json', 'Output in JSON format')
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
.action((options, command) => {
if (options.json) {
command.parent.parent.opts().format = 'json'
}
return withOutput(runLsCommand)(options, command)
})
permit
.command('allow')
.description('Allow a permission request')
.argument('<agent>', 'Agent ID (or prefix)')
.argument('[req_id]', 'Permission request ID (optional if --all)')
.option('--all', 'Allow all pending permissions for this agent')
.option('--input <json>', 'Modified input parameters (JSON)')
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runAllowCommand))
permit
.command('deny')
.description('Deny a permission request')
.argument('<agent>', 'Agent ID (or prefix)')
.argument('[req_id]', 'Permission request ID (optional if --all)')
.option('--all', 'Deny all pending permissions for this agent')
.option('--message <msg>', 'Denial reason message')
.option('--interrupt', 'Stop agent after denial')
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runDenyCommand))
return permit
}
+110
View File
@@ -0,0 +1,110 @@
import type { Command } from 'commander'
import type { AgentPermissionRequest, AgentSnapshotPayload } from '@paseo/server'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions, ListResult, OutputSchema, CommandError } from '../../output/index.js'
/** Permission list item for display */
export interface PermissionListItem {
id: string
agentId: string
agentShortId: string
agentName: string
name: string
kind: string
title: string
description: string
}
/** Schema for permit ls output */
export const permitLsSchema: OutputSchema<PermissionListItem> = {
idField: 'id',
columns: [
{ header: 'REQUEST ID', field: 'id', width: 12 },
{ header: 'AGENT', field: 'agentShortId', width: 10 },
{ header: 'NAME', field: 'agentName', width: 20 },
{ header: 'TOOL', field: 'name', width: 20 },
{
header: 'KIND',
field: 'kind',
width: 8,
color: (value) => {
if (value === 'tool') return 'blue'
if (value === 'plan') return 'magenta'
return undefined
},
},
{ header: 'TITLE', field: 'title', width: 30 },
],
}
/** Transform agent snapshot + permission to list item */
function toListItem(agent: AgentSnapshotPayload, permission: AgentPermissionRequest): PermissionListItem {
return {
id: permission.id.slice(0, 8),
agentId: agent.id,
agentShortId: agent.id.slice(0, 7),
agentName: agent.title ?? '-',
name: permission.name,
kind: permission.kind,
title: permission.title ?? '-',
description: permission.description ?? '-',
}
}
export type PermitLsResult = ListResult<PermissionListItem>
export interface PermitLsOptions extends CommandOptions {
host?: string
}
export async function runLsCommand(options: PermitLsOptions, _command: Command): Promise<PermitLsResult> {
const host = getDaemonHost({ host: options.host })
let client
try {
client = await connectToDaemon({ host: options.host })
} 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 {
// Request session state to get agent information
client.requestSessionState()
// Wait a moment for the session state to be populated
await new Promise((resolve) => setTimeout(resolve, 500))
const agents = client.listAgents()
await client.close()
// Collect all pending permissions from all agents
const items: PermissionListItem[] = []
for (const agent of agents) {
if (agent.pendingPermissions && agent.pendingPermissions.length > 0) {
for (const permission of agent.pendingPermissions) {
items.push(toListItem(agent, permission))
}
}
}
return {
type: 'list',
data: items,
schema: permitLsSchema,
}
} catch (err) {
await client.close().catch(() => {})
const message = err instanceof Error ? err.message : String(err)
const error: CommandError = {
code: 'LIST_PERMISSIONS_FAILED',
message: `Failed to list permissions: ${message}`,
}
throw error
}
}