feat: enrich commands.list with args schema + metadata

This commit is contained in:
Vignesh Natarajan
2026-01-22 19:05:23 -08:00
parent 1336c64c2a
commit 5d9485504a
18 changed files with 225 additions and 8 deletions
+21 -8
View File
@@ -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,
};
});
+13
View File
@@ -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`;
},
+20
View File
@@ -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` +
+12
View File
@@ -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 <items> | diff.last --key <stateKey>\n\nOutput:\n { changed, key, before, after }\n`;
},
+12
View File
@@ -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` +
+13
View File
@@ -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` +
+14
View File
@@ -29,6 +29,20 @@ function run(cmd: string, argv: string[], env: Record<string, string | undefined
export const gogGmailSearchCommand = {
name: "gog.gmail.search",
meta: {
description: "Fetch Gmail threads via gog (JSON)",
argsSchema: {
type: "object",
properties: {
query: { type: "string", description: "Gmail search query", default: "newer_than:1d" },
max: { type: "number", description: "Max results", default: 20 },
limit: { type: "number", description: "Alias for max" },
_: { type: "array", items: { type: "string" } },
},
required: [],
},
sideEffects: ['reads_email'],
},
help() {
return (
`gog.gmail.search — fetch Gmail messages via gog (JSON)\n\n` +
+13
View File
@@ -46,6 +46,19 @@ function parseDraft(item: any): Draft {
export const gogGmailSendCommand = {
name: "gog.gmail.send",
meta: {
description: "Send Gmail messages via gog",
argsSchema: {
type: "object",
properties: {
dryRun: { type: "boolean", description: "If true, do not send; echo drafts" },
"dry-run": { type: "boolean", description: "Alias for dryRun" },
_: { type: "array", items: { type: "string" } },
},
required: [],
},
sideEffects: ['sends_email'],
},
help() {
return (
`gog.gmail.send — send Gmail messages via gog\n\n` +
+12
View File
@@ -1,5 +1,17 @@
export const headCommand = {
name: 'head',
meta: {
description: 'Take first N items',
argsSchema: {
type: 'object',
properties: {
n: { type: 'number', description: 'Number of items to take', default: 10 },
_: { type: 'array', items: { type: 'string' } },
},
required: [],
},
sideEffects: [],
},
help() {
return `head — take first N items\n\nUsage:\n head --n 10\n`;
},
+5
View File
@@ -1,5 +1,10 @@
export const jsonCommand = {
name: 'json',
meta: {
description: 'Render pipeline output as JSON',
argsSchema: { type: 'object', properties: {}, required: [] },
sideEffects: [],
},
help() {
return `json — render pipeline output as JSON\n\nUsage:\n ... | json\n`;
},
+15
View File
@@ -1,5 +1,20 @@
export const pickCommand = {
name: 'pick',
meta: {
description: 'Project fields from objects',
argsSchema: {
type: 'object',
properties: {
_: {
type: 'array',
items: { type: 'string' },
description: 'First positional arg is a comma-separated list of fields',
},
},
required: ['_'],
},
sideEffects: [],
},
help() {
return `pick — project fields from objects\n\nUsage:\n ... | pick id,subject,from\n`;
},
+22
View File
@@ -4,6 +4,17 @@ import { defaultStateDir, keyToPath } from '../../state/store.js';
export const stateGetCommand = {
name: 'state.get',
meta: {
description: 'Read a JSON value from Lobster state',
argsSchema: {
type: 'object',
properties: {
_: { type: 'array', items: { type: 'string' }, description: 'Key' },
},
required: ['_'],
},
sideEffects: ['reads_state'],
},
help() {
return `state.get — read a JSON value from Lobster state\n\nUsage:\n state.get <key>\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 <value> | state.set <key>\n\nNotes:\n - Consumes the entire input stream; stores a single JSON value.\n`;
},
+5
View File
@@ -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`;
},
+15
View File
@@ -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`;
},
+13
View File
@@ -0,0 +1,13 @@
export type CommandMeta = {
description?: string;
argsSchema?: unknown;
examples?: Array<{ args: Record<string, unknown>; description?: string }>;
sideEffects?: string[];
};
export type LobsterCommand = {
name: string;
help: () => string;
run: (params: any) => Promise<any>;
meta?: CommandMeta;
};
+5
View File
@@ -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`;
},
+13
View File
@@ -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 <workflow> [--args-json '{...}']\n\nExample:\n workflows.run --name github.pr.monitor.notify --args-json '{"repo":"clawdbot/clawdbot","pr":1152}'\n`;
},
+2
View File
@@ -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);
});