BrowserCLI: complete plugin, integration, workflow, policy command surface
Some checks failed
Stuffle/agos-client/pipeline/head There was a failure building this commit
Some checks failed
Stuffle/agos-client/pipeline/head There was a failure building this commit
- plugin: add search, enable, disable, uninstall (was list/get only) - integration: add catalog and providers subcommands; rename list→instances alias - workflow: add create and status subcommands (was list/get/run/delete) - policy: add create and delete subcommands (was list/get only) - Update all MANUAL_ENTRIES to reflect expanded surfaces - Error messages updated to reference correct subcommand sets Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -124,19 +124,44 @@ const MANUAL_ENTRIES: Record<string, ManualEntry> = {
|
||||
examples: ['task list', 'task get task_123'],
|
||||
},
|
||||
plugin: {
|
||||
summary: 'List and inspect plugins available to this workspace.',
|
||||
usage: ['plugin list', 'plugin get <id>'],
|
||||
examples: ['plugin list', 'plugin get plugin_123'],
|
||||
summary: 'List, search, enable, disable, and remove plugins.',
|
||||
usage: ['plugin list', 'plugin search <query>', 'plugin get <id>', 'plugin enable <id>', 'plugin disable <id>', 'plugin uninstall <id>'],
|
||||
actions: [
|
||||
{ action: 'plugin list', description: 'List all plugins with trust level and scan status.' },
|
||||
{ action: 'plugin search <query>', description: 'Filter plugins by name or description.' },
|
||||
{ action: 'plugin get <id>', description: 'Fetch full details for a single plugin.' },
|
||||
{ action: 'plugin enable <id>', description: 'Enable a plugin for use by agents.' },
|
||||
{ action: 'plugin disable <id>', description: 'Disable a plugin without deleting it.' },
|
||||
{ action: 'plugin uninstall <id>', description: 'Permanently delete a plugin.' },
|
||||
],
|
||||
examples: ['plugin list', 'plugin search web', 'plugin enable plugin_123'],
|
||||
},
|
||||
integration: {
|
||||
summary: 'Inspect integration instances and connectivity.',
|
||||
usage: ['integration list', 'integration get <id>', 'integration test <id>', 'integration delete <id>'],
|
||||
examples: ['integration list', 'integration test instance_123'],
|
||||
summary: 'Inspect the integration catalog, providers, instances, and connectivity.',
|
||||
usage: ['integration catalog [search]', 'integration providers [category]', 'integration instances', 'integration get <id>', 'integration create --provider-id=<id> --name=<name>', 'integration test <id>', 'integration delete <id>'],
|
||||
actions: [
|
||||
{ action: 'integration catalog [query]', description: 'Browse the integration catalog.' },
|
||||
{ action: 'integration providers [category]', description: 'List configured integration providers.' },
|
||||
{ action: 'integration instances', description: 'List active integration instances.' },
|
||||
{ action: 'integration get <id>', description: 'Fetch full details for a single instance.' },
|
||||
{ action: 'integration create --provider-id=<id> --name=<name>', description: 'Create a new integration instance.' },
|
||||
{ action: 'integration test <id>', description: 'Test connectivity for an instance.' },
|
||||
{ action: 'integration delete <id>', description: 'Delete an integration instance.' },
|
||||
],
|
||||
examples: ['integration catalog', 'integration providers', 'integration instances', 'integration test instance_123'],
|
||||
},
|
||||
workflow: {
|
||||
summary: 'Inspect, create, run, and monitor workflows.',
|
||||
usage: ['workflow list', 'workflow get <id>', 'workflow run <id> [input]', 'workflow delete <id>'],
|
||||
examples: ['workflow list', 'workflow run wf_autoblogger_v1 Draft a launch note'],
|
||||
usage: ['workflow list', 'workflow get <id>', 'workflow create <name>', 'workflow status <id>', 'workflow run <id> [input]', 'workflow delete <id>'],
|
||||
actions: [
|
||||
{ action: 'workflow list', description: 'List all workflows with execution counts.' },
|
||||
{ action: 'workflow get <id>', description: 'Fetch full definition and metadata.' },
|
||||
{ action: 'workflow create <name>', description: 'Create a new workflow scaffold.' },
|
||||
{ action: 'workflow status <id>', description: 'Show status and recent executions for a workflow.' },
|
||||
{ action: 'workflow run <id> [input]', description: 'Trigger a workflow execution.' },
|
||||
{ action: 'workflow delete <id>', description: 'Delete a workflow.' },
|
||||
],
|
||||
examples: ['workflow list', 'workflow status wf_123', 'workflow run wf_autoblogger_v1 Draft a launch note'],
|
||||
},
|
||||
model: {
|
||||
summary: 'Inspect model registrations, providers, and connectivity.',
|
||||
@@ -168,13 +193,15 @@ const MANUAL_ENTRIES: Record<string, ManualEntry> = {
|
||||
examples: ['doc search how to create an agent', 'doc search "PKCE login"', 'doc list'],
|
||||
},
|
||||
policy: {
|
||||
summary: 'List and inspect policies governing agent behavior.',
|
||||
usage: ['policy list', 'policy get <id>'],
|
||||
summary: 'List, inspect, create, and delete policies governing agent behavior.',
|
||||
usage: ['policy list', 'policy get <id>', 'policy create <name> [--type=allow|deny] [--priority=N]', 'policy delete <id>'],
|
||||
actions: [
|
||||
{ action: 'policy list', description: 'List all policies with type and status.' },
|
||||
{ action: 'policy list [--type=...]', description: 'List all policies with type, priority, and enabled state.' },
|
||||
{ action: 'policy get <id>', description: 'Fetch full details for a single policy.' },
|
||||
{ action: 'policy create <name> [--type=allow|deny] [--priority=N]', description: 'Create a new policy.' },
|
||||
{ action: 'policy delete <id>', description: 'Permanently delete a policy.' },
|
||||
],
|
||||
examples: ['policy list', 'policy get policy_123'],
|
||||
examples: ['policy list', 'policy create "Block external calls" --type=deny --priority=10', 'policy delete policy_123'],
|
||||
},
|
||||
system: {
|
||||
summary: 'Show system health, identity, and quota overview.',
|
||||
@@ -358,14 +385,33 @@ async function execTaskCommand(args: string[]): Promise<CmdResult> {
|
||||
}
|
||||
|
||||
async function execPluginCommand(args: string[]): Promise<CmdResult> {
|
||||
const [subcmd, id] = args
|
||||
const [subcmd, id, ...rest] = args
|
||||
switch (subcmd) {
|
||||
case 'list': {
|
||||
const search = rest.find(a => a.startsWith('--search='))?.split('=')[1]
|
||||
const res = await pluginsApi.list({ page_size: 50 })
|
||||
if (!res.items.length) return { output: '\x1b[90mNo plugins found.\x1b[0m' }
|
||||
const header = `\x1b[1m\x1b[36m${'ID'.padEnd(36)} ${'NAME'.padEnd(28)} STATUS\x1b[0m`
|
||||
const rows = res.items.map((p: ApiPlugin) =>
|
||||
`${p.id.padEnd(36)} ${p.name.slice(0, 28).padEnd(28)} ${p.scan_status ?? '—'}`
|
||||
const items = search
|
||||
? res.items.filter((p: ApiPlugin) => p.name.toLowerCase().includes(search.toLowerCase()))
|
||||
: res.items
|
||||
if (!items.length) return { output: '\x1b[90mNo plugins found.\x1b[0m' }
|
||||
const header = `\x1b[1m\x1b[36m${'ID'.padEnd(36)} ${'NAME'.padEnd(28)} ${'TRUST'.padEnd(12)} STATUS\x1b[0m`
|
||||
const rows = items.map((p: ApiPlugin) =>
|
||||
`${p.id.padEnd(36)} ${p.name.slice(0, 28).padEnd(28)} ${String(p.trust_level ?? '—').padEnd(12)} ${p.scan_status ?? '—'}`
|
||||
)
|
||||
return { output: [header, ...rows].join('\r\n') }
|
||||
}
|
||||
case 'search': {
|
||||
const query = [id, ...rest].filter(Boolean).join(' ').trim()
|
||||
if (!query) return { output: '\x1b[31mUsage: plugin search <query>\x1b[0m', error: true }
|
||||
const res = await pluginsApi.list({ page_size: 100 })
|
||||
const matches = res.items.filter((p: ApiPlugin) =>
|
||||
p.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
(p.description ?? '').toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
if (!matches.length) return { output: `\x1b[90mNo plugins matching: ${query}\x1b[0m` }
|
||||
const header = `\x1b[1m\x1b[36m${'ID'.padEnd(36)} ${'NAME'.padEnd(28)} ${'TRUST'.padEnd(12)} STATUS\x1b[0m`
|
||||
const rows = matches.map((p: ApiPlugin) =>
|
||||
`${p.id.padEnd(36)} ${p.name.slice(0, 28).padEnd(28)} ${String(p.trust_level ?? '—').padEnd(12)} ${p.scan_status ?? '—'}`
|
||||
)
|
||||
return { output: [header, ...rows].join('\r\n') }
|
||||
}
|
||||
@@ -374,16 +420,34 @@ async function execPluginCommand(args: string[]): Promise<CmdResult> {
|
||||
const p = await pluginsApi.get(id)
|
||||
return { output: formatJson(p) }
|
||||
}
|
||||
case 'enable': {
|
||||
if (!id) return { output: '\x1b[31mUsage: plugin enable <id>\x1b[0m', error: true }
|
||||
const r = await pluginsApi.enable(id)
|
||||
return { output: `\x1b[32m✓ Plugin ${id} enabled (enabled=${r.enabled})\x1b[0m` }
|
||||
}
|
||||
case 'disable': {
|
||||
if (!id) return { output: '\x1b[31mUsage: plugin disable <id>\x1b[0m', error: true }
|
||||
const r = await pluginsApi.disable(id)
|
||||
return { output: `\x1b[33m○ Plugin ${id} disabled (enabled=${r.enabled})\x1b[0m` }
|
||||
}
|
||||
case 'uninstall':
|
||||
case 'delete': {
|
||||
if (!id) return { output: `\x1b[31mUsage: plugin ${subcmd} <id>\x1b[0m`, error: true }
|
||||
await pluginsApi.delete(id)
|
||||
return { output: `\x1b[31mPlugin ${id} deleted.\x1b[0m` }
|
||||
}
|
||||
default:
|
||||
return { output: `\x1b[31mUnknown plugin subcommand: ${subcmd}\x1b[0m\r\nUsage: plugin [list|get]`, error: true }
|
||||
return { output: `\x1b[31mUnknown plugin subcommand: ${subcmd}\x1b[0m\r\nUsage: plugin [list|search|get|enable|disable|uninstall]`, error: true }
|
||||
}
|
||||
}
|
||||
|
||||
async function execIntegrationCommand(args: string[]): Promise<CmdResult> {
|
||||
const [subcmd, id] = args
|
||||
const [subcmd, id, ...rest] = args
|
||||
switch (subcmd) {
|
||||
case 'instances':
|
||||
case 'list': {
|
||||
const res = await integrationsApi.instances()
|
||||
const category = rest.find(a => a.startsWith('--category='))?.split('=')[1]
|
||||
const res = await integrationsApi.instances(category ? { category } : undefined)
|
||||
if (!res.length) return { output: '\x1b[90mNo integration instances found.\x1b[0m' }
|
||||
const header = `\x1b[1m\x1b[36m${'ID'.padEnd(36)} ${'NAME'.padEnd(28)} ${'PROVIDER'.padEnd(20)} STATUS\x1b[0m`
|
||||
const rows = res.map((i: ApiIntegrationInstance) =>
|
||||
@@ -391,11 +455,39 @@ async function execIntegrationCommand(args: string[]): Promise<CmdResult> {
|
||||
)
|
||||
return { output: [header, ...rows].join('\r\n') }
|
||||
}
|
||||
case 'catalog': {
|
||||
const search = [id, ...rest].filter(Boolean).join(' ').trim() || undefined
|
||||
const entries = await integrationsApi.catalog(search ? { search } : undefined)
|
||||
if (!entries.length) return { output: '\x1b[90mNo catalog entries found.\x1b[0m' }
|
||||
const header = `\x1b[1m\x1b[36m${'SLUG'.padEnd(24)} ${'NAME'.padEnd(28)} ${'CATEGORY'.padEnd(16)} STATUS\x1b[0m`
|
||||
const rows = entries.map((e) =>
|
||||
`${String(e.slug ?? '—').padEnd(24)} ${String(e.name ?? '—').slice(0, 28).padEnd(28)} ${String(e.category ?? '—').slice(0, 16).padEnd(16)} ${e.status ?? '—'}`
|
||||
)
|
||||
return { output: [header, ...rows].join('\r\n') }
|
||||
}
|
||||
case 'providers': {
|
||||
const category = id || undefined
|
||||
const res = await integrationsApi.providers(category)
|
||||
if (!res.length) return { output: '\x1b[90mNo integration providers found.\x1b[0m' }
|
||||
const header = `\x1b[1m\x1b[36m${'ID'.padEnd(36)} ${'NAME'.padEnd(28)} ${'CATEGORY'.padEnd(16)} AUTH\x1b[0m`
|
||||
const rows = res.map((p) =>
|
||||
`${p.id.padEnd(36)} ${p.name.slice(0, 28).padEnd(28)} ${String(p.category ?? '—').slice(0, 16).padEnd(16)} ${p.auth_type ?? '—'}`
|
||||
)
|
||||
return { output: [header, ...rows].join('\r\n') }
|
||||
}
|
||||
case 'get': {
|
||||
if (!id) return { output: '\x1b[31mUsage: integration get <id>\x1b[0m', error: true }
|
||||
const i = await integrationsApi.getInstance(id)
|
||||
return { output: formatJson(i) }
|
||||
}
|
||||
case 'create': {
|
||||
const providerId = rest.find(a => a.startsWith('--provider-id='))?.split('=')[1] ?? id
|
||||
const name = rest.find(a => a.startsWith('--name='))?.split('=')[1]
|
||||
if (!providerId || !name) return { output: '\x1b[31mUsage: integration create --provider-id=<id> --name=<name>\x1b[0m', error: true }
|
||||
const body = { provider_id: providerId, name, config: {} }
|
||||
const created = await integrationsApi.createInstance(body)
|
||||
return { output: `\x1b[32m✓ Created integration instance ${created.id} (${created.name})\x1b[0m` }
|
||||
}
|
||||
case 'test': {
|
||||
if (!id) return { output: '\x1b[31mUsage: integration test <id>\x1b[0m', error: true }
|
||||
const r = await integrationsApi.testInstance(id)
|
||||
@@ -408,7 +500,7 @@ async function execIntegrationCommand(args: string[]): Promise<CmdResult> {
|
||||
return { output: `\x1b[31mIntegration ${id} deleted.\x1b[0m` }
|
||||
}
|
||||
default:
|
||||
return { output: `\x1b[31mUnknown integration subcommand: ${subcmd}\x1b[0m\r\nUsage: integration [list|get|test|delete]`, error: true }
|
||||
return { output: `\x1b[31mUnknown integration subcommand: ${subcmd}\x1b[0m\r\nUsage: integration [catalog|providers|instances|get|create|test|delete]`, error: true }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,9 +510,9 @@ async function execWorkflowCommand(args: string[]): Promise<CmdResult> {
|
||||
case 'list': {
|
||||
const res = await workflowsApi.list({ page_size: 50 })
|
||||
if (!res.items.length) return { output: '\x1b[90mNo workflows found.\x1b[0m' }
|
||||
const header = `\x1b[1m\x1b[36m${'ID'.padEnd(36)} ${'NAME'.padEnd(32)} STATUS\x1b[0m`
|
||||
const header = `\x1b[1m\x1b[36m${'ID'.padEnd(36)} ${'NAME'.padEnd(32)} ${'EXECUTIONS'.padEnd(10)} STATUS\x1b[0m`
|
||||
const rows = res.items.map((w: ApiWorkflow) =>
|
||||
`${w.id.padEnd(36)} ${w.name.slice(0, 32).padEnd(32)} \x1b[${stateColor(w.status ?? '')}m${w.status ?? '—'}\x1b[0m`
|
||||
`${w.id.padEnd(36)} ${w.name.slice(0, 32).padEnd(32)} ${String(w.execution_count ?? 0).padEnd(10)} \x1b[${stateColor(w.status ?? '')}m${w.status ?? '—'}\x1b[0m`
|
||||
)
|
||||
return { output: [header, ...rows].join('\r\n') }
|
||||
}
|
||||
@@ -429,10 +521,35 @@ async function execWorkflowCommand(args: string[]): Promise<CmdResult> {
|
||||
const w = await workflowsApi.get(id)
|
||||
return { output: formatJson(w) }
|
||||
}
|
||||
case 'create': {
|
||||
const name = [id, ...rest].filter(a => !a.startsWith('--')).join(' ').trim()
|
||||
const desc = rest.find(a => a.startsWith('--description='))?.split('=')[1]
|
||||
if (!name) return { output: '\x1b[31mUsage: workflow create <name> [--description=...]\x1b[0m', error: true }
|
||||
const w = await workflowsApi.create({ name, description: desc })
|
||||
return { output: `\x1b[32m✓ Workflow created:\x1b[0m ${w.id} ${w.name}` }
|
||||
}
|
||||
case 'status': {
|
||||
if (!id) return { output: '\x1b[31mUsage: workflow status <id>\x1b[0m', error: true }
|
||||
const w = await workflowsApi.get(id)
|
||||
const execs = await workflowsApi.executions(id, 5)
|
||||
const lines = [
|
||||
`\x1b[1mWorkflow:\x1b[0m ${w.name} (${w.id})`,
|
||||
`\x1b[1mStatus:\x1b[0m \x1b[${stateColor(w.status ?? '')}m${w.status ?? '—'}\x1b[0m`,
|
||||
`\x1b[1mRuns:\x1b[0m ${w.execution_count ?? 0}`,
|
||||
`\x1b[1mLast run:\x1b[0m ${w.last_execution_at ?? '—'} (${w.last_execution_status ?? '—'})`,
|
||||
'',
|
||||
'\x1b[1mRecent executions:\x1b[0m',
|
||||
...(execs.length
|
||||
? execs.map(e => ` ${e.id} \x1b[${stateColor(e.status)}m${e.status.padEnd(12)}\x1b[0m ${e.started_at}`)
|
||||
: [' \x1b[90m(none)\x1b[0m']),
|
||||
]
|
||||
return { output: lines.join('\r\n') }
|
||||
}
|
||||
case 'run': {
|
||||
if (!id) return { output: '\x1b[31mUsage: workflow run <id> [input]\x1b[0m', error: true }
|
||||
const input = rest.join(' ') || undefined
|
||||
const ex = await workflowsApi.run(id, { agent_id: id, parameters: input ? { goal: input } : {} })
|
||||
const input = rest.filter(a => !a.startsWith('--')).join(' ') || undefined
|
||||
const agentId = rest.find(a => a.startsWith('--agent-id='))?.split('=')[1] ?? id
|
||||
const ex = await workflowsApi.run(id, { agent_id: agentId, parameters: input ? { goal: input } : {} })
|
||||
return { output: `\x1b[32mExecution started:\x1b[0m ${formatJson(ex)}` }
|
||||
}
|
||||
case 'delete': {
|
||||
@@ -441,7 +558,7 @@ async function execWorkflowCommand(args: string[]): Promise<CmdResult> {
|
||||
return { output: `\x1b[31mWorkflow ${id} deleted.\x1b[0m` }
|
||||
}
|
||||
default:
|
||||
return { output: `\x1b[31mUnknown workflow subcommand: ${subcmd}\x1b[0m\r\nUsage: workflow [list|get|run|delete]`, error: true }
|
||||
return { output: `\x1b[31mUnknown workflow subcommand: ${subcmd}\x1b[0m\r\nUsage: workflow [list|get|create|status|run|delete]`, error: true }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,10 +806,11 @@ async function execDocCommand(args: string[]): Promise<CmdResult> {
|
||||
// ── Policy commands ──────────────────────────────────────────────────────────
|
||||
|
||||
async function execPolicyCommand(args: string[]): Promise<CmdResult> {
|
||||
const [subcmd, id] = args
|
||||
const [subcmd, id, ...rest] = args
|
||||
switch (subcmd) {
|
||||
case 'list': {
|
||||
const res = await policiesApi.list({ page_size: 50 })
|
||||
const policyType = rest.find(a => a.startsWith('--type='))?.split('=')[1]
|
||||
const res = await policiesApi.list({ page_size: 50, policy_type: policyType })
|
||||
const items = res.items ?? []
|
||||
if (!items.length) return { output: '\x1b[90mNo policies found.\x1b[0m' }
|
||||
const header = `\x1b[1m\x1b[36m${'ID'.padEnd(36)} ${'NAME'.padEnd(24)} ${'TYPE'.padEnd(14)} ${'PRIORITY'.padEnd(8)} ENABLED\x1b[0m`
|
||||
@@ -706,8 +824,21 @@ async function execPolicyCommand(args: string[]): Promise<CmdResult> {
|
||||
const p = await policiesApi.get(id)
|
||||
return { output: formatJson(p) }
|
||||
}
|
||||
case 'create': {
|
||||
const name = [id, ...rest].filter(a => !a.startsWith('--')).join(' ').trim()
|
||||
const policyType = rest.find(a => a.startsWith('--type='))?.split('=')[1] ?? 'allow'
|
||||
const priority = Number(rest.find(a => a.startsWith('--priority='))?.split('=')[1] ?? 50)
|
||||
if (!name) return { output: '\x1b[31mUsage: policy create <name> [--type=allow|deny] [--priority=N]\x1b[0m', error: true }
|
||||
const p = await policiesApi.create({ name, policy_type: policyType, priority })
|
||||
return { output: `\x1b[32m✓ Policy created:\x1b[0m ${p.id} ${p.name} (type=${p.policy_type}, priority=${p.priority})` }
|
||||
}
|
||||
case 'delete': {
|
||||
if (!id) return { output: '\x1b[31mUsage: policy delete <id>\x1b[0m', error: true }
|
||||
await policiesApi.delete(id)
|
||||
return { output: `\x1b[31mPolicy ${id} deleted.\x1b[0m` }
|
||||
}
|
||||
default:
|
||||
return { output: `\x1b[31mUnknown policy subcommand: ${subcmd}\x1b[0m\r\nUsage: policy [list|get]`, error: true }
|
||||
return { output: `\x1b[31mUnknown policy subcommand: ${subcmd}\x1b[0m\r\nUsage: policy [list|get|create|delete]`, error: true }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user