From 9df30679300742cc2700bf5de2dc3bbc699d14d7 Mon Sep 17 00:00:00 2001 From: Brent <67750428+eggpeat@users.noreply.github.com> Date: Fri, 31 Jul 2026 21:28:41 +0000 Subject: [PATCH 1/5] perf(cli): keep root help off runtime graph --- packages/coding-agent/CHANGELOG.md | 1 + packages/coding-agent/src/cli-commands.ts | 195 ++++++++++++++--- packages/coding-agent/src/cli.ts | 27 ++- packages/coding-agent/src/cli/args.ts | 91 +------- .../coding-agent/src/cli/completion-gen.ts | 10 +- packages/coding-agent/src/cli/help-extra.ts | 88 ++++++++ .../coding-agent/src/cli/thinking-levels.ts | 14 ++ .../coding-agent/src/commands/launch-help.ts | 111 ++++++++++ packages/coding-agent/src/commands/launch.ts | 198 +----------------- packages/coding-agent/src/thinking.ts | 10 +- .../test/eval/process-entry-import.test.ts | 11 +- packages/utils/CHANGELOG.md | 4 + packages/utils/src/cli.ts | 42 ++-- packages/utils/test/cli-help.test.ts | 44 ++++ 14 files changed, 492 insertions(+), 354 deletions(-) create mode 100644 packages/coding-agent/src/cli/help-extra.ts create mode 100644 packages/coding-agent/src/cli/thinking-levels.ts create mode 100644 packages/coding-agent/src/commands/launch-help.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3f76ce653..451438f96 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -91,6 +91,7 @@ - Optimized tool guidance for bash, grep, and glob to be more concise while clarifying shell boundaries and search timeouts. - Optimized models configuration resource probing to run in a single child process, reducing startup contention. - Startup release notes now default to a compact change-count summary. Use `startup.changelogMode` (`summary` | `expanded` | `hidden`) to control them; legacy `collapseChangelog` choices migrate automatically ([#6771](https://github.com/can1357/oh-my-pi/issues/6771)). +- Reduced `omp --help` cold-start latency and memory use by rendering lightweight command metadata without loading runtime command, provider, or native-addon graphs. ### Fixed diff --git a/packages/coding-agent/src/cli-commands.ts b/packages/coding-agent/src/cli-commands.ts index 17fd742e8..e9b60a030 100644 --- a/packages/coding-agent/src/cli-commands.ts +++ b/packages/coding-agent/src/cli-commands.ts @@ -10,41 +10,170 @@ */ import type { CommandEntry } from "@oh-my-pi/pi-utils/cli"; 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: "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: { description: "Run Oh My Pi as an ACP (Agent Client Protocol) server over stdio" }, + }, + { + name: "auth-broker", + load: () => import("./commands/auth-broker").then(m => m.default), + help: { description: "Manage the omp auth-broker (credential vault)" }, + }, + { + name: "auth-gateway", + load: () => import("./commands/auth-gateway").then(m => m.default), + help: { description: "Run an auth-gateway forward proxy backed by the configured broker" }, + }, + { + name: "agents", + load: () => import("./commands/agents").then(m => m.default), + help: { description: "Manage bundled task agents" }, + }, + { + name: "bench", + load: () => import("./commands/bench").then(m => m.default), + help: { + description: "Benchmark models with the same prompt: time-to-first-token and generation throughput (tokens/s)", + }, + }, + { + name: "cleanse", + load: () => import("./commands/cleanse").then(m => m.default), + help: { description: "Detect and fix project diagnostics with weighted parallel subagents" }, + }, + { + name: "commit", + load: () => import("./commands/commit").then(m => m.default), + help: { description: "Generate a commit message and update changelogs" }, + }, + { + name: "completions", + load: () => import("./commands/completions").then(m => m.default), + help: { description: "Print a shell completion script (bash, zsh, or fish)" }, + }, + { name: "__complete", load: () => import("./commands/complete").then(m => m.default), help: { hidden: true } }, + { + name: "config", + load: () => import("./commands/config").then(m => m.default), + help: { description: "Manage configuration settings" }, + }, + { + name: "dry-balance", + load: () => import("./commands/dry-balance").then(m => m.default), + help: { description: "Dry-run OAuth account balancing across random session ids" }, + }, + { + name: "gc", + load: () => import("./commands/gc").then(m => m.default), + help: { description: "Run storage garbage collection" }, + }, + { + name: "grep", + load: () => import("./commands/grep").then(m => m.default), + help: { description: "Test grep tool" }, + }, + { + name: "gallery", + load: () => import("./commands/gallery").then(m => m.default), + help: { description: "Preview tool renderers across streaming, in-progress, success, and failure states" }, + }, + { + name: "grievances", + load: () => import("./commands/grievances").then(m => m.default), + help: { description: "View, clean, or push reported tool issues (auto-QA grievances)" }, + }, + { + name: "install", + load: () => import("./commands/install").then(m => m.default), + help: { description: "Install or link an extension package (alias of `plugin install`/`plugin link`)" }, + }, + { + name: "join", + load: () => import("./commands/join").then(m => m.default), + help: { description: "Join a shared collab session (same as /join)" }, + }, + { + name: "models", + load: () => import("./commands/models").then(m => m.default), + help: { description: "List, search, and refresh available models" }, + }, + { + name: "plugin", + load: () => import("./commands/plugin").then(m => m.default), + help: { description: "Manage plugins (install, uninstall, list, etc.)" }, + }, + { + name: "say", + load: () => import("./commands/say").then(m => m.default), + help: { description: "Synthesize text with the local TTS engine and play it through the speakers" }, + }, + { + name: "setup", + load: () => import("./commands/setup").then(m => m.default), + help: { description: "Run onboarding setup or install dependencies for optional features" }, + }, + { + name: "shell", + load: () => import("./commands/shell").then(m => m.default), + help: { description: "Interactive shell console" }, + }, + { + name: "read", + load: () => import("./commands/read").then(m => m.default), + help: { description: "Show what the read tool will return for a path, URL, or internal URI" }, + }, + { + name: "ssh", + load: () => import("./commands/ssh").then(m => m.default), + help: { description: "Manage SSH host configurations" }, + }, + { + name: "stats", + load: () => import("./commands/stats").then(m => m.default), + help: { description: "View usage statistics" }, + }, + { + name: "update", + load: () => import("./commands/update").then(m => m.default), + help: { description: "Check for and install updates" }, + }, + { + name: "usage", + load: () => import("./commands/usage").then(m => m.default), + help: { description: "Show provider usage limits for every authenticated account" }, + }, + { + name: "tiny-models", + load: () => import("./commands/tiny-models").then(m => m.default), + help: { description: "Download tiny local models (session titles + memory)" }, + }, + { + name: "token", + load: () => import("./commands/token").then(m => m.default), + help: { description: "Get the API key or OAuth token for a provider" }, + }, + { + name: "ttsr", + load: () => import("./commands/ttsr").then(m => m.default), + help: { description: "Inspect and test Time-Traveling Stream Rules (TTSR)" }, + }, + { + name: "worktree", + load: () => import("./commands/worktree").then(m => m.default), + aliases: ["wt"], + help: { description: "List or clear agent-managed git worktrees (~/.omp/wt)" }, + }, + { + name: "search", + load: () => import("./commands/web-search").then(m => m.default), + aliases: ["q"], + help: { description: "Test web search providers" }, + }, ]; // 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 2a5d4d6e4..492ac7797 100755 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -29,13 +29,10 @@ import { setProcessName } from "@oh-my-pi/pi-utils/process-name"; import { declareWorkerHostEntry, installWorkerInbox, isWorkerHostSelector } from "@oh-my-pi/pi-utils/worker-host"; import { installProfileAlias, resolveProfileAliasCommandFromProcess } from "./cli/profile-alias"; import { extractProfileFlags } from "./cli/profile-bootstrap"; -import { startJsEvalProcess } from "./eval/js/process-entry"; import type { WorkerInbound as JsWorkerInbound, WorkerOutbound as JsWorkerOutbound } from "./eval/js/worker-protocol"; import { DAEMON_BROKER_WORKER_ARG } from "./launch/protocol"; import { TERMINAL_OUTPUT_WORKER_ARG } from "./launch/terminal-output-worker-protocol"; import { COMPUTER_WORKER_ARG } from "./tools/computer/protocol"; -import { smokeTestComputerWorker } from "./tools/computer/supervisor"; -import { startComputerWorker } from "./tools/computer/worker-entry"; if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) { process.stderr.write( @@ -61,8 +58,13 @@ const isProcessEntry = import.meta.main || process.env.PI_COMPILED === "true"; // 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"); + // Root help historically loads the selected profile's environment. Keep that + // contract after profile bootstrap without pulling in command/provider graphs. + await import("@oh-my-pi/pi-utils/env"); + 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) { @@ -86,6 +88,7 @@ async function runSmokeTest(): Promise { const { smokeTestSttWorker } = await import("./stt/asr-client"); const { smokeTestTtsWorker } = await import("./tts/tts-client"); const { smokeTestMnemopiEmbedWorker } = await import("./mnemopi/embed-client"); + const { smokeTestComputerWorker } = await import("./tools/computer/supervisor"); const { smokeTestJsEvalWorker } = await import("./eval/js/context-manager"); // Other smoke dependencies stay lazy so normal CLI startup does not load their worker clients. const { smokeTestDaemonBroker } = await import("./launch/client"); @@ -163,6 +166,8 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise { } if (arg === COMPUTER_WORKER_ARG) { if (parentPort) installWorkerInbox(parentPort); + // This selector is the module-loading boundary; desktop capture dependencies are worker-only. + const { startComputerWorker } = await import("./tools/computer/worker-entry"); startComputerWorker(); return true; } @@ -172,11 +177,13 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise { return true; } if (arg === JS_EVAL_PROCESS_ARG) { - // The bootstrap-safe interceptor seam is linked statically so this selector - // cannot load profile-scoped environment state after dispatch has begun. - // The JS evaluator forwards user-controlled payloads (tool-call args, - // display outputs); a non-serializable one must fail that cell, not - // SIGKILL the kernel and erase the eval session's state. + // This selector is the module-loading boundary; the evaluator process entry is worker-only. + // The bootstrap-safe interceptor stays linked statically so profile-scoped + // environment state cannot load after dispatch has begun. The JS evaluator + // forwards user-controlled payloads (tool-call args, display outputs); a + // non-serializable one must fail that cell, not SIGKILL the kernel and erase + // the eval session's state. + const { startJsEvalProcess } = await import("./eval/js/process-entry"); await runIpcSubprocessWorker( transport => startJsEvalProcess(transport, interceptUnhandledRejections), { rethrowConnectedSendErrors: true }, diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index da606c156..179e5774b 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 { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils"; +import { 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 { @@ -330,92 +333,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/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..129766226 --- /dev/null +++ b/packages/coding-agent/src/cli/help-extra.ts @@ -0,0 +1,88 @@ +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..0898f5581 --- /dev/null +++ b/packages/coding-agent/src/cli/thinking-levels.ts @@ -0,0 +1,14 @@ +/** + * 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", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", + "auto", +]; 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/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/eval/process-entry-import.test.ts b/packages/coding-agent/test/eval/process-entry-import.test.ts index d6bb57961..0c12d8d43 100644 --- a/packages/coding-agent/test/eval/process-entry-import.test.ts +++ b/packages/coding-agent/test/eval/process-entry-import.test.ts @@ -47,18 +47,25 @@ async function pingComputerWorker( } } -it("starts ordinary CLI paths without loading the native computer addon", async () => { +it("starts lightweight CLI paths without loading the native addon", async () => { const cliPath = path.resolve(import.meta.dir, "../../src/cli.ts"); for (const args of [ ["--no-addons", cliPath, "--version"], [cliPath, "--help"], ]) { const proc = Bun.spawn([process.execPath, ...args], { + env: { ...process.env, PI_DEBUG_STARTUP: "1" }, stdout: "pipe", stderr: "pipe", }); - const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]); + const [exitCode, stdout, stderr] = await Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + ]); expect(exitCode, `${args.at(-1)}: ${stderr}`).toBe(0); + expect(stdout).toContain(args.at(-1) === "--help" ? "USAGE" : "omp/"); + expect(stderr).not.toContain("native:loadNative"); } // Two cold CLI spawns (`--version`, `--help`) per run; the assertion is the exit // code, not the wall time. diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index 27466e466..a5fee29f9 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -9,6 +9,10 @@ - Added a `postmortem.quit` configuration option to safely handle shutdown paths when the terminal output has already disconnected. - Added project-keyed OMP security-state directory helpers under the user state root. +### Changed + +- Added static command metadata support to the lightweight CLI runner so root help can render without importing command implementations. + ## [17.1.8] - 2026-07-28 ### Added diff --git a/packages/utils/src/cli.ts b/packages/utils/src/cli.ts index dfd10b76b..ca15b854c 100644 --- a/packages/utils/src/cli.ts +++ b/packages/utils/src/cli.ts @@ -133,15 +133,18 @@ 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. */ @@ -149,7 +152,7 @@ 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. */ @@ -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[]; } @@ -506,10 +510,12 @@ async function loadEntry(entry: CommandEntry): Promise { /** Resolve all command loaders for help/alias display. */ 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); + const commands = new Map(); + const loaded = await Promise.all( + opts.commands.map(async entry => [entry.name, entry.help ?? (await loadEntry(entry))] as const), + ); + for (const [name, command] of loaded) { + commands.set(name, command); } return { bin: opts.bin, version: opts.version, commands }; } diff --git a/packages/utils/test/cli-help.test.ts b/packages/utils/test/cli-help.test.ts index 98c260ab9..cdeee02e9 100644 --- a/packages/utils/test/cli-help.test.ts +++ b/packages/utils/test/cli-help.test.ts @@ -54,6 +54,50 @@ 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"); + }); +}); + 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 From a572fcb9be4cceee35110387529856c470741e1f Mon Sep 17 00:00:00 2001 From: Brent <67750428+eggpeat@users.noreply.github.com> Date: Fri, 31 Jul 2026 22:01:19 +0000 Subject: [PATCH 2/5] fix(cli): preserve help metadata contracts --- packages/coding-agent/CHANGELOG.md | 5 ++- packages/coding-agent/src/cli.ts | 24 ++++++------- .../coding-agent/src/cli/thinking-levels.ts | 13 ++----- .../test/cli-command-metadata.test.ts | 30 ++++++++++++++++ .../test/eval/process-entry-import.test.ts | 11 ++---- packages/utils/CHANGELOG.md | 8 ++--- packages/utils/src/cli.ts | 34 ++++++++++++------- packages/utils/test/cli-help.test.ts | 23 +++++++++++++ 8 files changed, 98 insertions(+), 50 deletions(-) create mode 100644 packages/coding-agent/test/cli-command-metadata.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 451438f96..8f7bde306 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### 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 @@ -91,7 +95,6 @@ - Optimized tool guidance for bash, grep, and glob to be more concise while clarifying shell boundaries and search timeouts. - Optimized models configuration resource probing to run in a single child process, reducing startup contention. - Startup release notes now default to a compact change-count summary. Use `startup.changelogMode` (`summary` | `expanded` | `hidden`) to control them; legacy `collapseChangelog` choices migrate automatically ([#6771](https://github.com/can1357/oh-my-pi/issues/6771)). -- Reduced `omp --help` cold-start latency and memory use by rendering lightweight command metadata without loading runtime command, provider, or native-addon graphs. ### Fixed diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 492ac7797..20c7ca0b7 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, @@ -29,10 +29,13 @@ import { setProcessName } from "@oh-my-pi/pi-utils/process-name"; import { declareWorkerHostEntry, installWorkerInbox, isWorkerHostSelector } from "@oh-my-pi/pi-utils/worker-host"; import { installProfileAlias, resolveProfileAliasCommandFromProcess } from "./cli/profile-alias"; import { extractProfileFlags } from "./cli/profile-bootstrap"; +import { startJsEvalProcess } from "./eval/js/process-entry"; import type { WorkerInbound as JsWorkerInbound, WorkerOutbound as JsWorkerOutbound } from "./eval/js/worker-protocol"; import { DAEMON_BROKER_WORKER_ARG } from "./launch/protocol"; import { TERMINAL_OUTPUT_WORKER_ARG } from "./launch/terminal-output-worker-protocol"; import { COMPUTER_WORKER_ARG } from "./tools/computer/protocol"; +import { smokeTestComputerWorker } from "./tools/computer/supervisor"; +import { startComputerWorker } from "./tools/computer/worker-entry"; if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) { process.stderr.write( @@ -57,7 +60,7 @@ 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 { +async function showHelp(config: CliConfig): Promise { // Root help historically loads the selected profile's environment. Keep that // contract after profile bootstrap without pulling in command/provider graphs. await import("@oh-my-pi/pi-utils/env"); @@ -88,7 +91,6 @@ async function runSmokeTest(): Promise { const { smokeTestSttWorker } = await import("./stt/asr-client"); const { smokeTestTtsWorker } = await import("./tts/tts-client"); const { smokeTestMnemopiEmbedWorker } = await import("./mnemopi/embed-client"); - const { smokeTestComputerWorker } = await import("./tools/computer/supervisor"); const { smokeTestJsEvalWorker } = await import("./eval/js/context-manager"); // Other smoke dependencies stay lazy so normal CLI startup does not load their worker clients. const { smokeTestDaemonBroker } = await import("./launch/client"); @@ -166,8 +168,6 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise { } if (arg === COMPUTER_WORKER_ARG) { if (parentPort) installWorkerInbox(parentPort); - // This selector is the module-loading boundary; desktop capture dependencies are worker-only. - const { startComputerWorker } = await import("./tools/computer/worker-entry"); startComputerWorker(); return true; } @@ -177,13 +177,11 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise { return true; } if (arg === JS_EVAL_PROCESS_ARG) { - // This selector is the module-loading boundary; the evaluator process entry is worker-only. - // The bootstrap-safe interceptor stays linked statically so profile-scoped - // environment state cannot load after dispatch has begun. The JS evaluator - // forwards user-controlled payloads (tool-call args, display outputs); a - // non-serializable one must fail that cell, not SIGKILL the kernel and erase - // the eval session's state. - const { startJsEvalProcess } = await import("./eval/js/process-entry"); + // The bootstrap-safe interceptor seam is linked statically so this selector + // cannot load profile-scoped environment state after dispatch has begun. + // The JS evaluator forwards user-controlled payloads (tool-call args, + // display outputs); a non-serializable one must fail that cell, not + // SIGKILL the kernel and erase the eval session's state. await runIpcSubprocessWorker( transport => startJsEvalProcess(transport, interceptUnhandledRejections), { rethrowConnectedSendErrors: true }, @@ -404,7 +402,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/thinking-levels.ts b/packages/coding-agent/src/cli/thinking-levels.ts index 0898f5581..e954f2f30 100644 --- a/packages/coding-agent/src/cli/thinking-levels.ts +++ b/packages/coding-agent/src/cli/thinking-levels.ts @@ -1,14 +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", - "minimal", - "low", - "medium", - "high", - "xhigh", - "max", - "auto", -]; +export const CLI_THINKING_LEVELS: readonly string[] = ["off", ...THINKING_EFFORTS, "auto"]; 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..221af0518 --- /dev/null +++ b/packages/coding-agent/test/cli-command-metadata.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "bun:test"; +import type { CommandMetadata } from "@oh-my-pi/pi-utils/cli"; +import { commands } from "../src/cli-commands"; + +const METADATA_KEYS = [ + "description", + "hidden", + "flags", + "args", + "examples", +] as const satisfies readonly (keyof CommandMetadata)[]; + +describe("CLI command help metadata", () => { + it("is complete and matches every loaded command", async () => { + for (const entry of commands) { + const help = entry.help; + expect(help, `${entry.name} must provide static help metadata`).toBeDefined(); + if (!help) continue; + + const Command = await entry.load(); + for (const key of METADATA_KEYS) { + if (help[key] !== undefined) { + const expected: unknown = help[key]; + const actual: unknown = Command[key]; + expect(expected, `${entry.name}.${key} drifted from its command class`).toEqual(actual); + } + } + } + }); +}); diff --git a/packages/coding-agent/test/eval/process-entry-import.test.ts b/packages/coding-agent/test/eval/process-entry-import.test.ts index 0c12d8d43..d6bb57961 100644 --- a/packages/coding-agent/test/eval/process-entry-import.test.ts +++ b/packages/coding-agent/test/eval/process-entry-import.test.ts @@ -47,25 +47,18 @@ async function pingComputerWorker( } } -it("starts lightweight CLI paths without loading the native addon", async () => { +it("starts ordinary CLI paths without loading the native computer addon", async () => { const cliPath = path.resolve(import.meta.dir, "../../src/cli.ts"); for (const args of [ ["--no-addons", cliPath, "--version"], [cliPath, "--help"], ]) { const proc = Bun.spawn([process.execPath, ...args], { - env: { ...process.env, PI_DEBUG_STARTUP: "1" }, stdout: "pipe", stderr: "pipe", }); - const [exitCode, stdout, stderr] = await Promise.all([ - proc.exited, - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - ]); + const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]); expect(exitCode, `${args.at(-1)}: ${stderr}`).toBe(0); - expect(stdout).toContain(args.at(-1) === "--help" ? "USAGE" : "omp/"); - expect(stderr).not.toContain("native:loadNative"); } // Two cold CLI spawns (`--version`, `--help`) per run; the assertion is the exit // code, not the wall time. diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index a5fee29f9..3443fc6d5 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### 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 ### Added @@ -9,10 +13,6 @@ - Added a `postmortem.quit` configuration option to safely handle shutdown paths when the terminal output has already disconnected. - Added project-keyed OMP security-state directory helpers under the user state root. -### Changed - -- Added static command metadata support to the lightweight CLI runner so root help can render without importing command implementations. - ## [17.1.8] - 2026-07-28 ### Added diff --git a/packages/utils/src/cli.ts b/packages/utils/src/cli.ts index ca15b854c..65a7a4d8a 100644 --- a/packages/utils/src/cli.ts +++ b/packages/utils/src/cli.ts @@ -148,11 +148,11 @@ export interface CommandCtor extends CommandMetadata { } /** 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. */ @@ -293,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`); @@ -418,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. */ @@ -441,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; } @@ -508,14 +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 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), ); - for (const [name, command] of loaded) { - commands.set(name, command); - } - return { bin: opts.bin, version: opts.version, commands }; + 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 cdeee02e9..80f9e5450 100644 --- a/packages/utils/test/cli-help.test.ts +++ b/packages/utils/test/cli-help.test.ts @@ -96,6 +96,29 @@ describe("run() root help", () => { 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", () => { From c8871419e2812b2762161c41109f8dcc57dd38e3 Mon Sep 17 00:00:00 2001 From: Brent <67750428+eggpeat@users.noreply.github.com> Date: Fri, 31 Jul 2026 22:49:04 +0000 Subject: [PATCH 3/5] fix(cli): statically link help environment setup --- packages/coding-agent/src/cli.ts | 5 ++--- packages/coding-agent/src/cli/help-extra.ts | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 20c7ca0b7..96d9da2d5 100755 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -61,9 +61,8 @@ const isProcessEntry = import.meta.main || process.env.PI_COMPILED === "true"; // import time, so it must not be imported before `setProfile` runs. async function showHelp(config: CliConfig): Promise { - // Root help historically loads the selected profile's environment. Keep that - // contract after profile bootstrap without pulling in command/provider graphs. - await import("@oh-my-pi/pi-utils/env"); + // 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"), diff --git a/packages/coding-agent/src/cli/help-extra.ts b/packages/coding-agent/src/cli/help-extra.ts index 129766226..22f6985bb 100644 --- a/packages/coding-agent/src/cli/help-extra.ts +++ b/packages/coding-agent/src/cli/help-extra.ts @@ -1,3 +1,4 @@ +import "@oh-my-pi/pi-utils/env"; import { APP_NAME, CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils/dirs"; import chalk from "chalk"; From dc4d15e59d89040980a9094bd8547d45ddf092cb Mon Sep 17 00:00:00 2001 From: Brent <67750428+eggpeat@users.noreply.github.com> Date: Sat, 1 Aug 2026 00:29:57 +0000 Subject: [PATCH 4/5] fix(cli): centralize command help metadata --- packages/coding-agent/src/cli-commands.ts | 71 ++++++------- packages/coding-agent/src/cli/command-help.ts | 99 +++++++++++++++++++ packages/coding-agent/src/commands/acp.ts | 4 +- packages/coding-agent/src/commands/agents.ts | 5 +- .../coding-agent/src/commands/auth-broker.ts | 5 +- .../coding-agent/src/commands/auth-gateway.ts | 5 +- packages/coding-agent/src/commands/bench.ts | 5 +- packages/coding-agent/src/commands/cleanse.ts | 4 +- packages/coding-agent/src/commands/commit.ts | 5 +- .../coding-agent/src/commands/complete.ts | 3 +- .../coding-agent/src/commands/completions.ts | 5 +- packages/coding-agent/src/commands/config.ts | 5 +- .../coding-agent/src/commands/dry-balance.ts | 4 +- packages/coding-agent/src/commands/gallery.ts | 5 +- packages/coding-agent/src/commands/gc.ts | 5 +- packages/coding-agent/src/commands/grep.ts | 5 +- .../coding-agent/src/commands/grievances.ts | 5 +- packages/coding-agent/src/commands/install.ts | 4 +- packages/coding-agent/src/commands/join.ts | 5 +- packages/coding-agent/src/commands/models.ts | 5 +- packages/coding-agent/src/commands/plugin.ts | 5 +- packages/coding-agent/src/commands/read.ts | 5 +- packages/coding-agent/src/commands/say.ts | 5 +- packages/coding-agent/src/commands/setup.ts | 5 +- packages/coding-agent/src/commands/shell.ts | 5 +- packages/coding-agent/src/commands/ssh.ts | 5 +- packages/coding-agent/src/commands/stats.ts | 5 +- .../coding-agent/src/commands/tiny-models.ts | 4 +- packages/coding-agent/src/commands/token.ts | 4 +- packages/coding-agent/src/commands/ttsr.ts | 4 +- packages/coding-agent/src/commands/update.ts | 5 +- packages/coding-agent/src/commands/usage.ts | 5 +- .../coding-agent/src/commands/web-search.ts | 5 +- .../coding-agent/src/commands/worktree.ts | 5 +- 34 files changed, 224 insertions(+), 97 deletions(-) create mode 100644 packages/coding-agent/src/cli/command-help.ts diff --git a/packages/coding-agent/src/cli-commands.ts b/packages/coding-agent/src/cli-commands.ts index e9b60a030..8139257c2 100644 --- a/packages/coding-agent/src/cli-commands.ts +++ b/packages/coding-agent/src/cli-commands.ts @@ -9,6 +9,7 @@ * 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"; @@ -17,162 +18,164 @@ export const commands: CommandEntry[] = [ { name: "acp", load: () => import("./commands/acp").then(m => m.default), - help: { description: "Run Oh My Pi as an ACP (Agent Client Protocol) server over stdio" }, + help: commandHelp.acpHelp, }, { name: "auth-broker", load: () => import("./commands/auth-broker").then(m => m.default), - help: { description: "Manage the omp auth-broker (credential vault)" }, + help: commandHelp.authBrokerHelp, }, { name: "auth-gateway", load: () => import("./commands/auth-gateway").then(m => m.default), - help: { description: "Run an auth-gateway forward proxy backed by the configured broker" }, + help: commandHelp.authGatewayHelp, }, { name: "agents", load: () => import("./commands/agents").then(m => m.default), - help: { description: "Manage bundled task agents" }, + help: commandHelp.agentsHelp, }, { name: "bench", load: () => import("./commands/bench").then(m => m.default), - help: { - description: "Benchmark models with the same prompt: time-to-first-token and generation throughput (tokens/s)", - }, + help: commandHelp.benchHelp, }, { name: "cleanse", load: () => import("./commands/cleanse").then(m => m.default), - help: { description: "Detect and fix project diagnostics with weighted parallel subagents" }, + help: commandHelp.cleanseHelp, }, { name: "commit", load: () => import("./commands/commit").then(m => m.default), - help: { description: "Generate a commit message and update changelogs" }, + help: commandHelp.commitHelp, }, { name: "completions", load: () => import("./commands/completions").then(m => m.default), - help: { description: "Print a shell completion script (bash, zsh, or fish)" }, + help: commandHelp.completionsHelp, + }, + { + name: "__complete", + load: () => import("./commands/complete").then(m => m.default), + help: commandHelp.completeHelp, }, - { name: "__complete", load: () => import("./commands/complete").then(m => m.default), help: { hidden: true } }, { name: "config", load: () => import("./commands/config").then(m => m.default), - help: { description: "Manage configuration settings" }, + help: commandHelp.configHelp, }, { name: "dry-balance", load: () => import("./commands/dry-balance").then(m => m.default), - help: { description: "Dry-run OAuth account balancing across random session ids" }, + help: commandHelp.dryBalanceHelp, }, { name: "gc", load: () => import("./commands/gc").then(m => m.default), - help: { description: "Run storage garbage collection" }, + help: commandHelp.gcHelp, }, { name: "grep", load: () => import("./commands/grep").then(m => m.default), - help: { description: "Test grep tool" }, + help: commandHelp.grepHelp, }, { name: "gallery", load: () => import("./commands/gallery").then(m => m.default), - help: { description: "Preview tool renderers across streaming, in-progress, success, and failure states" }, + help: commandHelp.galleryHelp, }, { name: "grievances", load: () => import("./commands/grievances").then(m => m.default), - help: { description: "View, clean, or push reported tool issues (auto-QA grievances)" }, + help: commandHelp.grievancesHelp, }, { name: "install", load: () => import("./commands/install").then(m => m.default), - help: { description: "Install or link an extension package (alias of `plugin install`/`plugin link`)" }, + help: commandHelp.installHelp, }, { name: "join", load: () => import("./commands/join").then(m => m.default), - help: { description: "Join a shared collab session (same as /join)" }, + help: commandHelp.joinHelp, }, { name: "models", load: () => import("./commands/models").then(m => m.default), - help: { description: "List, search, and refresh available models" }, + help: commandHelp.modelsHelp, }, { name: "plugin", load: () => import("./commands/plugin").then(m => m.default), - help: { description: "Manage plugins (install, uninstall, list, etc.)" }, + help: commandHelp.pluginHelp, }, { name: "say", load: () => import("./commands/say").then(m => m.default), - help: { description: "Synthesize text with the local TTS engine and play it through the speakers" }, + help: commandHelp.sayHelp, }, { name: "setup", load: () => import("./commands/setup").then(m => m.default), - help: { description: "Run onboarding setup or install dependencies for optional features" }, + help: commandHelp.setupHelp, }, { name: "shell", load: () => import("./commands/shell").then(m => m.default), - help: { description: "Interactive shell console" }, + help: commandHelp.shellHelp, }, { name: "read", load: () => import("./commands/read").then(m => m.default), - help: { description: "Show what the read tool will return for a path, URL, or internal URI" }, + help: commandHelp.readHelp, }, { name: "ssh", load: () => import("./commands/ssh").then(m => m.default), - help: { description: "Manage SSH host configurations" }, + help: commandHelp.sshHelp, }, { name: "stats", load: () => import("./commands/stats").then(m => m.default), - help: { description: "View usage statistics" }, + help: commandHelp.statsHelp, }, { name: "update", load: () => import("./commands/update").then(m => m.default), - help: { description: "Check for and install updates" }, + help: commandHelp.updateHelp, }, { name: "usage", load: () => import("./commands/usage").then(m => m.default), - help: { description: "Show provider usage limits for every authenticated account" }, + help: commandHelp.usageHelp, }, { name: "tiny-models", load: () => import("./commands/tiny-models").then(m => m.default), - help: { description: "Download tiny local models (session titles + memory)" }, + help: commandHelp.tinyModelsHelp, }, { name: "token", load: () => import("./commands/token").then(m => m.default), - help: { description: "Get the API key or OAuth token for a provider" }, + help: commandHelp.tokenHelp, }, { name: "ttsr", load: () => import("./commands/ttsr").then(m => m.default), - help: { description: "Inspect and test Time-Traveling Stream Rules (TTSR)" }, + help: commandHelp.ttsrHelp, }, { name: "worktree", load: () => import("./commands/worktree").then(m => m.default), aliases: ["wt"], - help: { description: "List or clear agent-managed git worktrees (~/.omp/wt)" }, + help: commandHelp.worktreeHelp, }, { name: "search", load: () => import("./commands/web-search").then(m => m.default), aliases: ["q"], - help: { description: "Test web search providers" }, + help: commandHelp.searchHelp, }, ]; 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..ae331ad49 --- /dev/null +++ b/packages/coding-agent/src/cli/command-help.ts @@ -0,0 +1,99 @@ +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 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/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/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 = { From d67597d9646f3a7b8c1f6c48d2a5df53304c9b5a Mon Sep 17 00:00:00 2001 From: Brent <67750428+eggpeat@users.noreply.github.com> Date: Sat, 1 Aug 2026 00:40:37 +0000 Subject: [PATCH 5/5] test(cli): compare rendered command help --- .../test/cli-command-metadata.test.ts | 65 +++++++++++++------ 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/packages/coding-agent/test/cli-command-metadata.test.ts b/packages/coding-agent/test/cli-command-metadata.test.ts index 221af0518..69524b6be 100644 --- a/packages/coding-agent/test/cli-command-metadata.test.ts +++ b/packages/coding-agent/test/cli-command-metadata.test.ts @@ -1,30 +1,53 @@ -import { describe, expect, it } from "bun:test"; -import type { CommandMetadata } from "@oh-my-pi/pi-utils/cli"; +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"; -const METADATA_KEYS = [ - "description", - "hidden", - "flags", - "args", - "examples", -] as const satisfies readonly (keyof CommandMetadata)[]; +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("is complete and matches every loaded command", async () => { + 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) { - const help = entry.help; - expect(help, `${entry.name} must provide static help metadata`).toBeDefined(); - if (!help) continue; + 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 Command = await entry.load(); - for (const key of METADATA_KEYS) { - if (help[key] !== undefined) { - const expected: unknown = help[key]; - const actual: unknown = Command[key]; - expect(expected, `${entry.name}.${key} drifted from its command class`).toEqual(actual); - } - } + 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}`); } }); });