diff --git a/src/commands/commands_list.ts b/src/commands/commands_list.ts index 519e338..c76ab0b 100644 --- a/src/commands/commands_list.ts +++ b/src/commands/commands_list.ts @@ -1,4 +1,12 @@ -export const commandsListCommand = { +import type { CommandMeta, LobsterCommand } from './types.js'; + +function parseDescriptionFromHelp(helpText: string): string { + const firstLine = helpText.split('\n').find((l) => l.trim().length > 0) ?? ''; + // Expected pattern: "name — description" but fall back to the line as-is. + return firstLine.includes('—') ? firstLine.split('—').slice(1).join('—').trim() : firstLine.trim(); +} + +export const commandsListCommand: LobsterCommand = { name: 'commands.list', help() { return ( @@ -7,9 +15,14 @@ export const commandsListCommand = { ` commands.list\n\n` + `Notes:\n` + ` - Intended for agents (e.g. Clawdbot) to discover available pipeline stages dynamically.\n` + - ` - Output includes the command name and a short description extracted from help().\n` + ` - Output includes name/description plus optional metadata (argsSchema/examples/sideEffects) when provided by commands.\n` ); }, + meta: { + description: 'List available Lobster pipeline commands', + argsSchema: { type: 'object', properties: {}, required: [] }, + sideEffects: [], + } satisfies CommandMeta, async run({ input, ctx }) { // Drain input for await (const _ of input) { @@ -18,16 +31,16 @@ export const commandsListCommand = { const names = ctx.registry.list(); const output = names.map((name) => { - const cmd = ctx.registry.get(name); + const cmd = ctx.registry.get(name) as LobsterCommand | undefined; const help = typeof cmd?.help === 'function' ? String(cmd.help()) : ''; - const firstLine = help.split('\n').find((l) => l.trim().length > 0) ?? ''; - - // Expected pattern: "name — description" but fall back to the line as-is. - const desc = firstLine.includes('—') ? firstLine.split('—').slice(1).join('—').trim() : firstLine.trim(); + const description = cmd?.meta?.description ?? parseDescriptionFromHelp(help); return { name, - description: desc, + description, + argsSchema: cmd?.meta?.argsSchema ?? null, + examples: cmd?.meta?.examples ?? null, + sideEffects: cmd?.meta?.sideEffects ?? null, }; }); diff --git a/src/commands/stdlib/approve.ts b/src/commands/stdlib/approve.ts index 0872e0e..785b1b5 100644 --- a/src/commands/stdlib/approve.ts +++ b/src/commands/stdlib/approve.ts @@ -4,6 +4,19 @@ function isInteractive(stdin) { export const approveCommand = { name: 'approve', + meta: { + description: 'Require confirmation to continue', + argsSchema: { + type: 'object', + properties: { + prompt: { type: 'string', description: 'Approval prompt text', default: 'Approve?' }, + emit: { type: 'boolean', description: 'Force emit approval request + halt' }, + _: { type: 'array', items: { type: 'string' } }, + }, + required: [], + }, + sideEffects: [], + }, help() { return `approve — require confirmation to continue\n\nUsage:\n ... | approve --prompt "Send these emails?"\n ... | approve --emit --prompt "Send these emails?"\n\nModes:\n - Interactive (default): prompts on TTY and passes items through if approved.\n - Emit (--emit): returns an approval request object and stops the pipeline.\n\nNotes:\n - In tool mode (or non-interactive), this emits an approval request and halts.\n`; }, diff --git a/src/commands/stdlib/clawd_invoke.ts b/src/commands/stdlib/clawd_invoke.ts index 969cd95..7cafdec 100644 --- a/src/commands/stdlib/clawd_invoke.ts +++ b/src/commands/stdlib/clawd_invoke.ts @@ -1,5 +1,25 @@ export const clawdInvokeCommand = { name: 'clawd.invoke', + meta: { + description: 'Call a local Clawdbot tool endpoint', + argsSchema: { + type: 'object', + properties: { + url: { type: 'string', description: 'Clawdbot control URL (or CLAWD_URL)' }, + token: { type: 'string', description: 'Bearer token (or CLAWD_TOKEN)' }, + tool: { type: 'string', description: 'Tool name (e.g. message, cron, github, etc.)' }, + action: { type: 'string', description: 'Tool action' }, + 'args-json': { type: 'string', description: 'JSON string of tool args' }, + sessionKey: { type: 'string', description: 'Optional session key attribution' }, + 'session-key': { type: 'string', description: 'Alias for sessionKey' }, + dryRun: { type: 'boolean', description: 'Dry run' }, + 'dry-run': { type: 'boolean', description: 'Alias for dryRun' }, + _: { type: 'array', items: { type: 'string' } }, + }, + required: ['tool', 'action'], + }, + sideEffects: ['calls_clawd_tool'], + }, help() { return `clawd.invoke — call a local Clawdbot tool endpoint\n\n` + `Usage:\n` + diff --git a/src/commands/stdlib/diff_last.ts b/src/commands/stdlib/diff_last.ts index e0c7c7d..51a24ec 100644 --- a/src/commands/stdlib/diff_last.ts +++ b/src/commands/stdlib/diff_last.ts @@ -2,6 +2,18 @@ import { diffAndStore } from '../../state/store.js'; export const diffLastCommand = { name: 'diff.last', + meta: { + description: 'Compare current items to last stored snapshot', + argsSchema: { + type: 'object', + properties: { + key: { type: 'string', description: 'State key to diff against' }, + _: { type: 'array', items: { type: 'string' } }, + }, + required: ['key'], + }, + sideEffects: ['writes_state'], + }, help() { return `diff.last — compare current items to last stored snapshot\n\nUsage:\n | diff.last --key \n\nOutput:\n { changed, key, before, after }\n`; }, diff --git a/src/commands/stdlib/email_triage.ts b/src/commands/stdlib/email_triage.ts index 68ec0c7..8f5d71d 100644 --- a/src/commands/stdlib/email_triage.ts +++ b/src/commands/stdlib/email_triage.ts @@ -42,6 +42,18 @@ function isLikelyNoReply(from: string) { export const emailTriageCommand = { name: "email.triage", + meta: { + description: "Deterministic bucketing + summary for email messages", + argsSchema: { + type: "object", + properties: { + limit: { type: "number", description: "Maximum items to consume from input stream", default: 20 }, + _: { type: "array", items: { type: "string" } }, + }, + required: [], + }, + sideEffects: [], + }, help() { return ( `email.triage — deterministic bucketing + summary for email messages\n\n` + diff --git a/src/commands/stdlib/exec.ts b/src/commands/stdlib/exec.ts index 7dc4471..89f690f 100644 --- a/src/commands/stdlib/exec.ts +++ b/src/commands/stdlib/exec.ts @@ -2,6 +2,19 @@ import { spawn } from 'node:child_process'; export const execCommand = { name: 'exec', + meta: { + description: 'Run an OS command', + argsSchema: { + type: 'object', + properties: { + json: { type: 'boolean', description: 'Parse stdout as JSON (single value).' }, + shell: { type: 'string', description: 'Run via /bin/sh -lc with this command line.' }, + _: { type: 'array', items: { type: 'string' }, description: 'Command + args.' }, + }, + required: ['_'], + }, + sideEffects: ['local_exec'], + }, help() { return `exec — run an OS command\n\n` + `Usage:\n` + diff --git a/src/commands/stdlib/gog_gmail_search.ts b/src/commands/stdlib/gog_gmail_search.ts index b5e9b3a..c341706 100644 --- a/src/commands/stdlib/gog_gmail_search.ts +++ b/src/commands/stdlib/gog_gmail_search.ts @@ -29,6 +29,20 @@ function run(cmd: string, argv: string[], env: Record\n\nEnv:\n LOBSTER_STATE_DIR overrides storage directory\n`; }, @@ -32,6 +43,17 @@ export const stateGetCommand = { export const stateSetCommand = { name: 'state.set', + meta: { + description: 'Write a JSON value to Lobster state', + argsSchema: { + type: 'object', + properties: { + _: { type: 'array', items: { type: 'string' }, description: 'Key' }, + }, + required: ['_'], + }, + sideEffects: ['writes_state'], + }, help() { return `state.set — write a JSON value to Lobster state\n\nUsage:\n | state.set \n\nNotes:\n - Consumes the entire input stream; stores a single JSON value.\n`; }, diff --git a/src/commands/stdlib/table.ts b/src/commands/stdlib/table.ts index 17c0d36..3c76034 100644 --- a/src/commands/stdlib/table.ts +++ b/src/commands/stdlib/table.ts @@ -7,6 +7,11 @@ function stringifyCell(v) { export const tableCommand = { name: 'table', + meta: { + description: 'Render items as a simple table', + argsSchema: { type: 'object', properties: {}, required: [] }, + sideEffects: [], + }, help() { return `table — render items as a simple table\n\nUsage:\n ... | table\n\nNotes:\n - If items are objects, columns are union of keys (first 20 items).\n`; }, diff --git a/src/commands/stdlib/where.ts b/src/commands/stdlib/where.ts index 4cb6b34..1a6468d 100644 --- a/src/commands/stdlib/where.ts +++ b/src/commands/stdlib/where.ts @@ -36,6 +36,21 @@ function compare(left, op, right) { export const whereCommand = { name: 'where', + meta: { + description: 'Filter objects by a simple predicate', + argsSchema: { + type: 'object', + properties: { + _: { + type: 'array', + items: { type: 'string' }, + description: 'First positional arg is an expression like field=value or minutes>=30', + }, + }, + required: ['_'], + }, + sideEffects: [], + }, help() { return `where — filter objects by a simple predicate\n\nUsage:\n ... | where unread=true\n ... | where minutes>=30\n ... | where sender.domain==example.com\n`; }, diff --git a/src/commands/types.ts b/src/commands/types.ts new file mode 100644 index 0000000..cf8e7a1 --- /dev/null +++ b/src/commands/types.ts @@ -0,0 +1,13 @@ +export type CommandMeta = { + description?: string; + argsSchema?: unknown; + examples?: Array<{ args: Record; description?: string }>; + sideEffects?: string[]; +}; + +export type LobsterCommand = { + name: string; + help: () => string; + run: (params: any) => Promise; + meta?: CommandMeta; +}; diff --git a/src/commands/workflows/workflows_list.ts b/src/commands/workflows/workflows_list.ts index 7fea88c..fb4983f 100644 --- a/src/commands/workflows/workflows_list.ts +++ b/src/commands/workflows/workflows_list.ts @@ -2,6 +2,11 @@ import { listWorkflows } from '../../workflows/registry.js'; export const workflowsListCommand = { name: 'workflows.list', + meta: { + description: 'List available Lobster workflows', + argsSchema: { type: 'object', properties: {}, required: [] }, + sideEffects: [], + }, help() { return `workflows.list — list available Lobster workflows\n\nUsage:\n workflows.list\n\nNotes:\n - Intended for Clawdbot to discover workflows dynamically.\n`; }, diff --git a/src/commands/workflows/workflows_run.ts b/src/commands/workflows/workflows_run.ts index 6bf3e63..cd01b84 100644 --- a/src/commands/workflows/workflows_run.ts +++ b/src/commands/workflows/workflows_run.ts @@ -12,6 +12,19 @@ const recipeRunners = {}; export const workflowsRunCommand = { name: 'workflows.run', + meta: { + description: 'Run a named Lobster workflow', + argsSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Workflow name' }, + 'args-json': { type: 'string', description: 'JSON string of workflow args' }, + _: { type: 'array', items: { type: 'string' } }, + }, + required: ['name'], + }, + sideEffects: [], + }, help() { return `workflows.run — run a named Lobster workflow\n\nUsage:\n workflows.run --name [--args-json '{...}']\n\nExample:\n workflows.run --name github.pr.monitor.notify --args-json '{"repo":"clawdbot/clawdbot","pr":1152}'\n`; }, diff --git a/test/commands_list.test.ts b/test/commands_list.test.ts index 6fc16d5..8563160 100644 --- a/test/commands_list.test.ts +++ b/test/commands_list.test.ts @@ -42,4 +42,6 @@ test('commands.list returns command inventory including stdlib + workflows', asy assert.ok(self); assert.equal(typeof self.description, 'string'); assert.ok(self.description.length > 0); + // Schema should be present for commands that declare it. + assert.ok(self.argsSchema); });