feat: add <cli> help [subcmd] [-R] + Typer PEP 695 alias support#104
Merged
Conversation
Two related improvements to cc_lib.cli for workspace CLI quality: 1. `add_help_command(app)` — registers `<cli> help` with three modes: bare (= --help), subcommand (= <subcmd> --help with difflib typo suggestion + click.UsageError exit 2), and --recursive/-R (top-level help + every non-hidden subcommand's --help in one shot, the AI-consumable artifact this work was prompted by). 2. `Pep695AliasPatcher` — class wrapping a Typer monkey-patch so PEP 695 aliases (`type Browser = Literal[...]` in models.py) are recognized in CLI signatures. Installed lazily from create_app() so non-CLI consumers of cc_lib (hooks, tests, MCP runtime) don't mutate Typer's global state. Idempotent via sentinel marker, fails loudly when upstream PR #970 lands via end-to-end registration smoke test. Wires both into selenium-browser: `add_help_command(app)` + changes `browser: str | None` to `browser: Browser | None` at the two `navigate*` sites. `selenium-browser navigate --help` now shows `[chrome|chromium]` instead of `TEXT`; `selenium-browser help -R` produces the recursive dump (~67KB) for AI consumers. Design rationale + alternatives weighed in `docs/ai-cli-help-empirical.md`.
The empirical comparison of AI-native CLI manifest surfaces is now linked from the PR description as a secret gist rather than landing in-repo.
Two refinements to the cli.py changes from the prior commit: 1. add_help_command(): wire autocompletion= on the subcommand argument so `<cli> help <TAB>` lists subcommands and `<cli> help nav<TAB>` filters to navigate / navigate-with-profile-state. Previously fell back to _files completion. 2. Pep695AliasPatcher: convert classmethods to instance methods. Now used as Pep695AliasPatcher().install() to match CompletionInstaller's shape. No functional change; keeps door open for adding instance state later without touching every method signature.
… bugs
Propagates the Pep695AliasPatcher across selenium-browser's MCP↔CLI surface,
adopting PEP 695 aliases at every site. Fixes silent-failure modes where
the CLI accepted bogus input that downstream code rejected — exactly the
pattern --browser had before this PR.
New aliases in selenium_browser/models.py:
ScrollDirection = Literal['up', 'down', 'left', 'right']
ScrollPosition = Literal['top', 'bottom', 'left', 'right']
ScrollBehavior = Literal['instant', 'smooth']
WaitForSelectorState = Literal['visible', 'hidden', 'attached', 'detached']
ConsoleLogLevel = Literal['SEVERE', 'WARNING', 'INFO'] (data)
ConsoleLogLevelFilter = Literal['ALL', 'SEVERE', 'WARNING', 'INFO'] (filter)
New cross-CLI alias in cc-lib/cc_lib/types.py:
OutputFormat = Literal['text', 'json']
Bugs fixed (silent-failure modes verified empirically by ecosystem audit):
• scroll --direction xyz → was bare str, fell through to KeyError leak
• scroll --position xyz → same shape
• wait-for-selector --state X → bare str, all branches fall through, 30s timeout
• get-console-logs --level X → bare str, .get(default=0) silently no-filtering
• pipeline --on-error X → bare str, Pydantic catches but help text wrong
Sites changed:
• cli/main.py: 31 inline Literal['text','json'] → OutputFormat
6 type-degradation params replaced with new aliases
• models.py: ConsoleLogEntry.level, WaitForSelectorResult.state → aliases
• service.py, tools/{interaction,waiting,performance}.py: inline Literals → aliases
• scroll.py: 3 typed params + 4 bare-str helpers → aliases (full propagation)
Out of scope (deferred to follow-up):
• OutputFormat propagation to document-search (×12) and claude-session (×2)
• imessage-kit AttachmentMode/SendService propagation (drops 2 type-ignores)
• document-search StopAfterStage/FileType/SearchType propagation
• claude-session ArchiveFormat/GistVisibility cleanup
Four medium-priority fixes from PR #104 review: 1. Pep695AliasPatcher._resolve(): replace unbounded `while` with `for _ in range(32)` plus explicit TypeError. Guards against mutually-recursive aliases (`type A = B; type B = A`) that would otherwise hang at module-load time inside create_app(). 2. Extract HTTP wire-format types to selenium_browser/wire.py. Previously the CLI imported OnErrorPolicy from bridge.py, which transitively pulled FastAPI + Selenium + fastmcp through `from .service import BrowserService`. CLI cold-start drops from 3516ms → 156ms (22.5x), modules loaded 1656 → 479. bridge.py and cli/main.py both import from wire.py now. 3. Re-export add_help_command from cc_lib package __init__.py to match the other four CLI factory functions. Was exposed in cc_lib.cli.__all__ but missing at package level. 4. Pep695AliasPatcher docstring: document the coverage boundary — `type X = Literal[...] | None` (with `| None` inside the alias) fails the same way as no-patch state. Workaround: write `type X = Literal[...]` and use `X | None` at the call site. Test coverage: empirical CLI tests in PR description cover the happy paths and rejection paths end-to-end. Pytest tests deferred as scope creep — most candidate behaviors are already empirically verified (tab completion, --help rendering, parse-time rejection, typo suggestions, hidden-command exclusion).
This was referenced May 5, 2026
Merged
chrisguillory
added a commit
that referenced
this pull request
May 5, 2026
PR #104 shipped add_help_command() in cc_lib.cli and wired it only into selenium-browser as the pilot. This propagates it to every other Typer CLI in the workspace so each one supports `<cli> help`, `<cli> help <subcmd>`, and `<cli> help -R` (recursive). CLIs wired: - claude-remote-bash - claude-session - document-search - imessage-kit - claude-binary-patcher (script) - claude-exec (script — wires to ext_app) - claude-login (script) - claude-session-patcher (script) - claude-version-manager (script) - toggle-ask-before-auto-approval (script) python-interpreter is argparse-based and falls outside this helper's scope. Each change is two lines: import add_help_command, call add_help_command(app) after create_app(). No behavior change beyond adding the new subcommand. Empirical verification: imessage-kit help → top-level help (was: "No such command 'help'.") claude-session help -R | grep -c '^━━━ ' → 10 (one per subcommand) document-search help info → subcommand help (byte-equivalent to `info --help`)
chrisguillory
added a commit
that referenced
this pull request
May 6, 2026
…and across workspace CLIs (#105) * fix(cli): tighten Typer Literal annotations across imessage-kit, document-search, claude-session Closes the audit follow-up from PR #104 (which shipped PEP 695 alias support in cc_lib.cli). Replaces `str` annotations + `# type: ignore[arg-type]` + `cast()` with the existing Literal aliases at CLI/MCP boundaries so Typer enforces validation at parse time and `--help` renders valid choices. Three severity tiers in one PR (mechanical, ~50 LOC): P0 silent bugs (production foot-guns now rejected at parse time): - imessage-kit get-attachment --mode: `mode: str` accepted any value; `--mode VIEW` (caps) silently called save() instead of view() because service.py:248 falls through to save() for any value != 'view'. The comment claiming "service validates mode against AttachmentMode Literal" was false. Now: `mode: AttachmentMode` rejects bad values at parse time. - document-search index --stop-after: `cast(StopAfterStage, ...)` papered over invalid stage strings, silently running the full pipeline when the user expected early exit. Now: `stop_after: StopAfterStage | None`, Typer rejects bad values at parse time, cast removed. P1 help-text drift (visible, runtime-validated but `--help` showed `TEXT`): - imessage-kit send --service: `service_type: str` → `SendService` (CLI + MCP). Help now shows `[auto|iMessage|SMS]`. - document-search list --type: `file_type: str | None` → `FileType | None` (CLI + MCP). Help now shows the full enum (markdown, text, pdf, image, json, email, csv, jsonl). P2 DRY cleanups (no behavior change): - document-search: 12 inline `Literal['text', 'json']` sites collapsed to `OutputFormat` import from cc_lib.types. - claude-session: `_validate_archive_format` callback + `_is_archive_format` TypeGuard helper dropped; Typer enforces the Literal natively (verified: `--format zip` rejected the same way it was before). - claude-session: extracted `type GistVisibility = Literal['public', 'secret']` into new `claude_session/types.py` module, replacing 3 inline duplicates across CLI + MCP. - claude-session: `Literal['text', 'json']` (×2) → `OutputFormat`. Empirical verification (all confirmed): imessage-kit get-attachment 1 --mode VIEW → "Invalid value for '--mode': 'VIEW' is not one of 'view', 'save'." document-search index --stop-after garbage → "Invalid value for '--stop-after': 'garbage' is not one of 'scan', 'chunk', 'embed'." document-search list --type bogus → rejected with full enum claude-session archive --gist-visibility maybe → "Invalid value for '--gist-visibility': 'maybe' is not one of 'public', 'secret'." claude-session archive --format zip → still rejected after callback drop (Typer-native validation) Out of scope (separate follow-ups): - add_help_command() propagation to the other 10 workspace Typer CLIs - Lineage `Literal['text', 'tree', 'json']` extraction (different format set) - Storage-layer `Literal['public', 'secret']` propagation (5 more sites) * feat(cli): propagate add_help_command() to all 10 workspace Typer CLIs PR #104 shipped add_help_command() in cc_lib.cli and wired it only into selenium-browser as the pilot. This propagates it to every other Typer CLI in the workspace so each one supports `<cli> help`, `<cli> help <subcmd>`, and `<cli> help -R` (recursive). CLIs wired: - claude-remote-bash - claude-session - document-search - imessage-kit - claude-binary-patcher (script) - claude-exec (script — wires to ext_app) - claude-login (script) - claude-session-patcher (script) - claude-version-manager (script) - toggle-ask-before-auto-approval (script) python-interpreter is argparse-based and falls outside this helper's scope. Each change is two lines: import add_help_command, call add_help_command(app) after create_app(). No behavior change beyond adding the new subcommand. Empirical verification: imessage-kit help → top-level help (was: "No such command 'help'.") claude-session help -R | grep -c '^━━━ ' → 10 (one per subcommand) document-search help info → subcommand help (byte-equivalent to `info --help`) * refactor: propagate ArchiveFormat, GistVisibility, SearchType beyond CLI/MCP boundaries Closes the audit's deeper type-fidelity items (originally marked out of scope for #105's first cut): centralize the three Literal aliases that appeared inline in services, storage, and Pydantic schema layers. Changes: - Move `ArchiveFormat = Literal['json', 'zst']` from `claude_session/cli/main.py:80` to `claude_session/types.py` (now a PEP 695 `type` alias for consistency with `GistVisibility`). Pydantic 2.12 supports PEP 695 aliases natively. Propagated to: - `cli/main.py` (helper signature) - `mcp/main.py` (save_current_session signature) - `schemas/operations/archive.py` (ArchiveMetadata.format) - `services/archive.py` (FormatDetector.EXTENSION_MAP, detect_format arg + return, _detect_from_extension return, create_archive arg) - Propagate `GistVisibility` from `claude_session/types.py` to its remaining inline duplicate at `storage/gist.py:39`. Both CLI/MCP and the underlying storage layer now reference the same alias. - Propagate `SearchType` (already exists in `document_search/schemas/vectors.py:54`) into `document_search/cli/main.py` (×2 sites: `search --type` Annotated parameter at line 314, internal `_search_async` helper at line 615). Help text now shows `[hybrid|lexical|embedding]` instead of `TEXT`. Empirical verification: document-search search --help → shows [hybrid|lexical|embedding] document-search search hi --type bogus → rejected at parse time claude-session archive ... --format zst → archive completes successfully claude-session archive ... --format zip → rejected at parse time No behavior change for happy paths; just centralizes the source of truth for these aliases so future evolution touches one location instead of 5-8 duplicated sites. * fix: address two LOW findings from code-review-validated on PR #105 Both findings were consistency/maintainability gaps where the propagation sweep in earlier commits missed a sibling site: 1. claude-remote-bash-daemon (mcp/claude-remote-bash/.../daemon.py) is a Typer app with 6 subcommands and was missed in commit 4f96ffd despite matching every criterion (uses cc_lib.cli, Typer-based, user-facing CLI). Add `add_help_command(app)` for parity. The commit message claim "all 10 workspace Typer CLIs" is now accurate at 11 (10 from 4f96ffd + this one). 2. document-search MCP search_documents tool still had `file_types: Sequence[str] | None` plus a `cast(FileType, ft)` at the call site — exact same pattern commit 5441eda removed for the singular `file_type` on `list_documents`. Tighten to `Sequence[FileType] | None` and drop the cast (Pydantic was already validating downstream, just one frame deeper than the boundary). Empirical verification: claude-remote-bash-daemon help → top-level help (was: error) claude-remote-bash-daemon help -R → 9 subcommand sections
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three related improvements to
cc_lib.cliplus full PEP 695 alias adoption across selenium-browser's MCP↔CLI surface, fixing 5 silent-failure bugs.1.
add_help_command(app)— recursive--helpfor one-shot CLI discoveryA new
<cli> helpsubcommand with three modes:<cli> help— top-level help (byte-equivalent to--help)<cli> help <subcmd>— subcommand help; on unknown subcommand usesclick.UsageError(exit 2) plusdifflib.get_close_matchesfor typo suggestions<cli> help --recursive/<cli> help -R— top-level + every non-hidden subcommand's--helpin one shot, the AI-consumable artifactTab completion wired via
autocompletion=callback so<cli> help <TAB>lists subcommands.2.
Pep695AliasPatcher— Typer support fortype X = Literal[...]Vanilla Typer 0.23.x raises
RuntimeError: Type not yet supportedwhen registering CLI commands annotated with PEP 695 type aliases. This patcher teaches Typer'sis_literal_typeandliteral_valueshelpers to unwrap aliases via__value__. Tracked upstream as fastapi/typer#970.Properties:
create_app()so non-CLI consumers don't mutate Typer's global statecreate_app()calls in one process are safe3. selenium-browser PEP 695 alias propagation + bug fixes
Adopts PEP 695 aliases at every site in the selenium-browser MCP↔CLI surface. Fixes 5 silent-failure modes where the CLI accepted bogus input that downstream code rejected (exactly the pattern
--browserhad before this PR).New aliases in
selenium_browser/models.py:ScrollDirection=Literal['up', 'down', 'left', 'right']ScrollPosition=Literal['top', 'bottom', 'left', 'right']ScrollBehavior=Literal['instant', 'smooth']WaitForSelectorState=Literal['visible', 'hidden', 'attached', 'detached']ConsoleLogLevel=Literal['SEVERE', 'WARNING', 'INFO'](data)ConsoleLogLevelFilter=Literal['ALL', 'SEVERE', 'WARNING', 'INFO'](filter)New cross-CLI alias in
cc-lib/cc_lib/types.py:OutputFormat=Literal['text', 'json']— replaces 31 inline duplicates in selenium-browser CLIBugs fixed:
scroll --direction xyzstr→ KeyError leak through bridge[up|down|left|right]parse-time reject, exit 2scroll --position xyzstr→ KeyError leak[top|bottom|left|right]parse-time rejectwait-for-selector --state VISIBLEstr→ silently times out 30s (every branch falls through)[visible|hidden|attached|detached]parse-time rejectget-console-logs --level errorstr→ silently no-filtering (.get(default=0))[ALL|SEVERE|WARNING|INFO]parse-time rejectpipeline --on-error garbagestr→ Pydantic catches but help showsTEXT[stop|continue]shown in helpPlus
--browser(already wired):[chrome\|chromium]rendering, parse-time validation.Test plan
Empirical tests run end-to-end against the installed CLI:
diff <(selenium-browser --help) <(selenium-browser help)exits 0 (byte-equivalent)diff <(selenium-browser navigate --help) <(selenium-browser help navigate)exits 0selenium-browser help -Rproduces 34 dividers matching 34 non-hidden subcommandsselenium-browser help <TAB>autocompletes to subcommand list--help(e.g.,[up\|down\|left\|right])selenium-browser help nvigate→Did you mean 'navigate'?exit 2create_app()calls produce same patched function object idOut of scope (deferred to follow-up PRs)
add_help_command(app)into the other 10 workspace Typer CLIs (claude-session,document-search,claude-remote-bash,imessage-kit,claude-login,scripts/); one-line change per CLIOutputFormatpropagation todocument-search(×12 callsites) andclaude-session(×2)imessage-kitAttachmentMode/SendServicepropagation (drops 2# type: ignore[arg-type]; fixes--mode VIEWsilent miscall)document-searchStopAfterStage/FileType/SearchTypepropagation (drops 1cast())claude-sessionArchiveFormat(drops_validate_archive_formatcallback) +GistVisibilitycleanup@matches_reference); deferred per validator findingReference