diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c64fe3719..7f5ef5426 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -157,6 +157,10 @@ - Fixed the live Ask dialog crashing the whole session with a `replaceTabs` TypeError when a question reached `AskDialogComponent` without a string `question` field; questions are now normalized at dialog entry, mirroring the transcript renderer ([#7211](https://github.com/can1357/oh-my-pi/issues/7211)). - Fixed Codex web search collapsing backend errors to `Codex error (): Unknown error`; the SSE error parser now preserves the backend code and message from top-level, nested `error`, and `response.error` envelopes ([#7200](https://github.com/can1357/oh-my-pi/issues/7200)). +### Changed + +- Reduced `omp --help` cold-start latency and memory use by rendering lightweight command metadata without loading every runtime command and provider graph. + ## [17.2.2] - 2026-07-31 ### Added diff --git a/packages/coding-agent/src/cli-commands.ts b/packages/coding-agent/src/cli-commands.ts index 6d481446c..6a56da8c6 100644 --- a/packages/coding-agent/src/cli-commands.ts +++ b/packages/coding-agent/src/cli-commands.ts @@ -9,43 +9,179 @@ * regression that motivated the split. */ import type { CommandEntry } from "@oh-my-pi/pi-utils/cli"; +import * as commandHelp from "./cli/command-help"; import { flagConsumesValue } from "./cli/flag-tables"; +import { launchHelp } from "./commands/launch-help"; export const commands: CommandEntry[] = [ - { name: "launch", load: () => import("./commands/launch").then(m => m.default) }, - { name: "acp", load: () => import("./commands/acp").then(m => m.default) }, - { name: "auth-broker", load: () => import("./commands/auth-broker").then(m => m.default) }, - { name: "auth-gateway", load: () => import("./commands/auth-gateway").then(m => m.default) }, - { name: "agents", load: () => import("./commands/agents").then(m => m.default) }, - { name: "bench", load: () => import("./commands/bench").then(m => m.default) }, - { name: "browser-relay", load: () => import("./commands/browser-relay").then(m => m.default) }, - { name: "cleanse", load: () => import("./commands/cleanse").then(m => m.default) }, - { name: "commit", load: () => import("./commands/commit").then(m => m.default) }, - { name: "completions", load: () => import("./commands/completions").then(m => m.default) }, - { name: "__complete", load: () => import("./commands/complete").then(m => m.default) }, - { name: "config", load: () => import("./commands/config").then(m => m.default) }, - { name: "dry-balance", load: () => import("./commands/dry-balance").then(m => m.default) }, - { name: "gc", load: () => import("./commands/gc").then(m => m.default) }, - { name: "grep", load: () => import("./commands/grep").then(m => m.default) }, - { name: "gallery", load: () => import("./commands/gallery").then(m => m.default) }, - { name: "grievances", load: () => import("./commands/grievances").then(m => m.default) }, - { name: "install", load: () => import("./commands/install").then(m => m.default) }, - { name: "join", load: () => import("./commands/join").then(m => m.default) }, - { name: "models", load: () => import("./commands/models").then(m => m.default) }, - { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) }, - { name: "say", load: () => import("./commands/say").then(m => m.default) }, - { name: "setup", load: () => import("./commands/setup").then(m => m.default) }, - { name: "shell", load: () => import("./commands/shell").then(m => m.default) }, - { name: "read", load: () => import("./commands/read").then(m => m.default) }, - { name: "ssh", load: () => import("./commands/ssh").then(m => m.default) }, - { name: "stats", load: () => import("./commands/stats").then(m => m.default) }, - { name: "update", load: () => import("./commands/update").then(m => m.default) }, - { name: "usage", load: () => import("./commands/usage").then(m => m.default) }, - { name: "tiny-models", load: () => import("./commands/tiny-models").then(m => m.default) }, - { name: "token", load: () => import("./commands/token").then(m => m.default) }, - { name: "ttsr", load: () => import("./commands/ttsr").then(m => m.default) }, - { name: "worktree", load: () => import("./commands/worktree").then(m => m.default), aliases: ["wt"] }, - { name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"] }, + { name: "launch", load: () => import("./commands/launch").then(m => m.default), help: launchHelp }, + { + name: "acp", + load: () => import("./commands/acp").then(m => m.default), + help: commandHelp.acpHelp, + }, + { + name: "auth-broker", + load: () => import("./commands/auth-broker").then(m => m.default), + help: commandHelp.authBrokerHelp, + }, + { + name: "auth-gateway", + load: () => import("./commands/auth-gateway").then(m => m.default), + help: commandHelp.authGatewayHelp, + }, + { + name: "agents", + load: () => import("./commands/agents").then(m => m.default), + help: commandHelp.agentsHelp, + }, + { + name: "bench", + load: () => import("./commands/bench").then(m => m.default), + help: commandHelp.benchHelp, + }, + { + name: "browser-relay", + load: () => import("./commands/browser-relay").then(m => m.default), + help: commandHelp.browserRelayHelp, + }, + { + name: "cleanse", + load: () => import("./commands/cleanse").then(m => m.default), + help: commandHelp.cleanseHelp, + }, + { + name: "commit", + load: () => import("./commands/commit").then(m => m.default), + help: commandHelp.commitHelp, + }, + { + name: "completions", + load: () => import("./commands/completions").then(m => m.default), + help: commandHelp.completionsHelp, + }, + { + name: "__complete", + load: () => import("./commands/complete").then(m => m.default), + help: commandHelp.completeHelp, + }, + { + name: "config", + load: () => import("./commands/config").then(m => m.default), + help: commandHelp.configHelp, + }, + { + name: "dry-balance", + load: () => import("./commands/dry-balance").then(m => m.default), + help: commandHelp.dryBalanceHelp, + }, + { + name: "gc", + load: () => import("./commands/gc").then(m => m.default), + help: commandHelp.gcHelp, + }, + { + name: "grep", + load: () => import("./commands/grep").then(m => m.default), + help: commandHelp.grepHelp, + }, + { + name: "gallery", + load: () => import("./commands/gallery").then(m => m.default), + help: commandHelp.galleryHelp, + }, + { + name: "grievances", + load: () => import("./commands/grievances").then(m => m.default), + help: commandHelp.grievancesHelp, + }, + { + name: "install", + load: () => import("./commands/install").then(m => m.default), + help: commandHelp.installHelp, + }, + { + name: "join", + load: () => import("./commands/join").then(m => m.default), + help: commandHelp.joinHelp, + }, + { + name: "models", + load: () => import("./commands/models").then(m => m.default), + help: commandHelp.modelsHelp, + }, + { + name: "plugin", + load: () => import("./commands/plugin").then(m => m.default), + help: commandHelp.pluginHelp, + }, + { + name: "say", + load: () => import("./commands/say").then(m => m.default), + help: commandHelp.sayHelp, + }, + { + name: "setup", + load: () => import("./commands/setup").then(m => m.default), + help: commandHelp.setupHelp, + }, + { + name: "shell", + load: () => import("./commands/shell").then(m => m.default), + help: commandHelp.shellHelp, + }, + { + name: "read", + load: () => import("./commands/read").then(m => m.default), + help: commandHelp.readHelp, + }, + { + name: "ssh", + load: () => import("./commands/ssh").then(m => m.default), + help: commandHelp.sshHelp, + }, + { + name: "stats", + load: () => import("./commands/stats").then(m => m.default), + help: commandHelp.statsHelp, + }, + { + name: "update", + load: () => import("./commands/update").then(m => m.default), + help: commandHelp.updateHelp, + }, + { + name: "usage", + load: () => import("./commands/usage").then(m => m.default), + help: commandHelp.usageHelp, + }, + { + name: "tiny-models", + load: () => import("./commands/tiny-models").then(m => m.default), + help: commandHelp.tinyModelsHelp, + }, + { + name: "token", + load: () => import("./commands/token").then(m => m.default), + help: commandHelp.tokenHelp, + }, + { + name: "ttsr", + load: () => import("./commands/ttsr").then(m => m.default), + help: commandHelp.ttsrHelp, + }, + { + name: "worktree", + load: () => import("./commands/worktree").then(m => m.default), + aliases: ["wt"], + help: commandHelp.worktreeHelp, + }, + { + name: "search", + load: () => import("./commands/web-search").then(m => m.default), + aliases: ["q"], + help: commandHelp.searchHelp, + }, ]; // Documented-looking plugin/marketplace verbs that are NOT registered top-level diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 25541ddbe..237fe99b1 100755 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -15,7 +15,7 @@ try { * lightweight CLI runner from pi-utils. */ import { parentPort } from "node:worker_threads"; -import type { CliConfig } from "@oh-my-pi/pi-utils/cli"; +import type { CliConfig, CommandMetadata } from "@oh-my-pi/pi-utils/cli"; import { APP_NAME, getActiveProfile, @@ -61,9 +61,13 @@ const isProcessEntry = import.meta.main || process.env.PI_COMPILED === "true"; // `@oh-my-pi/pi-utils/env` eagerly loads `.env` from the agent directory at // import time, so it must not be imported before `setProfile` runs. -async function showHelp(config: CliConfig): Promise { - const { renderRootHelp } = await import("@oh-my-pi/pi-utils/cli"); - const { getExtraHelpText } = await import("./cli/args"); +async function showHelp(config: CliConfig): Promise { + // Root help historically loads the selected profile's environment. The + // lazily loaded help module imports it statically after profile bootstrap. + const [{ renderRootHelp }, { getExtraHelpText }] = await Promise.all([ + import("@oh-my-pi/pi-utils/cli"), + import("./cli/help-extra"), + ]); renderRootHelp(config); const extra = getExtraHelpText(); if (extra.trim().length > 0) { @@ -405,7 +409,7 @@ export async function runCli(argv: string[]): Promise { process.exitCode = 1; return; } - return run({ bin: APP_NAME, version: VERSION, argv: resolved.argv, commands, help: showHelp }); + return run({ bin: APP_NAME, version: VERSION, argv: resolved.argv, commands, metadataHelp: showHelp }); } // Floating call instead of top-level await: TLA forces `--bytecode` (CJS diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 0334039a5..b17194e81 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -1,7 +1,7 @@ /** * CLI argument parsing and help display */ -import { $env, APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils"; +import { $env, APP_NAME, logger } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { CLI_THINKING_LEVELS, type ConfiguredThinkingLevel, parseCliThinkingLevel } from "../thinking"; import { BUILTIN_TOOL_NAMES, HIDDEN_TOOL_NAMES, normalizeToolNames } from "../tools/builtin-names"; @@ -13,8 +13,11 @@ import { STRING_SETTERS, STRING_VALUE_FLAGS, } from "./flag-tables"; +import { getExtraHelpText } from "./help-extra"; import { CliUsageError } from "./usage-error"; +export { getExtraHelpText }; + export type Mode = "text" | "json" | "rpc" | "acp" | "rpc-ui"; export interface Args { @@ -331,92 +334,6 @@ export function reportCliUsageError( return true; } -export function getExtraHelpText(): string { - return `${chalk.bold("Environment Variables:")} - ${chalk.dim("# Core Providers")} - ANTHROPIC_API_KEY - Anthropic Claude models - ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth (takes precedence over API key) - CLAUDE_CODE_USE_FOUNDRY - Enable Anthropic Foundry mode (uses Foundry endpoint + mTLS) - FOUNDRY_BASE_URL - Anthropic Foundry base URL (e.g., https://) - ANTHROPIC_FOUNDRY_API_KEY - Anthropic token used as Authorization: Bearer in Foundry mode - ANTHROPIC_CUSTOM_HEADERS - Extra headers for Foundry or any custom ANTHROPIC_BASE_URL gateway (e.g., "user-id: USERNAME") - CLAUDE_CODE_CLIENT_CERT - Client certificate (PEM path or inline PEM) for mTLS - CLAUDE_CODE_CLIENT_KEY - Client private key (PEM path or inline PEM) for mTLS - NODE_EXTRA_CA_CERTS - CA bundle path (or inline PEM) for server certificate validation - OPENAI_API_KEY - OpenAI GPT models - GEMINI_API_KEY - Google Gemini models - COPILOT_GITHUB_TOKEN - GitHub Copilot - - ${chalk.dim("# Additional LLM Providers")} - AZURE_OPENAI_API_KEY - Azure OpenAI models - GROQ_API_KEY - Groq models - CEREBRAS_API_KEY - Cerebras models - XAI_API_KEY - xAI Grok models - OPENROUTER_API_KEY - OpenRouter aggregated models - KILO_API_KEY - Kilo Gateway models - MISTRAL_API_KEY - Mistral models - ZAI_API_KEY - z.ai models (ZhipuAI/GLM) - UMANS_AI_CODING_PLAN_API_KEY - Umans AI Coding Plan models - UMANS_WEBSEARCH_PROVIDER - Umans gateway web search backend (native or exa) - MINIMAX_API_KEY - MiniMax models - OPENCODE_API_KEY - OpenCode Zen/OpenCode Go models - CURSOR_ACCESS_TOKEN - Cursor AI models - AI_GATEWAY_API_KEY - Vercel AI Gateway - WAFER_SERVERLESS_API_KEY - Wafer Serverless (pay-as-you-go) - - ${chalk.dim("# Cloud Providers")} - AWS_PROFILE - AWS Bedrock (or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY) - GOOGLE_CLOUD_PROJECT - Google Vertex AI (requires GOOGLE_CLOUD_LOCATION) - GOOGLE_APPLICATION_CREDENTIALS - Service account for Vertex AI - - ${chalk.dim("# Search & Tools")} - EXA_API_KEY - Exa web search - BRAVE_API_KEY - Brave web search - PERPLEXITY_API_KEY - Perplexity web search API key (optional; anonymous fallback) - PERPLEXITY_COOKIES - Perplexity web search (session cookie) - TAVILY_API_KEY - Tavily web search - TINYFISH_API_KEY - TinyFish web search - FIRECRAWL_API_KEY - Firecrawl web search - ANTHROPIC_SEARCH_API_KEY - Anthropic web search (override; isolates search from main ANTHROPIC_API_KEY) - ANTHROPIC_SEARCH_BASE_URL - Anthropic web search base URL (override; pairs with ANTHROPIC_SEARCH_API_KEY) - - ${chalk.dim("# Configuration")} - OMP_PROFILE - Named profile for isolated agent state (same as --profile) - Use \`omp --profile --alias \` to create a shell shortcut for a profile - PI_CODING_AGENT_DIR - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent) - PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths) - PI_SMOL_MODEL - Override smol/fast model (see --smol) - PI_SLOW_MODEL - Override slow/reasoning model (see --slow) - PI_PLAN_MODEL - Override planning model (see --plan) - PI_NO_PTY - Disable PTY-based interactive bash execution - For complete environment variable reference, see: - ${chalk.dim("docs/environment-variables.md")} -${chalk.bold("Available Tools (default-enabled unless noted):")} - read - Read file contents - bash - Execute bash commands - edit - Edit files with find/replace - write - Write files (creates/overwrites) - grep - Search file contents - glob - Find files by glob pattern - lsp - Language server protocol (code intelligence) - python - Execute Python code (requires: ${APP_NAME} setup python) - notebook - Edit Jupyter notebooks - inspect_image - Analyze images with a vision model - browser - Browser automation (Puppeteer) - computer - Native host desktop capture and input (disabled by default) - task - Launch sub-agents for parallel tasks - todo - Manage todo/task lists - web_search - Search the web - ask - Ask user questions (interactive mode only) - -${chalk.bold("Plugin Options:")} - --plugin-dir Load plugin from directory (repeatable) - -${chalk.bold("Useful Commands:")} - omp agents unpack - Export bundled subagents to ~/.omp/agent/agents (default) - omp agents unpack --project - Export bundled subagents to ./.omp/agents`; -} - export function printHelp(): void { process.stdout.write( `${chalk.bold(APP_NAME)} - AI coding assistant\n\n` + diff --git a/packages/coding-agent/src/cli/command-help.ts b/packages/coding-agent/src/cli/command-help.ts new file mode 100644 index 000000000..0375b57d1 --- /dev/null +++ b/packages/coding-agent/src/cli/command-help.ts @@ -0,0 +1,103 @@ +import type { CommandMetadata } from "@oh-my-pi/pi-utils/cli"; + +export const acpHelp = { + description: "Run Oh My Pi as an ACP (Agent Client Protocol) server over stdio", +} satisfies CommandMetadata; + +export const agentsHelp = { description: "Manage bundled task agents" } satisfies CommandMetadata; + +export const authBrokerHelp = { + description: "Manage the omp auth-broker (credential vault)", +} satisfies CommandMetadata; + +export const authGatewayHelp = { + description: "Run an auth-gateway forward proxy backed by the configured broker", +} satisfies CommandMetadata; + +export const benchHelp = { + description: "Benchmark models with the same prompt: time-to-first-token and generation throughput (tokens/s)", +} satisfies CommandMetadata; + +export const browserRelayHelp = { + description: "Run the local CDP relay that lets the browser tool drive your own Chrome tabs", +} satisfies CommandMetadata; + +export const cleanseHelp = { + description: "Detect and fix project diagnostics with weighted parallel subagents", +} satisfies CommandMetadata; + +export const commitHelp = { description: "Generate a commit message and update changelogs" } satisfies CommandMetadata; + +export const completionsHelp = { + description: "Print a shell completion script (bash, zsh, or fish)", +} satisfies CommandMetadata; + +export const completeHelp = { hidden: true } satisfies CommandMetadata; + +export const configHelp = { description: "Manage configuration settings" } satisfies CommandMetadata; + +export const dryBalanceHelp = { + description: "Dry-run OAuth account balancing across random session ids", +} satisfies CommandMetadata; + +export const galleryHelp = { + description: "Preview tool renderers across streaming, in-progress, success, and failure states", +} satisfies CommandMetadata; + +export const gcHelp = { description: "Run storage garbage collection" } satisfies CommandMetadata; + +export const grepHelp = { description: "Test grep tool" } satisfies CommandMetadata; + +export const grievancesHelp = { + description: "View, clean, or push reported tool issues (auto-QA grievances)", +} satisfies CommandMetadata; + +export const installHelp = { + description: "Install or link an extension package (alias of `plugin install`/`plugin link`)", +} satisfies CommandMetadata; + +export const joinHelp = { description: "Join a shared collab session (same as /join)" } satisfies CommandMetadata; + +export const modelsHelp = { description: "List, search, and refresh available models" } satisfies CommandMetadata; + +export const pluginHelp = { description: "Manage plugins (install, uninstall, list, etc.)" } satisfies CommandMetadata; + +export const readHelp = { + description: "Show what the read tool will return for a path, URL, or internal URI", +} satisfies CommandMetadata; + +export const sayHelp = { + description: "Synthesize text with the local TTS engine and play it through the speakers", +} satisfies CommandMetadata; + +export const searchHelp = { description: "Test web search providers" } satisfies CommandMetadata; + +export const setupHelp = { + description: "Run onboarding setup or install dependencies for optional features", +} satisfies CommandMetadata; + +export const shellHelp = { description: "Interactive shell console" } satisfies CommandMetadata; + +export const sshHelp = { description: "Manage SSH host configurations" } satisfies CommandMetadata; + +export const statsHelp = { description: "View usage statistics" } satisfies CommandMetadata; + +export const tinyModelsHelp = { + description: "Download tiny local models (session titles + memory)", +} satisfies CommandMetadata; + +export const tokenHelp = { description: "Get the API key or OAuth token for a provider" } satisfies CommandMetadata; + +export const ttsrHelp = { + description: "Inspect and test Time-Traveling Stream Rules (TTSR)", +} satisfies CommandMetadata; + +export const updateHelp = { description: "Check for and install updates" } satisfies CommandMetadata; + +export const usageHelp = { + description: "Show provider usage limits for every authenticated account", +} satisfies CommandMetadata; + +export const worktreeHelp = { + description: "List or clear agent-managed git worktrees (~/.omp/wt)", +} satisfies CommandMetadata; diff --git a/packages/coding-agent/src/cli/completion-gen.ts b/packages/coding-agent/src/cli/completion-gen.ts index 60aecbc17..75fb5d905 100644 --- a/packages/coding-agent/src/cli/completion-gen.ts +++ b/packages/coding-agent/src/cli/completion-gen.ts @@ -14,7 +14,7 @@ * — see `commands/complete.ts`. The flag→source mapping below is the only manual * knob and is keyed by flag name so it stays stable as flags are added. */ -import type { ArgDescriptor, CliConfig, CommandCtor, FlagDescriptor } from "@oh-my-pi/pi-utils/cli"; +import type { ArgDescriptor, CliConfig, CommandMetadata, FlagDescriptor } from "@oh-my-pi/pi-utils/cli"; import { BUILTIN_TOOL_NAMES } from "../tools/builtin-names"; export type Shell = "bash" | "zsh" | "fish"; @@ -88,9 +88,9 @@ function argValue(desc: ArgDescriptor): ValueSource { return { kind: "file" }; } -function buildFlags(Cmd: CommandCtor): CompletionFlag[] { +function buildFlags(command: CommandMetadata): CompletionFlag[] { const out: CompletionFlag[] = []; - const flags = Cmd.flags ?? {}; + const flags = command.flags ?? {}; for (const name in flags) { const desc = flags[name]; out.push({ @@ -104,9 +104,9 @@ function buildFlags(Cmd: CommandCtor): CompletionFlag[] { return out; } -function buildArgs(Cmd: CommandCtor): CompletionArg[] { +function buildArgs(command: CommandMetadata): CompletionArg[] { const out: CompletionArg[] = []; - const args = Cmd.args ?? {}; + const args = command.args ?? {}; for (const name in args) { const desc = args[name]; out.push({ name, description: desc.description ?? "", value: argValue(desc) }); diff --git a/packages/coding-agent/src/cli/help-extra.ts b/packages/coding-agent/src/cli/help-extra.ts new file mode 100644 index 000000000..22f6985bb --- /dev/null +++ b/packages/coding-agent/src/cli/help-extra.ts @@ -0,0 +1,89 @@ +import "@oh-my-pi/pi-utils/env"; +import { APP_NAME, CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils/dirs"; +import chalk from "chalk"; + +export function getExtraHelpText(): string { + return `${chalk.bold("Environment Variables:")} + ${chalk.dim("# Core Providers")} + ANTHROPIC_API_KEY - Anthropic Claude models + ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth (takes precedence over API key) + CLAUDE_CODE_USE_FOUNDRY - Enable Anthropic Foundry mode (uses Foundry endpoint + mTLS) + FOUNDRY_BASE_URL - Anthropic Foundry base URL (e.g., https://) + ANTHROPIC_FOUNDRY_API_KEY - Anthropic token used as Authorization: Bearer in Foundry mode + ANTHROPIC_CUSTOM_HEADERS - Extra headers for Foundry or any custom ANTHROPIC_BASE_URL gateway (e.g., "user-id: USERNAME") + CLAUDE_CODE_CLIENT_CERT - Client certificate (PEM path or inline PEM) for mTLS + CLAUDE_CODE_CLIENT_KEY - Client private key (PEM path or inline PEM) for mTLS + NODE_EXTRA_CA_CERTS - CA bundle path (or inline PEM) for server certificate validation + OPENAI_API_KEY - OpenAI GPT models + GEMINI_API_KEY - Google Gemini models + COPILOT_GITHUB_TOKEN - GitHub Copilot + + ${chalk.dim("# Additional LLM Providers")} + AZURE_OPENAI_API_KEY - Azure OpenAI models + GROQ_API_KEY - Groq models + CEREBRAS_API_KEY - Cerebras models + XAI_API_KEY - xAI Grok models + OPENROUTER_API_KEY - OpenRouter aggregated models + KILO_API_KEY - Kilo Gateway models + MISTRAL_API_KEY - Mistral models + ZAI_API_KEY - z.ai models (ZhipuAI/GLM) + UMANS_AI_CODING_PLAN_API_KEY - Umans AI Coding Plan models + UMANS_WEBSEARCH_PROVIDER - Umans gateway web search backend (native or exa) + MINIMAX_API_KEY - MiniMax models + OPENCODE_API_KEY - OpenCode Zen/OpenCode Go models + CURSOR_ACCESS_TOKEN - Cursor AI models + AI_GATEWAY_API_KEY - Vercel AI Gateway + WAFER_SERVERLESS_API_KEY - Wafer Serverless (pay-as-you-go) + + ${chalk.dim("# Cloud Providers")} + AWS_PROFILE - AWS Bedrock (or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY) + GOOGLE_CLOUD_PROJECT - Google Vertex AI (requires GOOGLE_CLOUD_LOCATION) + GOOGLE_APPLICATION_CREDENTIALS - Service account for Vertex AI + + ${chalk.dim("# Search & Tools")} + EXA_API_KEY - Exa web search + BRAVE_API_KEY - Brave web search + PERPLEXITY_API_KEY - Perplexity web search API key (optional; anonymous fallback) + PERPLEXITY_COOKIES - Perplexity web search (session cookie) + TAVILY_API_KEY - Tavily web search + TINYFISH_API_KEY - TinyFish web search + FIRECRAWL_API_KEY - Firecrawl web search + ANTHROPIC_SEARCH_API_KEY - Anthropic web search (override; isolates search from main ANTHROPIC_API_KEY) + ANTHROPIC_SEARCH_BASE_URL - Anthropic web search base URL (override; pairs with ANTHROPIC_SEARCH_API_KEY) + + ${chalk.dim("# Configuration")} + OMP_PROFILE - Named profile for isolated agent state (same as --profile) + Use \`omp --profile --alias \` to create a shell shortcut for a profile + PI_CODING_AGENT_DIR - Session storage directory (default: ~/${CONFIG_DIR_NAME}/agent) + PI_PACKAGE_DIR - Override package directory (for Nix/Guix store paths) + PI_SMOL_MODEL - Override smol/fast model (see --smol) + PI_SLOW_MODEL - Override slow/reasoning model (see --slow) + PI_PLAN_MODEL - Override planning model (see --plan) + PI_NO_PTY - Disable PTY-based interactive bash execution + For complete environment variable reference, see: + ${chalk.dim("docs/environment-variables.md")} +${chalk.bold("Available Tools (default-enabled unless noted):")} + read - Read file contents + bash - Execute bash commands + edit - Edit files with find/replace + write - Write files (creates/overwrites) + grep - Search file contents + glob - Find files by glob pattern + lsp - Language server protocol (code intelligence) + python - Execute Python code (requires: ${APP_NAME} setup python) + notebook - Edit Jupyter notebooks + inspect_image - Analyze images with a vision model + browser - Browser automation (Puppeteer) + computer - Native host desktop capture and input (disabled by default) + task - Launch sub-agents for parallel tasks + todo - Manage todo/task lists + web_search - Search the web + ask - Ask user questions (interactive mode only) + +${chalk.bold("Plugin Options:")} + --plugin-dir Load plugin from directory (repeatable) + +${chalk.bold("Useful Commands:")} + omp agents unpack - Export bundled subagents to ~/.omp/agent/agents (default) + omp agents unpack --project - Export bundled subagents to ./.omp/agents`; +} diff --git a/packages/coding-agent/src/cli/thinking-levels.ts b/packages/coding-agent/src/cli/thinking-levels.ts new file mode 100644 index 000000000..e954f2f30 --- /dev/null +++ b/packages/coding-agent/src/cli/thinking-levels.ts @@ -0,0 +1,7 @@ +import { THINKING_EFFORTS } from "@oh-my-pi/pi-catalog/effort"; + +/** + * Thinking selectors accepted by the `--thinking` CLI flag, in display order. + * Shared by help metadata, shell completions, and validation warnings. + */ +export const CLI_THINKING_LEVELS: readonly string[] = ["off", ...THINKING_EFFORTS, "auto"]; diff --git a/packages/coding-agent/src/commands/acp.ts b/packages/coding-agent/src/commands/acp.ts index 89029a11a..fe411f7e3 100644 --- a/packages/coding-agent/src/commands/acp.ts +++ b/packages/coding-agent/src/commands/acp.ts @@ -4,13 +4,15 @@ * Thin wrapper around the launch flow that forces `mode: "acp"` unless the * ACP terminal-auth flag asks the same command to open the interactive TUI. */ + import { Command } from "@oh-my-pi/pi-utils/cli"; import { type Args as ParsedArgs, parseArgs, reportCliUsageError } from "../cli/args"; +import { acpHelp as commandHelp } from "../cli/command-help"; import { runRootCommand } from "../main"; import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth"; export default class Acp extends Command { - static description = "Run Oh My Pi as an ACP (Agent Client Protocol) server over stdio"; + static description = commandHelp.description; static strict = false; async run(): Promise { diff --git a/packages/coding-agent/src/commands/agents.ts b/packages/coding-agent/src/commands/agents.ts index a255b43f5..1ecdbfa84 100644 --- a/packages/coding-agent/src/commands/agents.ts +++ b/packages/coding-agent/src/commands/agents.ts @@ -1,15 +1,16 @@ /** * Manage bundled task agents. */ + import { Args, Command, Flags, renderCommandHelp } from "@oh-my-pi/pi-utils/cli"; import { type AgentsAction, type AgentsCommandArgs, runAgentsCommand } from "../cli/agents-cli"; +import { agentsHelp as commandHelp } from "../cli/command-help"; import { initTheme } from "../modes/theme/theme"; const ACTIONS: AgentsAction[] = ["unpack"]; export default class Agents extends Command { - static description = "Manage bundled task agents"; - + static description = commandHelp.description; static args = { action: Args.string({ description: "Agents action", diff --git a/packages/coding-agent/src/commands/auth-broker.ts b/packages/coding-agent/src/commands/auth-broker.ts index 85bb5906e..9f14473cb 100644 --- a/packages/coding-agent/src/commands/auth-broker.ts +++ b/packages/coding-agent/src/commands/auth-broker.ts @@ -1,6 +1,7 @@ /** * `omp auth-broker` — manage the omp credential vault. */ + import { Args, Command, Flags, renderCommandHelp } from "@oh-my-pi/pi-utils/cli"; import { AUTH_BROKER_ACTIONS, @@ -8,11 +9,11 @@ import { type AuthBrokerCommandArgs, runAuthBrokerCommand, } from "../cli/auth-broker-cli"; +import { authBrokerHelp as commandHelp } from "../cli/command-help"; import { initTheme } from "../modes/theme/theme"; export default class AuthBroker extends Command { - static description = "Manage the omp auth-broker (credential vault)"; - + static description = commandHelp.description; static args = { action: Args.string({ description: "Sub-command", diff --git a/packages/coding-agent/src/commands/auth-gateway.ts b/packages/coding-agent/src/commands/auth-gateway.ts index 7f713e94d..e791f7b25 100644 --- a/packages/coding-agent/src/commands/auth-gateway.ts +++ b/packages/coding-agent/src/commands/auth-gateway.ts @@ -1,6 +1,7 @@ /** * `omp auth-gateway` — run a forward proxy that injects auth from the broker. */ + import { Args, Command, Flags, renderCommandHelp } from "@oh-my-pi/pi-utils/cli"; import { AUTH_GATEWAY_ACTIONS, @@ -8,11 +9,11 @@ import { type AuthGatewayCommandArgs, runAuthGatewayCommand, } from "../cli/auth-gateway-cli"; +import { authGatewayHelp as commandHelp } from "../cli/command-help"; import { initTheme } from "../modes/theme/theme"; export default class AuthGateway extends Command { - static description = "Run an auth-gateway forward proxy backed by the configured broker"; - + static description = commandHelp.description; static args = { action: Args.string({ description: "Sub-command", diff --git a/packages/coding-agent/src/commands/bench.ts b/packages/coding-agent/src/commands/bench.ts index 9ac00e02c..203924e73 100644 --- a/packages/coding-agent/src/commands/bench.ts +++ b/packages/coding-agent/src/commands/bench.ts @@ -1,11 +1,10 @@ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; import { runBenchCommand } from "../cli/bench-cli"; +import { benchHelp as commandHelp } from "../cli/command-help"; import { SERVICE_TIER_OPENAI_VALUES } from "../config/service-tier"; export default class Bench extends Command { - static description = - "Benchmark models with the same prompt: time-to-first-token and generation throughput (tokens/s)"; - + static description = commandHelp.description; static args = { models: Args.string({ description: "Model selectors (provider/model or fuzzy id, e.g. opus)", diff --git a/packages/coding-agent/src/commands/cleanse.ts b/packages/coding-agent/src/commands/cleanse.ts index b79a1dde5..cb600a699 100644 --- a/packages/coding-agent/src/commands/cleanse.ts +++ b/packages/coding-agent/src/commands/cleanse.ts @@ -1,11 +1,11 @@ import { postmortem } from "@oh-my-pi/pi-utils"; import { Command, Flags } from "@oh-my-pi/pi-utils/cli"; import { runCleanseCommand } from "../cleanse"; +import { cleanseHelp as commandHelp } from "../cli/command-help"; import { CliUsageError } from "../cli/usage-error"; export default class Cleanse extends Command { - static description = "Detect and fix project diagnostics with weighted parallel subagents"; - + static description = commandHelp.description; static flags = { agents: Flags.integer({ char: "n", diff --git a/packages/coding-agent/src/commands/commit.ts b/packages/coding-agent/src/commands/commit.ts index d5abd4c38..ade85bd95 100644 --- a/packages/coding-agent/src/commands/commit.ts +++ b/packages/coding-agent/src/commands/commit.ts @@ -1,15 +1,16 @@ /** * Generate and optionally push a commit with changelog updates. */ + import { postmortem } from "@oh-my-pi/pi-utils"; import { Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { commitHelp as commandHelp } from "../cli/command-help"; import { runCommitCommand } from "../commit"; import type { CommitCommandArgs } from "../commit/types"; import { initTheme } from "../modes/theme/theme"; export default class Commit extends Command { - static description = "Generate a commit message and update changelogs"; - + static description = commandHelp.description; static flags = { push: Flags.boolean({ description: "Push after committing" }), "dry-run": Flags.boolean({ description: "Preview without committing" }), diff --git a/packages/coding-agent/src/commands/complete.ts b/packages/coding-agent/src/commands/complete.ts index f9eb67c53..a341a51c5 100644 --- a/packages/coding-agent/src/commands/complete.ts +++ b/packages/coding-agent/src/commands/complete.ts @@ -10,10 +10,11 @@ */ import { type GeneratedProvider, getBundledModels, getBundledProviders } from "@oh-my-pi/pi-catalog/models"; import { Command } from "@oh-my-pi/pi-utils/cli"; +import { completeHelp as commandHelp } from "../cli/command-help"; import { SessionManager } from "../session/session-manager"; export default class Complete extends Command { - static hidden = true; + static hidden = commandHelp.hidden; static strict = false; async run(): Promise { diff --git a/packages/coding-agent/src/commands/completions.ts b/packages/coding-agent/src/commands/completions.ts index 146e884ee..260979f18 100644 --- a/packages/coding-agent/src/commands/completions.ts +++ b/packages/coding-agent/src/commands/completions.ts @@ -4,8 +4,10 @@ * The script is derived entirely from the declarative command/flag metadata * (see `cli/completion-gen.ts`), so it never drifts from the actual CLI surface. */ + import { APP_NAME, VERSION } from "@oh-my-pi/pi-utils"; import { Args, type CliConfig, Command, type CommandCtor } from "@oh-my-pi/pi-utils/cli"; +import { completionsHelp as commandHelp } from "../cli/command-help"; import { buildSpec, generateCompletion, type Shell } from "../cli/completion-gen"; import { commands } from "../cli-commands"; @@ -14,8 +16,7 @@ const ROOT_COMMAND = "launch"; const SHELLS = ["bash", "zsh", "fish"] as const; export default class Completions extends Command { - static description = "Print a shell completion script (bash, zsh, or fish)"; - + static description = commandHelp.description; static args = { shell: Args.string({ description: "Target shell", diff --git a/packages/coding-agent/src/commands/config.ts b/packages/coding-agent/src/commands/config.ts index 444361a8e..eef07e438 100644 --- a/packages/coding-agent/src/commands/config.ts +++ b/packages/coding-agent/src/commands/config.ts @@ -1,15 +1,16 @@ /** * Manage configuration settings. */ + import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { configHelp as commandHelp } from "../cli/command-help"; import { type ConfigAction, type ConfigCommandArgs, runConfigCommand } from "../cli/config-cli"; import { initTheme } from "../modes/theme/theme"; const ACTIONS: ConfigAction[] = ["list", "get", "set", "reset", "path", "init-xdg"]; export default class Config extends Command { - static description = "Manage configuration settings"; - + static description = commandHelp.description; static args = { action: Args.string({ description: "Config action", diff --git a/packages/coding-agent/src/commands/dry-balance.ts b/packages/coding-agent/src/commands/dry-balance.ts index e27763014..0ae544755 100644 --- a/packages/coding-agent/src/commands/dry-balance.ts +++ b/packages/coding-agent/src/commands/dry-balance.ts @@ -1,9 +1,9 @@ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { dryBalanceHelp as commandHelp } from "../cli/command-help"; import { runDryBalanceCommand } from "../cli/dry-balance-cli"; export default class DryBalance extends Command { - static description = "Dry-run OAuth account balancing across random session ids"; - + static description = commandHelp.description; static args = { model: Args.string({ description: "Model selector (provider/model or fuzzy id). Defaults to the configured default model.", diff --git a/packages/coding-agent/src/commands/gallery.ts b/packages/coding-agent/src/commands/gallery.ts index b54b2a822..3165ea7e5 100644 --- a/packages/coding-agent/src/commands/gallery.ts +++ b/packages/coding-agent/src/commands/gallery.ts @@ -1,12 +1,13 @@ /** * Render every built-in tool's renderer across its lifecycle states. */ + import { Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { galleryHelp as commandHelp } from "../cli/command-help"; import { GALLERY_STATE_TOKENS, type GalleryState, parseGalleryStates, runGalleryCommand } from "../cli/gallery-cli"; export default class Gallery extends Command { - static description = "Preview tool renderers across streaming, in-progress, success, and failure states"; - + static description = commandHelp.description; static flags = { tool: Flags.string({ char: "t", description: "Render a single tool by name" }), state: Flags.string({ diff --git a/packages/coding-agent/src/commands/gc.ts b/packages/coding-agent/src/commands/gc.ts index 10e56c002..81cd56c57 100644 --- a/packages/coding-agent/src/commands/gc.ts +++ b/packages/coding-agent/src/commands/gc.ts @@ -1,12 +1,13 @@ /** * Run on-disk storage maintenance. */ + import { Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { gcHelp as commandHelp } from "../cli/command-help"; import { collectGcErrors, type GcCommandArgs, runGcCommand } from "../cli/gc-cli"; export default class Gc extends Command { - static description = "Run storage garbage collection"; - + static description = commandHelp.description; static flags = { apply: Flags.boolean({ description: "Apply changes (default is dry-run)" }), json: Flags.boolean({ description: "Output JSON" }), diff --git a/packages/coding-agent/src/commands/grep.ts b/packages/coding-agent/src/commands/grep.ts index 8a63e954a..b1fed0b21 100644 --- a/packages/coding-agent/src/commands/grep.ts +++ b/packages/coding-agent/src/commands/grep.ts @@ -1,14 +1,15 @@ /** * Test grep tool. */ + import { GrepOutputMode } from "@oh-my-pi/pi-natives"; import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { grepHelp as commandHelp } from "../cli/command-help"; import { type GrepCommandArgs, runGrepCommand } from "../cli/grep-cli"; import { initTheme } from "../modes/theme/theme"; export default class Grep extends Command { - static description = "Test grep tool"; - + static description = commandHelp.description; static args = { pattern: Args.string({ description: "Regex pattern to search for", required: false }), path: Args.string({ description: "Directory or file to search", required: false }), diff --git a/packages/coding-agent/src/commands/grievances.ts b/packages/coding-agent/src/commands/grievances.ts index d651b1d90..fd0c9452a 100644 --- a/packages/coding-agent/src/commands/grievances.ts +++ b/packages/coding-agent/src/commands/grievances.ts @@ -1,12 +1,13 @@ /** * View, clean, and push reported tool issues from automated QA. */ + import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { grievancesHelp as commandHelp } from "../cli/command-help"; import { cleanGrievances, listGrievances, pushGrievances } from "../cli/grievances-cli"; export default class Grievances extends Command { - static description = "View, clean, or push reported tool issues (auto-QA grievances)"; - + static description = commandHelp.description; static args = { // Positional action: "list" (default), "clean", or "push". A positional // arg keeps the historical `omp grievances` invocation working unchanged diff --git a/packages/coding-agent/src/commands/install.ts b/packages/coding-agent/src/commands/install.ts index dc707f981..944963905 100644 --- a/packages/coding-agent/src/commands/install.ts +++ b/packages/coding-agent/src/commands/install.ts @@ -21,6 +21,7 @@ import { existsSync } from "node:fs"; import * as path from "node:path"; import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { installHelp as commandHelp } from "../cli/command-help"; import { type PluginAction, type PluginCommandArgs, runPluginCommand } from "../cli/plugin-cli"; import { initTheme } from "../modes/theme/theme"; @@ -41,8 +42,7 @@ export function looksLikeLocalPath(target: string, cwd?: string): boolean { } export default class Install extends Command { - static description = "Install or link an extension package (alias of `plugin install`/`plugin link`)"; - + static description = commandHelp.description; static args = { targets: Args.string({ description: "Local path, npm spec, or marketplace ref (e.g. ./my-ext, my-pkg@1.2.3, name@marketplace)", diff --git a/packages/coding-agent/src/commands/join.ts b/packages/coding-agent/src/commands/join.ts index c7676e477..65f9edca0 100644 --- a/packages/coding-agent/src/commands/join.ts +++ b/packages/coding-agent/src/commands/join.ts @@ -2,14 +2,15 @@ * Join a shared collab session from the CLI: launches the interactive TUI and * immediately runs `/join `. */ + import { APP_NAME } from "@oh-my-pi/pi-utils"; import { Args, Command } from "@oh-my-pi/pi-utils/cli"; import { parseArgs } from "../cli/args"; +import { joinHelp as commandHelp } from "../cli/command-help"; import { runRootCommand } from "../main"; export default class Join extends Command { - static description = "Join a shared collab session (same as /join)"; - + static description = commandHelp.description; static args = { link: Args.string({ description: "Collab link shared by the host (/collab)", diff --git a/packages/coding-agent/src/commands/launch-help.ts b/packages/coding-agent/src/commands/launch-help.ts new file mode 100644 index 000000000..0481b7929 --- /dev/null +++ b/packages/coding-agent/src/commands/launch-help.ts @@ -0,0 +1,111 @@ +import { Args, type CommandMetadata, Flags } from "@oh-my-pi/pi-utils/cli"; +import { APP_NAME } from "@oh-my-pi/pi-utils/dirs"; +import { CLI_THINKING_LEVELS } from "../cli/thinking-levels"; + +export const launchHelp = { + description: "AI coding assistant", + hidden: true, + args: { + messages: Args.string({ + description: "Messages to send (prefix files with @)", + required: false, + multiple: true, + }), + }, + flags: { + model: Flags.string({ + description: 'Model to use (fuzzy match: "opus", "gpt-5.2", or "openai/gpt-5.2")', + }), + smol: Flags.string({ description: "Smol/fast model for lightweight tasks (or PI_SMOL_MODEL env)" }), + slow: Flags.string({ description: "Slow/reasoning model for thorough analysis (or PI_SLOW_MODEL env)" }), + plan: Flags.string({ description: "Plan model for architectural planning (or PI_PLAN_MODEL env)" }), + prewalk: Flags.boolean({ + description: + "Switch from the active model to a fast/cheap model at the first edit/write after the plan's todo list exists (default off; see prewalk.enabled)", + }), + "no-prewalk": Flags.boolean({ description: "Disable prewalk even if prewalk.enabled is set" }), + "prewalk-into": Flags.string({ description: 'Target model for prewalk (default the "smol" role)' }), + "plan-yolo": Flags.boolean({ + description: + "Force read-only plan mode at start, auto-approve the plan on the model's first resolve call, then switch to --plan-yolo-into to implement it", + }), + "plan-yolo-into": Flags.string({ description: 'Target model for plan-yolo execution (default the "smol" role)' }), + provider: Flags.string({ description: "Provider to use (legacy; prefer --model)" }), + "api-key": Flags.string({ description: "API key (defaults to env vars)" }), + "system-prompt": Flags.string({ description: "System prompt (default: coding assistant prompt)" }), + "append-system-prompt": Flags.string({ description: "Append text or file contents to the system prompt" }), + "allow-home": Flags.boolean({ description: "Allow starting in ~ without auto-switching to a temp dir" }), + profile: Flags.string({ description: "Use an isolated profile for auth, sessions, settings, and caches" }), + alias: Flags.string({ description: "Create a shell shortcut for the selected profile and exit" }), + cwd: Flags.string({ description: "Directory to start in (overrides the launch cwd)" }), + mode: Flags.string({ + description: "Output mode: text (default), json, rpc, or rpc-ui", + options: ["text", "json", "rpc", "acp", "rpc-ui"], + }), + config: Flags.string({ + description: "Load an extra config.yml-style overlay for this run (repeatable)", + multiple: true, + }), + "add-dir": Flags.string({ + description: "Add a workspace directory beyond the working directory (repeatable)", + multiple: true, + }), + print: Flags.boolean({ char: "p", description: "Non-interactive mode: process prompt and exit" }), + continue: Flags.boolean({ char: "c", description: "Continue previous session" }), + resume: Flags.string({ char: "r", description: "Resume a session (by ID prefix, path, or picker if omitted)" }), + "from-claude": Flags.boolean({ description: "Import a Claude Code session into OMP" }), + "from-codex": Flags.boolean({ description: "Import a Codex session into OMP" }), + "session-dir": Flags.string({ description: "Directory for session storage and lookup" }), + "no-session": Flags.boolean({ description: "Don't save session (ephemeral)" }), + models: Flags.string({ description: "Comma-separated model patterns for Ctrl+P cycling" }), + "no-tools": Flags.boolean({ description: "Disable all built-in tools" }), + "no-lsp": Flags.boolean({ description: "Disable LSP tools, formatting, and diagnostics" }), + "no-pty": Flags.boolean({ description: "Disable PTY-based interactive bash execution" }), + tools: Flags.string({ description: "Comma-separated list of tools to enable (default: all)" }), + thinking: Flags.string({ + description: `Set thinking level: ${CLI_THINKING_LEVELS.join(", ")}`, + options: [...CLI_THINKING_LEVELS], + }), + "hide-thinking": Flags.boolean({ + description: "Hide thinking blocks in TUI output (display only, does not disable model thinking)", + }), + advisor: Flags.boolean({ + description: "Enable the advisor runtime (passively reviews each turn and injects notes)", + }), + hook: Flags.string({ description: "Load a hook/extension file (can be used multiple times)", multiple: true }), + extension: Flags.string({ + char: "e", + description: "Load an extension file (can be used multiple times)", + multiple: true, + }), + "no-extensions": Flags.boolean({ + description: "Disable extension discovery (explicit -e paths still work)", + }), + "no-skills": Flags.boolean({ description: "Disable skills discovery and loading" }), + skills: Flags.string({ description: "Comma-separated glob patterns to filter skills (e.g., git-*,docker)" }), + "no-rules": Flags.boolean({ description: "Disable rules discovery and loading" }), + export: Flags.string({ description: "Export session file to HTML and exit" }), + "no-title": Flags.boolean({ description: "Disable title auto-generation" }), + "print-thoughts": Flags.boolean({ description: "Include thinking blocks in print mode text output" }), + "max-time": Flags.string({ description: "Stop the session after this duration (e.g., 600, 10m, 1h)" }), + "auto-approve": Flags.boolean({ + aliases: ["yolo"], + description: "Auto-approve all tool calls (skip approval prompts)", + }), + "approval-mode": Flags.string({ + options: ["always-ask", "write", "yolo"], + description: "Override tools.approvalMode for this session (always-ask|write|yolo)", + }), + }, + examples: [ + `# Interactive mode\n ${APP_NAME}`, + `# Interactive mode with initial prompt\n ${APP_NAME} "List all .ts files in src/"`, + `# Include files in initial message\n ${APP_NAME} @prompt.md @image.png "What color is the sky?"`, + `# Non-interactive mode (process and exit)\n ${APP_NAME} -p "List all .ts files in src/"`, + `# Continue previous session\n ${APP_NAME} --continue "What did we discuss?"`, + `# Create a shell shortcut for a work profile\n ${APP_NAME} --profile work --alias omp-work`, + `# Use different model (fuzzy matching)\n ${APP_NAME} --model opus "Help me refactor this code"`, + `# Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o`, + `# Export a session file to HTML\n ${APP_NAME} --export ~/.omp/agent/sessions/--path--/session.jsonl`, + ], +} satisfies CommandMetadata; diff --git a/packages/coding-agent/src/commands/launch.ts b/packages/coding-agent/src/commands/launch.ts index 8019bf4e6..a0c8ac6a7 100644 --- a/packages/coding-agent/src/commands/launch.ts +++ b/packages/coding-agent/src/commands/launch.ts @@ -2,202 +2,18 @@ * Root command for the coding agent CLI. */ -import { APP_NAME } from "@oh-my-pi/pi-utils"; -import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { Command } from "@oh-my-pi/pi-utils/cli"; import { type Args as ParsedArgs, parseArgs, reportCliUsageError } from "../cli/args"; import { runRootCommand } from "../main"; import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth"; -import { CLI_THINKING_LEVELS } from "../thinking"; +import { launchHelp } from "./launch-help"; export default class Index extends Command { - static description = "AI coding assistant"; - static hidden = true; - - static args = { - messages: Args.string({ - description: "Messages to send (prefix files with @)", - required: false, - multiple: true, - }), - }; - - static flags = { - model: Flags.string({ - description: 'Model to use (fuzzy match: "opus", "gpt-5.2", or "openai/gpt-5.2")', - }), - smol: Flags.string({ - description: "Smol/fast model for lightweight tasks (or PI_SMOL_MODEL env)", - }), - slow: Flags.string({ - description: "Slow/reasoning model for thorough analysis (or PI_SLOW_MODEL env)", - }), - plan: Flags.string({ - description: "Plan model for architectural planning (or PI_PLAN_MODEL env)", - }), - prewalk: Flags.boolean({ - description: - "Switch from the active model to a fast/cheap model at the first edit/write after the plan's todo list exists (default off; see prewalk.enabled)", - }), - "no-prewalk": Flags.boolean({ - description: "Disable prewalk even if prewalk.enabled is set", - }), - "prewalk-into": Flags.string({ - description: 'Target model for prewalk (default the "smol" role)', - }), - "plan-yolo": Flags.boolean({ - description: - "Force read-only plan mode at start, auto-approve the plan on the model's first resolve call, then switch to --plan-yolo-into to implement it", - }), - "plan-yolo-into": Flags.string({ - description: 'Target model for plan-yolo execution (default the "smol" role)', - }), - provider: Flags.string({ - description: "Provider to use (legacy; prefer --model)", - }), - "api-key": Flags.string({ - description: "API key (defaults to env vars)", - }), - "system-prompt": Flags.string({ - description: "System prompt (default: coding assistant prompt)", - }), - "append-system-prompt": Flags.string({ - description: "Append text or file contents to the system prompt", - }), - "allow-home": Flags.boolean({ - description: "Allow starting in ~ without auto-switching to a temp dir", - }), - profile: Flags.string({ - description: "Use an isolated profile for auth, sessions, settings, and caches", - }), - alias: Flags.string({ - description: "Create a shell shortcut for the selected profile and exit", - }), - cwd: Flags.string({ - description: "Directory to start in (overrides the launch cwd)", - }), - mode: Flags.string({ - description: "Output mode: text (default), json, rpc, or rpc-ui", - options: ["text", "json", "rpc", "acp", "rpc-ui"], - }), - config: Flags.string({ - description: "Load an extra config.yml-style overlay for this run (repeatable)", - multiple: true, - }), - "add-dir": Flags.string({ - description: "Add a workspace directory beyond the working directory (repeatable)", - multiple: true, - }), - print: Flags.boolean({ - char: "p", - description: "Non-interactive mode: process prompt and exit", - }), - continue: Flags.boolean({ - char: "c", - description: "Continue previous session", - }), - resume: Flags.string({ - char: "r", - description: "Resume a session (by ID prefix, path, or picker if omitted)", - }), - "from-claude": Flags.boolean({ - description: "Import a Claude Code session into OMP", - }), - "from-codex": Flags.boolean({ - description: "Import a Codex session into OMP", - }), - "session-dir": Flags.string({ - description: "Directory for session storage and lookup", - }), - "no-session": Flags.boolean({ - description: "Don't save session (ephemeral)", - }), - models: Flags.string({ - description: "Comma-separated model patterns for Ctrl+P cycling", - }), - "no-tools": Flags.boolean({ - description: "Disable all built-in tools", - }), - "no-lsp": Flags.boolean({ - description: "Disable LSP tools, formatting, and diagnostics", - }), - "no-pty": Flags.boolean({ - description: "Disable PTY-based interactive bash execution", - }), - tools: Flags.string({ - description: "Comma-separated list of tools to enable (default: all)", - }), - thinking: Flags.string({ - description: `Set thinking level: ${CLI_THINKING_LEVELS.join(", ")}`, - options: [...CLI_THINKING_LEVELS], - }), - "hide-thinking": Flags.boolean({ - description: "Hide thinking blocks in TUI output (display only, does not disable model thinking)", - }), - advisor: Flags.boolean({ - description: "Enable the advisor runtime (passively reviews each turn and injects notes)", - }), - hook: Flags.string({ - description: "Load a hook/extension file (can be used multiple times)", - multiple: true, - }), - extension: Flags.string({ - char: "e", - description: "Load an extension file (can be used multiple times)", - multiple: true, - }), - "no-extensions": Flags.boolean({ - description: "Disable extension discovery (explicit -e paths still work)", - }), - "no-skills": Flags.boolean({ - description: "Disable skills discovery and loading", - }), - skills: Flags.string({ - description: "Comma-separated glob patterns to filter skills (e.g., git-*,docker)", - }), - "no-rules": Flags.boolean({ - description: "Disable rules discovery and loading", - }), - export: Flags.string({ - description: "Export session file to HTML and exit", - }), - "no-title": Flags.boolean({ - description: "Disable title auto-generation", - }), - "print-thoughts": Flags.boolean({ - description: "Include thinking blocks in print mode text output", - }), - "max-time": Flags.string({ - description: "Stop the session after this duration (e.g., 600, 10m, 1h)", - }), - // `--auto-approve` / `--yolo`: declared here so oclif's auto-generated `--help` lists it. - // Runtime parsing happens in `cli/args.ts parseArgs` (line 176 in that file) — `runRootCommand` - // consumes the manual-parser output, not these oclif flag values. If you rename or remove - // either form, update both call sites in lockstep. - "auto-approve": Flags.boolean({ - aliases: ["yolo"], - description: "Auto-approve all tool calls (skip approval prompts)", - }), - // `--approval-mode`: declared here so oclif's auto-generated `--help` lists it; runtime parsing - // happens in `cli/args.ts parseArgs`. The value is applied via `Settings.override("tools.approvalMode", …)` - // in `main.ts` after the `Settings` instance is constructed, so every `settings.get("tools.approvalMode")` - // site (wrapper, `/settings` UI) observes the same value. - "approval-mode": Flags.string({ - options: ["always-ask", "write", "yolo"], - description: "Override tools.approvalMode for this session (always-ask|write|yolo)", - }), - }; - - static examples = [ - `# Interactive mode\n ${APP_NAME}`, - `# Interactive mode with initial prompt\n ${APP_NAME} "List all .ts files in src/"`, - `# Include files in initial message\n ${APP_NAME} @prompt.md @image.png "What color is the sky?"`, - `# Non-interactive mode (process and exit)\n ${APP_NAME} -p "List all .ts files in src/"`, - `# Continue previous session\n ${APP_NAME} --continue "What did we discuss?"`, - `# Create a shell shortcut for a work profile\n ${APP_NAME} --profile work --alias omp-work`, - `# Use different model (fuzzy matching)\n ${APP_NAME} --model opus "Help me refactor this code"`, - `# Limit model cycling to specific models\n ${APP_NAME} --models claude-sonnet,claude-haiku,gpt-4o`, - `# Export a session file to HTML\n ${APP_NAME} --export ~/.omp/agent/sessions/--path--/session.jsonl`, - ]; + static description = launchHelp.description; + static hidden = launchHelp.hidden; + static args = launchHelp.args; + static flags = launchHelp.flags; + static examples = launchHelp.examples; static strict = false; diff --git a/packages/coding-agent/src/commands/models.ts b/packages/coding-agent/src/commands/models.ts index 8bc2fecbc..0bb30e916 100644 --- a/packages/coding-agent/src/commands/models.ts +++ b/packages/coding-agent/src/commands/models.ts @@ -1,13 +1,14 @@ /** * List, search, and refresh available models. */ + import { APP_NAME } from "@oh-my-pi/pi-utils"; import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { modelsHelp as commandHelp } from "../cli/command-help"; import { resolveModelsArgs, runModelsCommand } from "../cli/models-cli"; export default class Models extends Command { - static description = "List, search, and refresh available models"; - + static description = commandHelp.description; static args = { action: Args.string({ description: "ls (default) | find | refresh | ", diff --git a/packages/coding-agent/src/commands/plugin.ts b/packages/coding-agent/src/commands/plugin.ts index 9fed148a6..e26e11c0a 100644 --- a/packages/coding-agent/src/commands/plugin.ts +++ b/packages/coding-agent/src/commands/plugin.ts @@ -1,7 +1,9 @@ /** * Manage plugins (install, uninstall, list, etc.). */ + import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { pluginHelp as commandHelp } from "../cli/command-help"; import { type PluginAction, type PluginCommandArgs, runPluginCommand } from "../cli/plugin-cli"; import { initTheme } from "../modes/theme/theme"; @@ -21,8 +23,7 @@ const ACTIONS: PluginAction[] = [ ]; export default class Plugin extends Command { - static description = "Manage plugins (install, uninstall, list, etc.)"; - + static description = commandHelp.description; static args = { action: Args.string({ description: "Plugin action", diff --git a/packages/coding-agent/src/commands/read.ts b/packages/coding-agent/src/commands/read.ts index c8e8c8a32..a5ca06a5c 100644 --- a/packages/coding-agent/src/commands/read.ts +++ b/packages/coding-agent/src/commands/read.ts @@ -1,13 +1,14 @@ /** * Show what the read tool will return for a path, URL, or internal URI. */ + import { Args, Command } from "@oh-my-pi/pi-utils/cli"; +import { readHelp as commandHelp } from "../cli/command-help"; import { type ReadCommandArgs, runReadCommand } from "../cli/read-cli"; import { initTheme } from "../modes/theme/theme"; export default class Read extends Command { - static description = "Show what the read tool will return for a path, URL, or internal URI"; - + static description = commandHelp.description; static args = { path: Args.string({ description: diff --git a/packages/coding-agent/src/commands/say.ts b/packages/coding-agent/src/commands/say.ts index 11a747e94..44294b490 100644 --- a/packages/coding-agent/src/commands/say.ts +++ b/packages/coding-agent/src/commands/say.ts @@ -8,9 +8,11 @@ * streamed segments into one WAV. The first run downloads the configured local * model into the worker's cache. */ + import { getProjectDir } from "@oh-my-pi/pi-utils"; import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; import chalk from "chalk"; +import { sayHelp as commandHelp } from "../cli/command-help"; import { Settings, settings } from "../config/settings"; import { TTS_LOCAL_VOICE_VALUES } from "../tts/models"; import { SpeakableStream } from "../tts/speakable"; @@ -19,8 +21,7 @@ import { shutdownTtsClient, ttsClient } from "../tts/tts-client"; import { encodeWav } from "../tts/wav"; export default class Say extends Command { - static description = "Synthesize text with the local TTS engine and play it through the speakers"; - + static description = commandHelp.description; static args = { text: Args.string({ description: "Text to speak (or use --file)" }), }; diff --git a/packages/coding-agent/src/commands/setup.ts b/packages/coding-agent/src/commands/setup.ts index 70e7ad975..b7d7d6dc5 100644 --- a/packages/coding-agent/src/commands/setup.ts +++ b/packages/coding-agent/src/commands/setup.ts @@ -1,8 +1,10 @@ /** * Run onboarding setup or install dependencies for optional features. */ + import { Args, Command, Flags, renderCommandHelp } from "@oh-my-pi/pi-utils/cli"; import { parseArgs } from "../cli/args"; +import { setupHelp as commandHelp } from "../cli/command-help"; import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli"; import { runRootCommand } from "../main"; import { initTheme } from "../modes/theme/theme"; @@ -29,8 +31,7 @@ export async function runOnboardingSetup(deps: OnboardingSetupDependencies = {}) } export default class Setup extends Command { - static description = "Run onboarding setup or install dependencies for optional features"; - + static description = commandHelp.description; static args = { component: Args.string({ description: "Optional component to install", diff --git a/packages/coding-agent/src/commands/shell.ts b/packages/coding-agent/src/commands/shell.ts index f2459de0b..01b38fb28 100644 --- a/packages/coding-agent/src/commands/shell.ts +++ b/packages/coding-agent/src/commands/shell.ts @@ -1,13 +1,14 @@ /** * Interactive shell console. */ + import { Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { shellHelp as commandHelp } from "../cli/command-help"; import { runShellCommand, type ShellCommandArgs } from "../cli/shell-cli"; import { initTheme } from "../modes/theme/theme"; export default class Shell extends Command { - static description = "Interactive shell console"; - + static description = commandHelp.description; static flags = { cwd: Flags.string({ char: "C", description: "Set working directory for commands" }), timeout: Flags.integer({ char: "t", description: "Timeout per command in milliseconds" }), diff --git a/packages/coding-agent/src/commands/ssh.ts b/packages/coding-agent/src/commands/ssh.ts index 0446fb1aa..3bea4fd03 100644 --- a/packages/coding-agent/src/commands/ssh.ts +++ b/packages/coding-agent/src/commands/ssh.ts @@ -1,15 +1,16 @@ /** * Manage SSH host configurations. */ + import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { sshHelp as commandHelp } from "../cli/command-help"; import { runSSHCommand, type SSHAction, type SSHCommandArgs } from "../cli/ssh-cli"; import { initTheme } from "../modes/theme/theme"; const ACTIONS: SSHAction[] = ["add", "remove", "list"]; export default class SSH extends Command { - static description = "Manage SSH host configurations"; - + static description = commandHelp.description; static args = { action: Args.string({ description: "SSH action", diff --git a/packages/coding-agent/src/commands/stats.ts b/packages/coding-agent/src/commands/stats.ts index 06bf9d807..a52800c80 100644 --- a/packages/coding-agent/src/commands/stats.ts +++ b/packages/coding-agent/src/commands/stats.ts @@ -1,13 +1,14 @@ /** * View usage statistics dashboard. */ + import { Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { statsHelp as commandHelp } from "../cli/command-help"; import { runStatsCommand, type StatsCommandArgs } from "../cli/stats-cli"; import { initTheme } from "../modes/theme/theme"; export default class Stats extends Command { - static description = "View usage statistics"; - + static description = commandHelp.description; static flags = { port: Flags.integer({ char: "p", description: "Port for the dashboard server", default: 3847 }), json: Flags.boolean({ char: "j", description: "Output stats as JSON", default: false }), diff --git a/packages/coding-agent/src/commands/tiny-models.ts b/packages/coding-agent/src/commands/tiny-models.ts index 5ae036b03..04e55ec43 100644 --- a/packages/coding-agent/src/commands/tiny-models.ts +++ b/packages/coding-agent/src/commands/tiny-models.ts @@ -1,11 +1,11 @@ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { tinyModelsHelp as commandHelp } from "../cli/command-help"; import { runTinyModelsCommand, type TinyModelsAction, type TinyModelsCommandArgs } from "../cli/tiny-models-cli"; const ACTIONS: TinyModelsAction[] = ["download", "list"]; export default class TinyModels extends Command { - static description = "Download tiny local models (session titles + memory)"; - + static description = commandHelp.description; static args = { action: Args.string({ description: "Action to perform", diff --git a/packages/coding-agent/src/commands/token.ts b/packages/coding-agent/src/commands/token.ts index 620294b67..b3c6a4cc4 100644 --- a/packages/coding-agent/src/commands/token.ts +++ b/packages/coding-agent/src/commands/token.ts @@ -5,13 +5,13 @@ import { PROVIDER_REGISTRY } from "@oh-my-pi/pi-ai"; import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; import chalk from "chalk"; +import { tokenHelp as commandHelp } from "../cli/command-help"; import { isAuthenticated, ModelRegistry } from "../config/model-registry"; import { discoverAuthStorage } from "../sdk"; import { getAvailableAuthMethods } from "../web/search/providers/perplexity-auth"; export default class Token extends Command { - static description = "Get the API key or OAuth token for a provider"; - + static description = commandHelp.description; static args = { provider: Args.string({ description: "Provider ID (e.g. anthropic, openai)", diff --git a/packages/coding-agent/src/commands/ttsr.ts b/packages/coding-agent/src/commands/ttsr.ts index 361cbc334..041a1a48b 100644 --- a/packages/coding-agent/src/commands/ttsr.ts +++ b/packages/coding-agent/src/commands/ttsr.ts @@ -8,6 +8,7 @@ import * as path from "node:path"; * shows every TTSR-registered rule the current project/user config would load. */ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { ttsrHelp as commandHelp } from "../cli/command-help"; import { runTtsrCommand, TTSR_ACTIONS, @@ -19,8 +20,7 @@ import { import type { TtsrMatchSource } from "../export/ttsr"; export default class Ttsr extends Command { - static description = "Inspect and test Time-Traveling Stream Rules (TTSR)"; - + static description = commandHelp.description; static args = { action: Args.string({ description: "TTSR action", diff --git a/packages/coding-agent/src/commands/update.ts b/packages/coding-agent/src/commands/update.ts index 34276938a..7da0197b8 100644 --- a/packages/coding-agent/src/commands/update.ts +++ b/packages/coding-agent/src/commands/update.ts @@ -1,14 +1,15 @@ /** * Check for and install updates. */ + import { Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { updateHelp as commandHelp } from "../cli/command-help"; import * as pluginCli from "../cli/plugin-cli"; import * as updateCli from "../cli/update-cli"; import { initTheme } from "../modes/theme/theme"; export default class Update extends Command { - static description = "Check for and install updates"; - + static description = commandHelp.description; static flags = { force: Flags.boolean({ char: "f", description: "Force update", default: false }), check: Flags.boolean({ char: "c", description: "Check for updates without installing", default: false }), diff --git a/packages/coding-agent/src/commands/usage.ts b/packages/coding-agent/src/commands/usage.ts index bfaf3a3f3..7c05cade0 100644 --- a/packages/coding-agent/src/commands/usage.ts +++ b/packages/coding-agent/src/commands/usage.ts @@ -1,12 +1,13 @@ /** * Show provider usage limits for every authenticated account. */ + import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { usageHelp as commandHelp } from "../cli/command-help"; import { runUsageCommand } from "../cli/usage-cli"; export default class Usage extends Command { - static description = "Show provider usage limits for every authenticated account"; - + static description = commandHelp.description; static args = { action: Args.string({ description: "Optional subcommand to execute", diff --git a/packages/coding-agent/src/commands/web-search.ts b/packages/coding-agent/src/commands/web-search.ts index fc890749e..f5c300414 100644 --- a/packages/coding-agent/src/commands/web-search.ts +++ b/packages/coding-agent/src/commands/web-search.ts @@ -1,7 +1,9 @@ /** * Test web search providers. */ + import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { searchHelp as commandHelp } from "../cli/command-help"; import { runSearchCommand, type SearchCommandArgs } from "../cli/web-search-cli"; import { SEARCH_PROVIDER_ORDER } from "../web/search/provider"; @@ -10,8 +12,7 @@ const PROVIDERS: Array = ["auto", ...SEARCH_PROVIDER_ORDER]; const RECENCY: NonNullable[] = ["day", "week", "month", "year"]; export default class Search extends Command { - static description = "Test web search providers"; - + static description = commandHelp.description; static aliases = ["q"]; static args = { diff --git a/packages/coding-agent/src/commands/worktree.ts b/packages/coding-agent/src/commands/worktree.ts index f5c110591..b52392db7 100644 --- a/packages/coding-agent/src/commands/worktree.ts +++ b/packages/coding-agent/src/commands/worktree.ts @@ -1,14 +1,15 @@ /** * List and clean up agent-managed git worktrees under `~/.omp/wt`. */ + import { getProjectDir } from "@oh-my-pi/pi-utils"; import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli"; +import { worktreeHelp as commandHelp } from "../cli/command-help"; import { clearWorktrees, listWorktrees } from "../cli/worktree-cli"; import { Settings } from "../config/settings"; export default class Worktree extends Command { - static description = "List or clear agent-managed git worktrees (~/.omp/wt)"; - + static description = commandHelp.description; static aliases = ["wt"]; static args = { diff --git a/packages/coding-agent/src/thinking.ts b/packages/coding-agent/src/thinking.ts index 565c62060..b96e5d725 100644 --- a/packages/coding-agent/src/thinking.ts +++ b/packages/coding-agent/src/thinking.ts @@ -3,6 +3,8 @@ import { Effort, type Model, THINKING_EFFORTS } from "@oh-my-pi/pi-ai"; import { clampThinkingLevelForModel, getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking"; import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models"; +export { CLI_THINKING_LEVELS } from "./cli/thinking-levels"; + /** * Metadata used to render thinking selector values in the coding-agent UI. */ @@ -209,14 +211,6 @@ export function getConfiguredThinkingLevelMetadata(level: ConfiguredThinkingLeve return level === AUTO_THINKING ? AUTO_THINKING_METADATA : getThinkingLevelMetadata(level); } -/** - * Thinking selectors accepted by the `--thinking` CLI flag, in display order: - * `off`, every concrete effort (`minimal`..`max`), then `auto`. Single source - * for the flag's `options` list, shell completions, and the "invalid level" - * warning so all three stay in sync. - */ -export const CLI_THINKING_LEVELS: readonly string[] = [ThinkingLevel.Off, ...THINKING_EFFORTS, AUTO_THINKING]; - /** * Parses a `--thinking` CLI value. Accepts every {@link parseConfiguredThinkingLevel} * selector (`off`, `auto`, `minimal`..`max`) but rejects diff --git a/packages/coding-agent/test/cli-command-metadata.test.ts b/packages/coding-agent/test/cli-command-metadata.test.ts new file mode 100644 index 000000000..69524b6be --- /dev/null +++ b/packages/coding-agent/test/cli-command-metadata.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it, spyOn } from "bun:test"; +import { + type CliConfig, + type CommandCtor, + type CommandMetadata, + renderCommandHelp, + renderRootHelp, +} from "@oh-my-pi/pi-utils/cli"; +import { commands } from "../src/cli-commands"; + +function captureStdout(render: () => void): string { + const chunks: string[] = []; + const stdoutSpy = spyOn(process.stdout, "write").mockImplementation(chunk => { + chunks.push(String(chunk)); + return true; + }); + try { + render(); + } finally { + stdoutSpy.mockRestore(); + } + return chunks.join(""); +} + +describe("CLI command help metadata", () => { + it("renders the same root help as the loaded command classes", async () => { + const metadata = new Map(); + const constructors = new Map(); + for (const entry of commands) { + expect(entry.help, `${entry.name} must provide static help metadata`).toBeDefined(); + if (!entry.help) continue; + metadata.set(entry.name, entry.help); + constructors.set(entry.name, await entry.load()); + } + + const base = { bin: "omp", version: "test" }; + const metadataConfig: CliConfig = { ...base, commands: metadata }; + const constructorConfig: CliConfig = { ...base, commands: constructors }; + const metadataRoot = captureStdout(() => renderRootHelp(metadataConfig)); + const constructorRoot = captureStdout(() => renderRootHelp(constructorConfig)); + expect(metadataRoot).toBe(constructorRoot); + + const visibleNames = commands.filter(entry => !entry.help?.hidden).map(entry => entry.name); + const maxNameLength = Math.max(...visibleNames.map(name => name.length)); + for (const name of visibleNames) { + const Command = constructors.get(name); + if (!Command) throw new Error(`Missing loaded command: ${name}`); + const commandOutput = captureStdout(() => renderCommandHelp("omp", name, Command)); + const description = commandOutput.split("\n", 1)[0]; + expect(metadataRoot).toContain(` ${name.padEnd(maxNameLength + 2)}${description}`); + } + }); +}); diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index aedff8635..b5a01a644 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -20,6 +20,9 @@ ### Fixed - Fixed Bun test-runtime detection treating application-owned `NODE_ENV=test` and `BUN_ENV=test` values as test-runner signals ([#7261](https://github.com/can1357/oh-my-pi/issues/7261)). +### Changed + +- Added static command metadata support to the lightweight CLI runner so root help can render without importing command implementations. ## [17.2.1] - 2026-07-30 diff --git a/packages/utils/src/cli.ts b/packages/utils/src/cli.ts index dfd10b76b..65a7a4d8a 100644 --- a/packages/utils/src/cli.ts +++ b/packages/utils/src/cli.ts @@ -133,23 +133,26 @@ export interface ParseOutput< // Command base class // --------------------------------------------------------------------------- -export interface CommandCtor { - new (argv: string[], config: CliConfig): Command; +export interface CommandMetadata { description?: string; hidden?: boolean; - strict?: boolean; - aliases?: string[]; - examples?: string[]; flags?: Record; args?: Record; + examples?: string[]; +} + +export interface CommandCtor extends CommandMetadata { + new (argv: string[], config: CliConfig): Command; + strict?: boolean; + aliases?: string[]; } /** Configuration passed to every command instance and help renderers. */ -export interface CliConfig { +export interface CliConfig { bin: string; version: string; /** All registered commands keyed by their canonical name. */ - commands: Map; + commands: Map; } /** Minimal Command base matching the oclif surface we use. */ @@ -290,7 +293,7 @@ export abstract class Command { // --------------------------------------------------------------------------- /** Render full root help: header, default command details, subcommand list. */ -export function renderRootHelp(config: CliConfig): void { +export function renderRootHelp(config: CliConfig): void { const { bin, version, commands } = config; const lines: string[] = []; lines.push(`${bin} v${version}\n`); @@ -299,7 +302,7 @@ export function renderRootHelp(config: CliConfig): void { // Show the default command's flags/args/examples inline. // The default command is the one marked hidden (it's the implicit entry point). - const defaultCmd = [...commands.values()].find(C => C.hidden); + const defaultCmd = [...commands.values()].find(command => command.hidden); if (defaultCmd) { renderCommandBody(lines, defaultCmd); } @@ -309,8 +312,8 @@ export function renderRootHelp(config: CliConfig): void { if (visible.length > 0) { lines.push("COMMANDS"); const maxLen = Math.max(...visible.map(([n]) => n.length)); - for (const [name, C] of visible.sort((a, b) => a[0].localeCompare(b[0]))) { - lines.push(` ${name.padEnd(maxLen + 2)}${C.description ?? ""}`); + for (const [name, command] of visible.sort((a, b) => a[0].localeCompare(b[0]))) { + lines.push(` ${name.padEnd(maxLen + 2)}${command.description ?? ""}`); } lines.push(""); } @@ -350,9 +353,9 @@ export function renderCommandHelp(bin: string, id: string, Cmd: CommandCtor): vo process.stdout.write(lines.join("\n")); } -function renderCommandBody(lines: string[], Cmd: CommandCtor): void { - const argDefs = Cmd.args ?? {}; - const flagDefs = Cmd.flags ?? {}; +function renderCommandBody(lines: string[], command: CommandMetadata): void { + const argDefs = command.args ?? {}; + const flagDefs = command.flags ?? {}; // Arguments const argEntries = Object.entries(argDefs); @@ -387,9 +390,9 @@ function renderCommandBody(lines: string[], Cmd: CommandCtor): void { } // Examples - if (Cmd.examples && Cmd.examples.length > 0) { + if (command.examples && command.examples.length > 0) { lines.push("EXAMPLES"); - for (const ex of Cmd.examples) { + for (const ex of command.examples) { for (const line of ex.split("\n")) { lines.push(` ${line}`); } @@ -406,6 +409,7 @@ function renderCommandBody(lines: string[], Cmd: CommandCtor): void { export interface CommandEntry { name: string; load: () => Promise; + help?: CommandMetadata; aliases?: string[]; } @@ -414,8 +418,10 @@ export interface RunOptions { version: string; argv: string[]; commands: CommandEntry[]; - /** Custom help renderer. Receives fully-populated config. */ + /** Custom help renderer with the fully loaded command constructors. */ help?: (config: CliConfig) => Promise | void; + /** Lightweight help renderer backed by static command metadata. */ + metadataHelp?: (config: CliConfig) => Promise | void; } /** Find a command entry by exact name or alias. */ @@ -437,11 +443,15 @@ export async function run(opts: RunOptions): Promise { // Top-level help if (commandId === "--help" || commandId === "-h" || commandId === "help" || commandId === "") { - const config = await loadAllCommands(opts); if (opts.help) { - await opts.help(config); + await opts.help(await loadAllCommands(opts)); } else { - renderRootHelp(config); + const config = await loadAllCommandMetadata(opts); + if (opts.metadataHelp) { + await opts.metadataHelp(config); + } else { + renderRootHelp(config); + } } return; } @@ -504,12 +514,16 @@ async function loadEntry(entry: CommandEntry): Promise { return Cmd; } -/** Resolve all command loaders for help/alias display. */ +/** Load every command constructor for backward-compatible custom help callbacks. */ async function loadAllCommands(opts: RunOptions): Promise { - const commands = new Map(); - const loaded = await Promise.all(opts.commands.map(async e => [e.name, await loadEntry(e)] as const)); - for (const [name, Cmd] of loaded) { - commands.set(name, Cmd); - } - return { bin: opts.bin, version: opts.version, commands }; + const loaded = await Promise.all(opts.commands.map(async entry => [entry.name, await loadEntry(entry)] as const)); + return { bin: opts.bin, version: opts.version, commands: new Map(loaded) }; +} + +/** Resolve static command metadata for lightweight root help. */ +async function loadAllCommandMetadata(opts: RunOptions): Promise> { + const loaded = await Promise.all( + opts.commands.map(async entry => [entry.name, entry.help ?? (await loadEntry(entry))] as const), + ); + return { bin: opts.bin, version: opts.version, commands: new Map(loaded) }; } diff --git a/packages/utils/test/cli-help.test.ts b/packages/utils/test/cli-help.test.ts index 98c260ab9..80f9e5450 100644 --- a/packages/utils/test/cli-help.test.ts +++ b/packages/utils/test/cli-help.test.ts @@ -54,6 +54,73 @@ describe("run() per-command help", () => { }); }); +describe("run() root help", () => { + // Contract: root help renders registered metadata without importing command + // implementations. Heavy or unavailable optional commands must not make + // `omp --help` slow or crash. + it("renders static metadata without loading command modules", async () => { + let loads = 0; + const commands: CommandEntry[] = [ + { + name: "launch", + load: async () => { + loads++; + throw new Error("runtime graph loaded"); + }, + help: { + hidden: true, + flags: { model: Flags.string({ description: "model selector" }) }, + }, + }, + { + name: "good", + load: async () => { + loads++; + throw new Error("runtime graph loaded"); + }, + help: { description: "prints good things" }, + }, + ]; + const writes: string[] = []; + const stdoutSpy = spyOn(process.stdout, "write").mockImplementation(chunk => { + writes.push(String(chunk)); + return true; + }); + try { + await run({ bin: "omp", version: "0.0.0", argv: ["--help"], commands }); + } finally { + stdoutSpy.mockRestore(); + } + const output = writes.join(""); + expect(loads).toBe(0); + expect(output).toContain("--model="); + expect(output).toContain("good prints good things"); + }); + + it("preserves constructable commands for existing custom help callbacks", async () => { + const commands: CommandEntry[] = [ + { name: "good", load: async () => GoodCommand, help: { description: "static summary" } }, + ]; + let receivedConstructor = false; + + await run({ + bin: "omp", + version: "0.0.0", + argv: ["--help"], + commands, + help: config => { + const Command = config.commands.get("good"); + expect(Command).toBe(GoodCommand); + if (Command) { + receivedConstructor = new Command([], config) instanceof GoodCommand; + } + }, + }); + + expect(receivedConstructor).toBe(true); + }); +}); + describe("run() usage errors", () => { // Contract: a missing required arg prints a concise `error:` + USAGE line to // stderr and exits 1 — it must NOT throw past run() (which would dump a