diff --git a/apps/docs/content/docs/cli/billing.mdx b/apps/docs/content/docs/cli/billing.mdx index 7e9d7cf6efa..2058936e030 100644 --- a/apps/docs/content/docs/cli/billing.mdx +++ b/apps/docs/content/docs/cli/billing.mdx @@ -7,13 +7,13 @@ import { CommandTable } from '@/components/ui/command-table' Every command below also accepts the [global options](/cli/commands#global-options). -## Show billing status and current-period credit usage +## Show billing status, credits spent this period, and storage used ```bash sim billing status [options] ``` -Show billing status and current-period credit usage (credits and storage require an OAuth login or personal API key) +Show billing status, credits spent this period, and storage used (credits and storage require an OAuth login or personal API key) **Options** diff --git a/apps/docs/content/docs/cli/cli.mdx b/apps/docs/content/docs/cli/cli.mdx new file mode 100644 index 00000000000..2135fc78fb5 --- /dev/null +++ b/apps/docs/content/docs/cli/cli.mdx @@ -0,0 +1,24 @@ +--- +title: CLI +description: Discover commands in this CLI — every subcommand, argument, and flag +--- + +import { CommandTable } from '@/components/ui/command-table' + +Every command below also accepts the [global options](/cli/commands#global-options). + +## Find the command for a task, described in plain words + +```bash +sim cli search +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `query...` | Yes | What you want to do, e.g. "cancel a workflow run" | + + diff --git a/apps/docs/content/docs/cli/commands.mdx b/apps/docs/content/docs/cli/commands.mdx index e63345e7a0f..85c9eb0da58 100644 --- a/apps/docs/content/docs/cli/commands.mdx +++ b/apps/docs/content/docs/cli/commands.mdx @@ -32,6 +32,7 @@ These apply to every command, and may be written before or after it. | --- | --- | | [`sim profiles`](/cli/profiles) | List profiles or add a workspace profile that shares a stored login | | [`sim telemetry`](/cli/telemetry) | Control anonymous usage reporting | +| [`sim cli`](/cli/cli) | Discover commands in this CLI | | [`sim audit-logs`](/cli/audit-logs) | Manage audit logs | | [`sim billing`](/cli/billing) | Manage billing | | [`sim blocks`](/cli/blocks) | Manage blocks | @@ -56,7 +57,7 @@ These apply to every command, and may be written before or after it. | [`sim workflows`](/cli/workflows) | Manage workflows | | [`sim workspaces`](/cli/workspaces) | Manage workspaces | -## Sign in through the browser and store the login for the profile +## Log in through the browser and store the login for the profile ```bash sim login [options] @@ -108,7 +109,7 @@ sim whoami [options] -## Set a profile's endpoint, default workspace, or output format +## Switch a profile's default workspace, or set its endpoint or output format ```bash sim configure [options] diff --git a/apps/docs/content/docs/cli/configuration.mdx b/apps/docs/content/docs/cli/configuration.mdx index 7963852f478..630e23f7090 100644 --- a/apps/docs/content/docs/cli/configuration.mdx +++ b/apps/docs/content/docs/cli/configuration.mdx @@ -40,7 +40,7 @@ sim profile add acme --workspace 7e2d9c14-6b83-4a55-8f01-c4d3e9a76b28 ```bash sim configure --set-endpoint http://localhost:3000 --profile dev sim configure --set-workspace 5c81f3a6-0e27-4b94-8d15-a7f60c39b2e8 --profile dev -sim configure --set-output json +sim configure --set-output table ``` | Option | What it sets | @@ -64,7 +64,7 @@ Each setting resolves independently, and the first match wins: | 1 | Command-line flag — `--endpoint`, `--workspace`, `--output` | | 2 | Environment — `SIM_ENDPOINT`, `SIM_API_KEY`, `SIM_WORKSPACE`, `SIM_OUTPUT` | | 3 | `~/.sim/config` for the selected profile and `~/.sim/credentials` for its `auth_profile`, when set | -| 4 | Built-in default — `https://www.sim.ai` and `table` | +| 4 | Built-in default — `https://www.sim.ai` and `json` | `sim whoami` prints the winning source for each setting: diff --git a/apps/docs/content/docs/cli/files.mdx b/apps/docs/content/docs/cli/files.mdx index aad0f887ff0..b79f79045f5 100644 --- a/apps/docs/content/docs/cli/files.mdx +++ b/apps/docs/content/docs/cli/files.mdx @@ -646,7 +646,7 @@ sim files upload [options] -## Get a file’s content +## Download a file’s content to stdout or a local file ```bash sim files get [options] diff --git a/apps/docs/content/docs/cli/index.mdx b/apps/docs/content/docs/cli/index.mdx index cf2b958cdef..f9996d4c3f9 100644 --- a/apps/docs/content/docs/cli/index.mdx +++ b/apps/docs/content/docs/cli/index.mdx @@ -7,9 +7,10 @@ import { Step, Steps } from 'fumadocs-ui/components/steps' import { Tab, Tabs } from 'fumadocs-ui/components/tabs' `sim` is the command line for Sim. Sign in once, then run workflows, query tables, -move files, search knowledge bases, and read run logs from the terminal. Use -`--output json` for structured results that pipe into `jq`, cron jobs, and CI -pipelines. See [Output formats](/cli/output) for commands that emit raw content +move files, search knowledge bases, and read run logs from the terminal. Results +are JSON by default, so they pipe into `jq`, cron jobs, CI pipelines, and coding +agents; use `--output table` to read them as tables. Don't know a command? Run +`sim cli search ""`. See [Output formats](/cli/output) for commands that emit raw content or local configuration. ## Install diff --git a/apps/docs/content/docs/cli/knowledge.mdx b/apps/docs/content/docs/cli/knowledge.mdx index 01a792b744b..e449824c7b9 100644 --- a/apps/docs/content/docs/cli/knowledge.mdx +++ b/apps/docs/content/docs/cli/knowledge.mdx @@ -455,7 +455,7 @@ Enable or disable every matching document (OAuth login or personal API key requi -## Delete document +## Delete a document from a knowledge base ```bash sim knowledge documents delete [options] diff --git a/apps/docs/content/docs/cli/logs.mdx b/apps/docs/content/docs/cli/logs.mdx index 4b35e0db28b..62b302d9781 100644 --- a/apps/docs/content/docs/cli/logs.mdx +++ b/apps/docs/content/docs/cli/logs.mdx @@ -60,7 +60,7 @@ sim logs stats [options] -## List logs +## List run logs, such as failed or errored runs, by workflow, trigger or time ```bash sim logs list [options] @@ -100,7 +100,7 @@ sim logs list [options] -## Watch runs as they arrive, printing each new run once +## Watch runs live as they arrive, printing each new run once ```bash sim logs follow [options] diff --git a/apps/docs/content/docs/cli/meta.json b/apps/docs/content/docs/cli/meta.json index 0ba739e7a34..14ef23a60dd 100644 --- a/apps/docs/content/docs/cli/meta.json +++ b/apps/docs/content/docs/cli/meta.json @@ -15,6 +15,7 @@ "commands", "profiles", "telemetry", + "cli", "audit-logs", "billing", "blocks", diff --git a/apps/docs/content/docs/cli/output.mdx b/apps/docs/content/docs/cli/output.mdx index 234149a1a5d..67df6820722 100644 --- a/apps/docs/content/docs/cli/output.mdx +++ b/apps/docs/content/docs/cli/output.mdx @@ -7,16 +7,16 @@ Commands that return structured API data support four output formats. | Format | For | | --- | --- | -| `table` | reading (default) | -| `json` | piping into `jq` | +| `json` | agents, scripts, and piping into `jq` (default) | +| `table` | reading | | `yaml` | piping into anything that reads YAML | | `text` | shell loops — tab-separated, no header, no colour | Select one per command, save it to the profile, or set it in the environment: ```bash -sim tables get tbl_9f3c1a05d4b7426e8c2f0917ab35de64 --output json -sim configure --set-output json +sim tables get tbl_9f3c1a05d4b7426e8c2f0917ab35de64 --output table +sim configure --set-output table SIM_OUTPUT=yaml sim logs list > logs.yaml ``` diff --git a/apps/docs/content/docs/cli/reference.mdx b/apps/docs/content/docs/cli/reference.mdx index 459a37af741..40f326941b0 100644 --- a/apps/docs/content/docs/cli/reference.mdx +++ b/apps/docs/content/docs/cli/reference.mdx @@ -26,7 +26,7 @@ These apply to every command, and may be written before or after it. ## sim login -Sign in through the browser and store the login for the profile +Log in through the browser and store the login for the profile ```bash sim login [options] @@ -84,7 +84,7 @@ sim whoami [options] ## sim configure -Set a profile's endpoint, default workspace, or output format +Switch a profile's default workspace, or set its endpoint or output format ```bash sim configure [options] @@ -215,6 +215,26 @@ Turn usage reporting off for this machine sim telemetry disable ``` +## sim cli + +### sim cli search + +Find the command for a task, described in plain words + +```bash +sim cli search +``` + +**Arguments** + + + +| Argument | Required | Description | +| --- | --- | --- | +| `query...` | Yes | What you want to do, e.g. "cancel a workflow run" | + + + ## sim audit-logs Also spelled `sim audit-log`. @@ -280,7 +300,7 @@ sim audit-logs list [options] ### sim billing status -Show billing status and current-period credit usage (credits and storage require an OAuth login or personal API key) +Show billing status, credits spent this period, and storage used (credits and storage require an OAuth login or personal API key) ```bash sim billing status [options] @@ -1424,7 +1444,7 @@ sim files upload [options] ### sim files get -Get a file’s content +Download a file’s content to stdout or a local file ```bash sim files get [options] @@ -1952,7 +1972,7 @@ sim knowledge documents batch-update [options] ### sim knowledge documents delete -Delete Document +Delete a document from a knowledge base ```bash sim knowledge documents delete [options] @@ -2764,7 +2784,7 @@ sim logs stats [options] ### sim logs list -List Logs +List run logs, such as failed or errored runs, by workflow, trigger or time ```bash sim logs list [options] @@ -2806,7 +2826,7 @@ sim logs list [options] ### sim logs follow -Watch runs as they arrive, printing each new run once +Watch runs live as they arrive, printing each new run once ```bash sim logs follow [options] @@ -4074,7 +4094,7 @@ sim secrets list [options] ### sim secrets set -Create or replace a named secret (OAuth login or personal API key required) +Create or replace a named secret, such as an API key or environment variable (OAuth login or personal API key required) ```bash sim secrets set [options] @@ -4763,7 +4783,7 @@ sim tables rows list [options] ### sim tables rows query -Query Rows +Query rows with a filter and sort ```bash sim tables rows query [options] @@ -6048,7 +6068,7 @@ sim workflows activate create [options] ### sim workflows operations apply -Apply Workflow Operations (OAuth login or personal API key required) +Edit a workflow’s blocks, connections and settings with a batch of operations (OAuth login or personal API key required) ```bash sim workflows operations apply [options] diff --git a/apps/docs/content/docs/cli/secrets.mdx b/apps/docs/content/docs/cli/secrets.mdx index 67ce8e273eb..2b6d70dc1f5 100644 --- a/apps/docs/content/docs/cli/secrets.mdx +++ b/apps/docs/content/docs/cli/secrets.mdx @@ -60,13 +60,13 @@ List Secrets (OAuth login or personal API key required) -## Create or replace a named secret +## Create or replace a named secret, such as an API key or environment variable ```bash sim secrets set [options] ``` -Create or replace a named secret (OAuth login or personal API key required) +Create or replace a named secret, such as an API key or environment variable (OAuth login or personal API key required) **Arguments** diff --git a/apps/docs/content/docs/cli/tables.mdx b/apps/docs/content/docs/cli/tables.mdx index 3f92acd7992..79f0a440134 100644 --- a/apps/docs/content/docs/cli/tables.mdx +++ b/apps/docs/content/docs/cli/tables.mdx @@ -382,7 +382,7 @@ sim tables rows list [options] -## Query rows +## Query rows with a filter and sort ```bash sim tables rows query [options] diff --git a/apps/docs/content/docs/cli/workflows.mdx b/apps/docs/content/docs/cli/workflows.mdx index f2c37240f5e..9a98eb0f113 100644 --- a/apps/docs/content/docs/cli/workflows.mdx +++ b/apps/docs/content/docs/cli/workflows.mdx @@ -38,13 +38,13 @@ Activate Workflow Version (OAuth login or personal API key required) -## Apply workflow operations +## Edit a workflow’s blocks, connections and settings with a batch of operations ```bash sim workflows operations apply [options] ``` -Apply Workflow Operations (OAuth login or personal API key required) +Edit a workflow’s blocks, connections and settings with a batch of operations (OAuth login or personal API key required) **Arguments** diff --git a/apps/sim/lib/mothership/generated/docs-manifest.ts b/apps/sim/lib/mothership/generated/docs-manifest.ts index 0b2d18c507b..59ca1a8326d 100644 --- a/apps/sim/lib/mothership/generated/docs-manifest.ts +++ b/apps/sim/lib/mothership/generated/docs-manifest.ts @@ -28,6 +28,7 @@ export const DOCS_MANIFEST: readonly string[] = [ 'cli/billing.mdx', 'cli/blocks.mdx', 'cli/chat-deployments.mdx', + 'cli/cli.mdx', 'cli/commands.mdx', 'cli/configuration.mdx', 'cli/connector-types.mdx', diff --git a/bun.lock b/bun.lock index 1a944bffe0e..7ca10cbf07c 100644 --- a/bun.lock +++ b/bun.lock @@ -656,7 +656,7 @@ }, "packages/sim-cli": { "name": "sim", - "version": "2.1.2", + "version": "2.2.0", "bin": { "sim": "dist/index.js", }, @@ -664,6 +664,7 @@ "chalk": "5.6.2", "commander": "^11.1.0", "js-yaml": "4.3.2", + "minisearch": "7.2.0", "proper-lockfile": "4.1.2", }, "devDependencies": { @@ -3859,6 +3860,8 @@ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "minisearch": ["minisearch@7.2.0", "", {}, "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg=="], + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], "mixpanel": ["mixpanel@0.18.1", "", { "dependencies": { "https-proxy-agent": "5.0.0" } }, "sha512-YD1xfn6WP6ZLQ6Pmgh0KgdXhueJEsrodThMTsHzHMH0VbWa9ck8s+ynDtM83OSgt+yQ61W/SQNrH8Y4wIwocGg=="], diff --git a/packages/sim-cli/README.md b/packages/sim-cli/README.md index 366264388b4..902e19c75a6 100644 --- a/packages/sim-cli/README.md +++ b/packages/sim-cli/README.md @@ -166,7 +166,7 @@ is saved with the profile. sim profiles sim configure --profile work sim configure --profile work --set-workspace -sim configure --profile work --set-output json +sim configure --profile work --set-output table sim configure --profile local --set-endpoint http://localhost:3000 sim whoami --profile work ``` @@ -198,6 +198,17 @@ sim workflows --help sim tables rows query --help ``` +Or describe the task and let the CLI find the command. Search ranks the commands +this version ships, locally; your query is never sent anywhere: + +```bash +sim cli search "cancel a running workflow" +sim --output table cli search list table rows +``` + +It returns the five best matches. When a coding agent runs the CLI, root and +group `--help` open with a note pointing it to `sim cli search`. + The commands you will use most often are: | Task | Command | @@ -242,16 +253,20 @@ argument, and flag. ## JSON input and output -Human-readable tables are the default. Use JSON or YAML when another program -will consume the result, and `text` for tab-separated shell output: +JSON is the default, so agents and scripts can parse every result directly. +Use `table` for aligned human-readable output, YAML if you prefer it, and `text` +for tab-separated shell output: ```bash -sim workflows list --output json -sim logs list --output json | jq -r '.data[].runId' +sim workflows list --output table +sim logs list | jq -r '.data[].runId' SIM_OUTPUT=yaml sim tables get -sim configure --set-output json +sim configure --set-output table ``` +Scripts that must not depend on a profile's saved format can still pass +`--output json` explicitly. + Paginated lists return `{ "data": [...], "nextCursor": "..." }` in JSON and YAML. `nextCursor` is `null` when no pages remain. Resource lists and directory `ls` fetch every page by default; use `--limit N` to cap them. Table rows (including diff --git a/packages/sim-cli/THIRD_PARTY_LICENSES b/packages/sim-cli/THIRD_PARTY_LICENSES index 33ea71bba06..d8eff820553 100644 --- a/packages/sim-cli/THIRD_PARTY_LICENSES +++ b/packages/sim-cli/THIRD_PARTY_LICENSES @@ -9,6 +9,9 @@ Copyright (c) 2011 TJ Holowaychuk js-yaml Copyright (C) 2011-2015 by Vitaly Puzrin +minisearch +Copyright 2022 Luca Ongaro + proper-lockfile Copyright (c) 2018 Made With MOXY Lda diff --git a/packages/sim-cli/package.json b/packages/sim-cli/package.json index ce8771e59e5..303dac51d08 100644 --- a/packages/sim-cli/package.json +++ b/packages/sim-cli/package.json @@ -1,6 +1,6 @@ { "name": "sim", - "version": "2.1.2", + "version": "2.2.0", "description": "Sim CLI - talk to the Sim API from your terminal", "type": "module", "imports": { @@ -53,6 +53,7 @@ "chalk": "5.6.2", "commander": "^11.1.0", "js-yaml": "4.3.2", + "minisearch": "7.2.0", "proper-lockfile": "4.1.2" }, "devDependencies": { diff --git a/packages/sim-cli/src/commands/auth.test.ts b/packages/sim-cli/src/commands/auth.test.ts index 395b15db743..b10df840471 100644 --- a/packages/sim-cli/src/commands/auth.test.ts +++ b/packages/sim-cli/src/commands/auth.test.ts @@ -88,12 +88,14 @@ vi.mock('../auth/oauth-flow', () => ({ vi.mock('../config/index', async () => ({ ...(await import('../config/profile').then( ({ + DEFAULT_OUTPUT_FORMAT, FORBIDDEN_IN_VALUE, normalizeWorkspaceId, OUTPUT_FORMATS, ProfileConfigError, validateProfileName, }) => ({ + DEFAULT_OUTPUT_FORMAT, FORBIDDEN_IN_VALUE, normalizeWorkspaceId, OUTPUT_FORMATS, diff --git a/packages/sim-cli/src/commands/auth.ts b/packages/sim-cli/src/commands/auth.ts index 8ac387846fa..07971d206ee 100644 --- a/packages/sim-cli/src/commands/auth.ts +++ b/packages/sim-cli/src/commands/auth.ts @@ -19,6 +19,7 @@ import { import { configPath, credentialsPath, + DEFAULT_OUTPUT_FORMAT, DEFAULT_PROFILE, deleteProfile, FORBIDDEN_IN_VALUE, @@ -533,7 +534,7 @@ async function loginWithOAuth( export function loginCommand(): Command { return new Command('login') - .description('Sign in through the browser and store the login for the profile') + .description('Log in through the browser and store the login for the profile') .addOption( new Option( '--method ', @@ -1108,14 +1109,14 @@ function profileListingContext(command: Command): { activeName: string; output: if (named && named !== DEFAULT_PROFILE && !listProfiles().includes(named)) throw error // A bad format is the caller's own request, not a broken profile: falling - // back to a table would hand a script human output with exit 0. Only the + // back to the default would hand a script output it did not ask for with exit 0. Only the // profile's *resolution* is tolerated here, never its arguments. const requested = globals.output ?? process.env.SIM_OUTPUT if (requested && !(OUTPUT_FORMATS as readonly string[]).includes(requested)) throw error return { activeName: named || DEFAULT_PROFILE, - output: requested ? (requested as OutputFormat) : 'table', + output: requested ? (requested as OutputFormat) : DEFAULT_OUTPUT_FORMAT, } } } diff --git a/packages/sim-cli/src/commands/configure.ts b/packages/sim-cli/src/commands/configure.ts index 39b3dd64d03..eec3f9009ec 100644 --- a/packages/sim-cli/src/commands/configure.ts +++ b/packages/sim-cli/src/commands/configure.ts @@ -78,7 +78,7 @@ function quoteProfileArgument(name: string): string { export function configureCommand(): Command { return new Command('configure') - .description("Set a profile's endpoint, default workspace, or output format") + .description("Switch a profile's default workspace, or set its endpoint or output format") .option('--set-endpoint ', 'Sim deployment to talk to') .option('--set-workspace ', 'Default workspace for workspace-scoped commands') .option('--set-output ', `Default output format (${OUTPUT_FORMATS.join(' | ')})`) diff --git a/packages/sim-cli/src/commands/protocol/files-get.ts b/packages/sim-cli/src/commands/protocol/files-get.ts index 588fb024a25..be36e1feaec 100644 --- a/packages/sim-cli/src/commands/protocol/files-get.ts +++ b/packages/sim-cli/src/commands/protocol/files-get.ts @@ -340,7 +340,7 @@ export function attachFileGet(files: Command): void { .command('get') .argument('', 'File whose content to read') .allowExcessArguments(false) - .description('Get a file’s content') + .description('Download a file’s content to stdout or a local file') .option('-o, --output-file ', 'Write content to a file instead of stdout') .option('--force', 'Overwrite --output-file if it already exists') .action((fileId: string, options: DownloadOutputOptions, command: Command) => diff --git a/packages/sim-cli/src/commands/protocol/logs-follow.ts b/packages/sim-cli/src/commands/protocol/logs-follow.ts index 9e6e7bc807b..d0d374dac87 100644 --- a/packages/sim-cli/src/commands/protocol/logs-follow.ts +++ b/packages/sim-cli/src/commands/protocol/logs-follow.ts @@ -499,7 +499,7 @@ function inSeconds(ms: number): number { export function attachLogsFollow(logs: Command): void { logs .command('follow') - .description('Watch runs as they arrive, printing each new run once') + .description('Watch runs live as they arrive, printing each new run once') .option('--workflow ', 'Only follow runs of this workflow (repeatable)', collect, []) .option( '--folder ', diff --git a/packages/sim-cli/src/commands/search.test.ts b/packages/sim-cli/src/commands/search.test.ts new file mode 100644 index 00000000000..a86bfead085 --- /dev/null +++ b/packages/sim-cli/src/commands/search.test.ts @@ -0,0 +1,129 @@ +/** + * @vitest-environment node + */ +import { Command } from 'commander' +import { describe, expect, it } from 'vitest' +import { buildProgram } from '../program' +import { searchCommands } from './search' + +function searchCommandOf(program: Command): Command { + const search = program.commands + .find((command) => command.name() === 'cli') + ?.commands.find((command) => command.name() === 'search') + if (!search) throw new Error('cli search is not registered') + return search +} + +function searchReal(query: string) { + const program = buildProgram() + return searchCommands(program, query, searchCommandOf(program)) +} + +describe('sim cli search', () => { + it.each([ + ['cancel a running workflow', 'sim workflows runs cancel'], + ['add a column to a table', 'sim tables columns create'], + ['who am i logged in as', 'sim whoami'], + ['delete a knowledge base document', 'sim knowledge documents delete'], + ['workflw deploy', 'sim workflows deploy'], + ['store an environment variable', 'sim secrets set'], + ['switch workspace', 'sim configure'], + ['edit workflow blocks', 'sim workflows operations apply'], + ['how much have i spent', 'sim billing status'], + ])('ranks the intended command first for "%s"', (query, expected) => { + expect(searchReal(query)[0]?.command).toBe(expected) + }) + + it('returns at most five compact matches', () => { + const results = searchReal('workflow') + + expect(results).toHaveLength(5) + for (const result of results) expect(Object.keys(result).sort()).toEqual(['command', 'summary']) + }) + + it('never offers itself, or its group as if it were a command', () => { + const commands = searchReal('cli search find command task').map((r) => r.command) + + expect(commands).not.toContain('sim cli search') + expect(commands).not.toContain('sim cli') + }) + + /** Prefix matching on `a` or `i` would otherwise match most of the index. */ + it('finds nothing for a query made only of filler words', () => { + expect(searchReal('how do i')).toEqual([]) + }) + + it('indexes leaves only, skipping hidden commands and flags', () => { + const program = new Command('sim') + const group = program.command('widgets').description('Manage widgets') + group.command('list').description('List widgets') + group.command('purge', { hidden: true }).description('Purge widgets') + group + .command('rename') + .description('Rename a widget') + .addOption(new Command().createOption('--frobnicate', 'Secret knob').hideHelp()) + + expect(searchCommands(program, 'purge', new Command())).toEqual([]) + expect(searchCommands(program, 'frobnicate', new Command())).toEqual([]) + expect(searchCommands(program, 'widgets', new Command()).map((r) => r.command)).toEqual( + expect.arrayContaining(['sim widgets list', 'sim widgets rename']) + ) + expect(searchCommands(program, 'manage', new Command()).map((r) => r.command)).not.toContain( + 'sim widgets' + ) + }) +}) + +describe('a trailing qualifier', () => { + it('is shown but not ranked', () => { + const program = new Command('sim') + const widgets = program.command('widgets') + widgets + .command('delete') + .description('Delete a widget (OAuth login or personal API key required)') + widgets.command('read').description('Read a widget') + + expect(searchCommands(program, 'login', new Command())).toEqual([]) + expect(searchCommands(program, 'delete widget', new Command())[0]).toEqual({ + command: 'sim widgets delete', + summary: 'Delete a widget (OAuth login or personal API key required)', + }) + }) +}) + +describe('the agent discovery note', () => { + function helpOf(argv: string[], env: NodeJS.ProcessEnv): string { + const program = buildProgram({ env }) + let out = '' + const capture = (command: Command) => { + command.exitOverride() + command.configureOutput({ + writeOut: (text) => { + out += text + }, + writeErr: () => {}, + }) + command.commands.forEach(capture) + } + capture(program) + try { + program.parse(['node', 'sim', ...argv]) + } catch {} + return out + } + + it('leads root and group help when an agent runs the CLI', () => { + expect(helpOf(['--help'], { CLAUDECODE: '1' })).toMatch(/^=== AGENT COMMAND DISCOVERY ===/) + expect(helpOf(['workflows', '--help'], { CLAUDECODE: '1' })).toContain('sim cli search') + }) + + it('stays off leaf help, where the command is already found', () => { + expect(helpOf(['workflows', 'runs', 'get', '--help'], { CLAUDECODE: '1' })).not.toContain( + 'AGENT COMMAND DISCOVERY' + ) + }) + + it('is never shown to a person', () => { + expect(helpOf(['--help'], {})).not.toContain('AGENT COMMAND DISCOVERY') + }) +}) diff --git a/packages/sim-cli/src/commands/search.ts b/packages/sim-cli/src/commands/search.ts new file mode 100644 index 00000000000..ff96f43d52f --- /dev/null +++ b/packages/sim-cli/src/commands/search.ts @@ -0,0 +1,153 @@ +import { Command } from 'commander' +import MiniSearch from 'minisearch' +import { profileFrom } from '../context' +import { type Column, printList } from '../output/render' + +/** Enough to pick from without flooding an agent's context. */ +const MAX_RESULTS = 5 + +/** + * A command's name is the strongest signal and its flags the weakest: a flag + * description mentions other resources in passing ("Workflow ID"), so weighting + * it heavily would rank every command that takes `--workflow` as a workflow + * command. + */ +const FIELD_BOOST = { command: 8, summary: 5, context: 0.5 } as const + +/** + * Filler in a plain-language query. Left in, prefix matching turns `a` or `i` + * into a match on every word that starts with that letter, burying the one + * content word that names the command. + */ +const STOP_WORDS: ReadonlySet = new Set( + 'a am an and are as at be by can do does for from how i in into is it me my of on or so that the this to what when where which why with you your'.split( + ' ' + ) +) + +/** Shorter terms prefix- or fuzzy-match too much of the index to mean anything. */ +const MIN_EXPANDED_TERM_LENGTH = 3 +const MIN_FUZZY_TERM_LENGTH = 5 + +const SEARCH_OPTIONS = { + boost: FIELD_BOOST, + processTerm: (term: string) => { + const lower = term.toLowerCase() + return STOP_WORDS.has(lower) ? null : lower + }, + prefix: (term: string) => term.length >= MIN_EXPANDED_TERM_LENGTH, + fuzzy: (term: string) => (term.length >= MIN_FUZZY_TERM_LENGTH ? 0.2 : false), +} + +/** + * A trailing qualifier says how a command behaves, not what it does. Indexed, + * "(OAuth login or personal API key required)" — stamped on dozens of commands — + * made each of them a strong match for "log in" or "api key". + */ +const TRAILING_PARENTHETICAL = /\s*\([^)]*\)\s*$/ + +interface SearchDocument { + command: string + /** The summary as printed, qualifier included. */ + display: string + /** The summary with its trailing qualifier removed, so only what the command does is ranked. */ + summary: string + context: string +} + +export interface SearchResult { + command: string + summary: string +} + +const COLUMNS: Column[] = [ + { header: 'command', value: (result) => result.command }, + { header: 'summary', value: (result) => result.summary }, +] + +/** Commander records a hidden command on a private field and offers no getter. */ +function isHidden(command: Command): boolean { + return (command as Command & { _hidden?: boolean })._hidden === true +} + +function visibleSubcommands(command: Command): Command[] { + return command.commands.filter((child) => child.name() !== 'help' && !isHidden(child)) +} + +/** + * One document per runnable command, read from the same tree the terminal + * parses, so the index can never name a command that does not exist or miss a + * hand-written one. + */ +function collectDocuments( + command: Command, + path: string[], + ancestors: string[], + exclude: Command +): SearchDocument[] { + const children = visibleSubcommands(command) + if (children.length === 0) { + const display = command.description() + return [ + { + command: ['sim', ...path].join(' '), + display, + summary: display.replace(TRAILING_PARENTHETICAL, ''), + context: [ + ...ancestors, + ...command.registeredArguments.map((argument) => + `${argument.name()} ${argument.description}`.trim() + ), + ...command.options + .filter((option) => !option.hidden) + .map((option) => `${option.long ?? option.short ?? ''} ${option.description}`.trim()), + ].join(' '), + }, + ] + } + const nextAncestors = path.length > 0 ? [...ancestors, command.description()] : ancestors + return children + .filter((child) => child !== exclude) + .flatMap((child) => collectDocuments(child, [...path, child.name()], nextAncestors, exclude)) +} + +/** Ranks every command in `program` against a plain-language query, best first. */ +export function searchCommands(program: Command, query: string, exclude: Command): SearchResult[] { + const index = new MiniSearch({ + fields: ['command', 'summary', 'context'], + storeFields: ['command', 'display'], + idField: 'command', + }) + index.addAll(collectDocuments(program, [], [], exclude)) + return index + .search(query, SEARCH_OPTIONS) + .slice(0, MAX_RESULTS) + .map((hit) => ({ command: hit.command as string, summary: hit.display as string })) +} + +export function searchCommand(): Command { + return new Command('search') + .description('Find the command for a task, described in plain words') + .argument('', 'What you want to do, e.g. "cancel a workflow run"') + .addHelpText( + 'after', + ` +Ranks the commands this CLI ships, locally; your query is never sent anywhere. +Returns the ${MAX_RESULTS} best matches. Run \` --help\` on one for its flags. + +Examples: + $ sim cli search "cancel a running workflow" + $ sim cli search list table rows` + ) + .action((words: string[], _options: unknown, command: Command) => { + let program = command + while (program.parent) program = program.parent + const results = searchCommands(program, words.join(' '), command) + printList(profileFrom(command).output, results, COLUMNS) + }) +} + +/** Commands about the CLI itself rather than a Sim resource. */ +export function cliCommand(): Command { + return new Command('cli').description('Discover commands in this CLI').addCommand(searchCommand()) +} diff --git a/packages/sim-cli/src/commands/secrets.ts b/packages/sim-cli/src/commands/secrets.ts index 2fba85fe330..6d25662663a 100644 --- a/packages/sim-cli/src/commands/secrets.ts +++ b/packages/sim-cli/src/commands/secrets.ts @@ -164,7 +164,12 @@ export function attachSecretCommands(program: Command): void { secrets .command('set') .argument('', 'Secret name, as referenced in workflows') - .description(describeOperation(V2_OPERATIONS.setSecret, 'Create or replace a named secret')) + .description( + describeOperation( + V2_OPERATIONS.setSecret, + 'Create or replace a named secret, such as an API key or environment variable' + ) + ) .addOption( new Option('--scope ', 'Secret ownership scope (required)') .choices([...SECRET_SCOPES]) diff --git a/packages/sim-cli/src/config/index.ts b/packages/sim-cli/src/config/index.ts index 7679ce4ba79..f2fd85174d2 100644 --- a/packages/sim-cli/src/config/index.ts +++ b/packages/sim-cli/src/config/index.ts @@ -1,6 +1,7 @@ export { configDir, configPath, credentialsPath, telemetryStatePath } from './paths' export { DEFAULT_ENDPOINT, + DEFAULT_OUTPUT_FORMAT, DEFAULT_PROFILE, deleteProfile, FORBIDDEN_IN_VALUE, diff --git a/packages/sim-cli/src/config/profile.ts b/packages/sim-cli/src/config/profile.ts index 3ee5a442040..7e8ca957fdb 100644 --- a/packages/sim-cli/src/config/profile.ts +++ b/packages/sim-cli/src/config/profile.ts @@ -49,6 +49,13 @@ export const DEFAULT_ENDPOINT = 'https://www.sim.ai' export const OUTPUT_FORMATS = ['table', 'json', 'yaml', 'text'] as const export type OutputFormat = (typeof OUTPUT_FORMATS)[number] +/** + * JSON unless the flag, `SIM_OUTPUT`, or the profile says otherwise: agents and + * scripts are the main callers and parse it directly, and a person who prefers + * tables saves that once with `sim configure --set-output table`. + */ +export const DEFAULT_OUTPUT_FORMAT: OutputFormat = 'json' + export { FORBIDDEN_IN_VALUE, ProfileConfigError } from './ini' /** {@link FORBIDDEN_IN_VALUE}, for redacting every match out of an error message. */ @@ -788,7 +795,7 @@ export function resolveProfile(overrides: ProfileOverrides = {}): ResolvedProfil ['env', process.env.SIM_OUTPUT], ['config', config.output], ], - 'table', + DEFAULT_OUTPUT_FORMAT, 'default' ) if (!(OUTPUT_FORMATS as readonly string[]).includes(output.value as string)) { diff --git a/packages/sim-cli/src/contract/commands.ts b/packages/sim-cli/src/contract/commands.ts index b20b7352028..9c1d3aa204e 100644 --- a/packages/sim-cli/src/contract/commands.ts +++ b/packages/sim-cli/src/contract/commands.ts @@ -153,7 +153,7 @@ export const CLI_CONTRACT: CliContract = { // fields otherwise render as an unexplained em-dash for exactly the key // most people run the CLI with. describe: - 'Show billing status and current-period credit usage (credits and storage require an OAuth login or personal API key)', + 'Show billing status, credits spent this period, and storage used (credits and storage require an OAuth login or personal API key)', fields: [ { header: 'plan' }, { header: 'status' }, @@ -332,6 +332,7 @@ export const CLI_CONTRACT: CliContract = { 'This archives the knowledge base and every document in it; restore with `knowledge restore`.', }, deleteKnowledgeDocument: { + describe: 'Delete a document from a knowledge base', pathArgumentNames: KNOWLEDGE_BASE_PATH_ARGUMENT, confirm: 'This deletes the document and its embeddings.', }, @@ -376,6 +377,7 @@ export const CLI_CONTRACT: CliContract = { // ─── Fields whose type misdescribes their meaning ───────────────────────── // `z.string()` that the route splits on commas. No generator can infer this. listLogs: { + describe: 'List run logs, such as failed or errored runs, by workflow, trigger or time', flags: { ...LOG_LIST_FILTER_FLAGS, // The `workflow` column below reads `workflow.name`, which the API only @@ -530,6 +532,7 @@ export const CLI_CONTRACT: CliContract = { }, applyWorkflowOperations: { command: 'workflows operations apply', + describe: 'Edit a workflow’s blocks, connections and settings with a batch of operations', confirm: 'This edits the draft graph: the batch adds, edits, or deletes blocks and their edges as written.', flags: { @@ -664,6 +667,7 @@ export const CLI_CONTRACT: CliContract = { }, queryRows: { command: 'tables rows query', + describe: 'Query rows with a filter and sort', flags: { predicate: { name: 'filter', json: true, describe: TABLE_READ_FILTER_HELP }, sort: { json: true, describe: TABLE_SORT_HELP }, diff --git a/packages/sim-cli/src/program.ts b/packages/sim-cli/src/program.ts index cbea07b2fa9..3d208e7a6f5 100644 --- a/packages/sim-cli/src/program.ts +++ b/packages/sim-cli/src/program.ts @@ -4,6 +4,7 @@ import { loginCommand, logoutCommand, profilesCommand, whoamiCommand } from './c import { configureCommand } from './commands/configure' import { attachCredentialCommands } from './commands/credentials' import { attachProtocolCommands } from './commands/protocol/index' +import { cliCommand } from './commands/search' import { attachSecretCommands } from './commands/secrets' import { telemetryCommand } from './commands/telemetry' import { OUTPUT_FORMATS } from './config/index' @@ -12,6 +13,7 @@ import { buildGeneratedCommands, refuseHelpAfterUnknownCommand, } from './runtime/build' +import { detectCodingAgent } from './telemetry/coding-agent' import { announceUpdateIfAvailable } from './update/check' import { cliVersion } from './version' @@ -29,13 +31,14 @@ or custom-tool ID can open with a dash, which reads as a flag; put -- in front of it, as in sim audit-logs get -- -HlDcD1z76nK6R4crsUp0. Examples: + $ sim cli search "cancel a workflow run" Find the command for a task $ sim login Authorize the default profile $ sim login --profile dev --endpoint http://localhost:3000 $ sim profile add acme --workspace 7e2d9c14-6b83-4a55-8f01-c4d3e9a76b28 $ sim workflows list $ sim logs list --level error --limit 20 - $ sim configure --set-output json Save a profile output default - $ sim --output json tables get tbl_9f3c1a05d4b7426e8c2f0917ab35de64 + $ sim configure --set-output table Prefer tables over the JSON default + $ sim --output table tables get tbl_9f3c1a05d4b7426e8c2f0917ab35de64 $ sim knowledge search --query "refund policy" --kb 4c1b7f60-2d55-4a3e-9c18-70b6ea2f9d31 $ sim workflows export 3a9e21d8-5f47-4c0b-b2ea-91d7c6034ef8 > wf.json $ sim workflows import --workflow @wf.json @@ -43,6 +46,22 @@ Examples: $ sim whoami --profile dev ` +/** + * Printed above group-level help when a coding agent runs the CLI. + * + * Agents otherwise map the surface by walking `--help` down every group, which + * costs a call and a screenful of context per level. Leaf help is left alone: + * an agent reading it has already found its command. + */ +export const AGENT_DISCOVERY_NOTE = `=== AGENT COMMAND DISCOVERY === +Do not explore commands by chaining nested --help calls. Instead run: + sim cli search "" +It returns the 5 best-matching commands, each with a one-line summary. Pick the +best match rather than repeating similar searches, then run +\` --help\` for its flags. +=== END AGENT COMMAND DISCOVERY === +` + /** The root's own value-taking options, which consume the token after them. */ const ROOT_VALUE_FLAGS: ReadonlySet = new Set([ '-P', @@ -129,8 +148,9 @@ function addVersionOption(program: Command): void { * release. */ export function buildProgram( - options: { version?: boolean; helpText?: string; program?: Command } = {} + options: { version?: boolean; helpText?: string; program?: Command; env?: NodeJS.ProcessEnv } = {} ): Command { + const env = options.env ?? process.env const program = options.program ?? new Command() program.name('sim').description(PROGRAM_DESCRIPTION) @@ -155,6 +175,7 @@ export function buildProgram( const update = updateCommand() program.addCommand(update) program.addCommand(telemetryCommand()) + program.addCommand(cliCommand()) for (const command of buildGeneratedCommands()) { program.addCommand(command) @@ -165,6 +186,9 @@ export function buildProgram( attachSecretCommands(program) program.addHelpText('after', options.helpText ?? HELP_EPILOGUE) + program.addHelpText('beforeAll', ({ command }) => + detectCodingAgent(env) !== undefined && command.commands.length > 0 ? AGENT_DISCOVERY_NOTE : '' + ) program.hook('preAction', async (_program, command) => { if (command === update) return