From 20cd183fa096e404f5fb88f4f2ba410b9e99e352 Mon Sep 17 00:00:00 2001 From: mohiit1502 Date: Sun, 23 Aug 2026 11:49:43 +0530 Subject: [PATCH] BrowserCLI: complete plugin, integration, workflow, policy command surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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> --- src/components/BrowserCLI.tsx | 189 ++++++++++++++++++++++++++++------ 1 file changed, 160 insertions(+), 29 deletions(-) diff --git a/src/components/BrowserCLI.tsx b/src/components/BrowserCLI.tsx index 19b6502..3569baf 100644 --- a/src/components/BrowserCLI.tsx +++ b/src/components/BrowserCLI.tsx @@ -124,19 +124,44 @@ const MANUAL_ENTRIES: Record = { examples: ['task list', 'task get task_123'], }, plugin: { - summary: 'List and inspect plugins available to this workspace.', - usage: ['plugin list', 'plugin get '], - examples: ['plugin list', 'plugin get plugin_123'], + summary: 'List, search, enable, disable, and remove plugins.', + usage: ['plugin list', 'plugin search ', 'plugin get ', 'plugin enable ', 'plugin disable ', 'plugin uninstall '], + actions: [ + { action: 'plugin list', description: 'List all plugins with trust level and scan status.' }, + { action: 'plugin search ', description: 'Filter plugins by name or description.' }, + { action: 'plugin get ', description: 'Fetch full details for a single plugin.' }, + { action: 'plugin enable ', description: 'Enable a plugin for use by agents.' }, + { action: 'plugin disable ', description: 'Disable a plugin without deleting it.' }, + { action: 'plugin uninstall ', 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 ', 'integration test ', 'integration delete '], - 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 ', 'integration create --provider-id= --name=', 'integration test ', 'integration delete '], + 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 ', description: 'Fetch full details for a single instance.' }, + { action: 'integration create --provider-id= --name=', description: 'Create a new integration instance.' }, + { action: 'integration test ', description: 'Test connectivity for an instance.' }, + { action: 'integration delete ', 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 ', 'workflow run [input]', 'workflow delete '], - examples: ['workflow list', 'workflow run wf_autoblogger_v1 Draft a launch note'], + usage: ['workflow list', 'workflow get ', 'workflow create ', 'workflow status ', 'workflow run [input]', 'workflow delete '], + actions: [ + { action: 'workflow list', description: 'List all workflows with execution counts.' }, + { action: 'workflow get ', description: 'Fetch full definition and metadata.' }, + { action: 'workflow create ', description: 'Create a new workflow scaffold.' }, + { action: 'workflow status ', description: 'Show status and recent executions for a workflow.' }, + { action: 'workflow run [input]', description: 'Trigger a workflow execution.' }, + { action: 'workflow delete ', 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 = { 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 '], + summary: 'List, inspect, create, and delete policies governing agent behavior.', + usage: ['policy list', 'policy get ', 'policy create [--type=allow|deny] [--priority=N]', 'policy delete '], 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 ', description: 'Fetch full details for a single policy.' }, + { action: 'policy create [--type=allow|deny] [--priority=N]', description: 'Create a new policy.' }, + { action: 'policy delete ', 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 { } async function execPluginCommand(args: string[]): Promise { - 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 \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 { const p = await pluginsApi.get(id) return { output: formatJson(p) } } + case 'enable': { + if (!id) return { output: '\x1b[31mUsage: plugin enable \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 \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} \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 { - 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 { ) 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 \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= --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 \x1b[0m', error: true } const r = await integrationsApi.testInstance(id) @@ -408,7 +500,7 @@ async function execIntegrationCommand(args: string[]): Promise { 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 { 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 { 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 [--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 \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 [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 { 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 { // ── Policy commands ────────────────────────────────────────────────────────── async function execPolicyCommand(args: string[]): Promise { - 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 { 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 [--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 \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 } } }