diff --git a/hermes_cli/_parser.py b/hermes_cli/_parser.py index b3c3bd273b..577568ad85 100644 --- a/hermes_cli/_parser.py +++ b/hermes_cli/_parser.py @@ -1,8 +1,8 @@ """Top-level argparse construction for the hermes CLI. Only the top-level parser and the ``chat`` subparser live here. Every other subparser (model, -gateway, sessions, …) is built inline in ``main.py`` because its dispatch is tightly coupled to -module-level ``cmd_*`` functions. +gateway, sessions, …) is built by ``hermes_cli/subcommands/.py`` and wired in +``main._build_cli_parser`` with its ``cmd_*`` handler injected. """ import argparse @@ -120,20 +120,8 @@ For more help on a command: """ -def build_top_level_parser(): - """Build the top-level parser, the subparsers action, and the ``chat`` subparser. - - Returns ``(parser, subparsers, chat_parser)``; the caller wires - ``chat_parser.set_defaults(func= cmd_chat)`` and registers further subparsers via - ``subparsers.add_parser(...)``. - """ - parser = argparse.ArgumentParser( - prog="hermes", - description="Hermes Agent - AI assistant with tool-calling capabilities", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=_EPILOGUE, - ) - +def _add_top_level_flags(parser: argparse.ArgumentParser) -> None: + """Top-level (pre-subcommand) flags; ``-m/--provider`` pair with ``-z`` without ``chat``.""" parser.add_argument( "--version", "-V", action="store_true", help="Show version and exit" ) @@ -326,11 +314,9 @@ def build_top_level_parser(): help="With --tui: run TypeScript sources via tsx (skip dist build)", ) - subparsers = parser.add_subparsers(dest="command", help="Command to run") - # ========================================================================= - # chat command - # ========================================================================= +def _build_chat_parser(subparsers) -> argparse.ArgumentParser: + """The ``chat`` subparser (also the implicit default command).""" chat_parser = subparsers.add_parser( "chat", help="Interactive chat with the agent", @@ -592,5 +578,26 @@ def build_top_level_parser(): default=argparse.SUPPRESS, help="With --tui: run TypeScript sources via tsx (skip dist build)", ) + return chat_parser + + +def build_top_level_parser(): + """Build the top-level parser, the subparsers action, and the ``chat`` subparser. + + Returns ``(parser, subparsers, chat_parser)``; the caller wires + ``chat_parser.set_defaults(func= cmd_chat)`` and registers further subparsers via + ``subparsers.add_parser(...)``. + """ + parser = argparse.ArgumentParser( + prog="hermes", + description="Hermes Agent - AI assistant with tool-calling capabilities", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=_EPILOGUE, + ) + + _add_top_level_flags(parser) + + subparsers = parser.add_subparsers(dest="command", help="Command to run") + chat_parser = _build_chat_parser(subparsers) return parser, subparsers, chat_parser diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 1cdaac75ac..aed000b311 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -416,7 +416,6 @@ if _try_ultrafast_version(): import argparse import json import re -import shlex import shutil import subprocess from pathlib import Path @@ -1803,13 +1802,16 @@ def _resolve_continue_arg(args, *, use_tui: bool) -> None: sys.exit(1) -def cmd_chat(args): - """Run interactive chat CLI.""" - _apply_safe_mode(args) - _apply_user_config_bypass(args) - _guard_noninteractive_user_config(args) - use_tui = _resolve_use_tui(args) +def _resolve_chat_session_args(args, use_tui: bool) -> None: + """Normalize --in / --resume / --continue on ``args`` before agent init. + Order matters: ``--in DIR`` chdirs first so workspace-scoped "latest"/-c + lookups key off DIR (and pins the session there, skipping cwd restore); + then ``--resume latest`` → MRU id, ``--continue`` → ``--resume``, + ``--resume @claude/@codex`` → imported session id, title → id; finally + cd back into a resumed session's recorded cwd (best-effort, opt-out via + --no-restore-cwd, skipped under --worktree). + """ # --in DIR: run in DIR. Must happen before any session resolution so the # workspace-scoped "latest"/-c lookups key off DIR, and it pins the # session there — an explicit --in wins over a resumed session's @@ -1919,7 +1921,9 @@ def cmd_chat(args): except Exception: pass - # xAI retirement warning — one-shot, non-blocking, never fails startup + +def _warn_retired_xai_models() -> None: + """One-shot xAI retirement warning on stderr; non-blocking, never fails startup.""" try: from hermes_cli.xai_retirement import ( MIGRATION_GUIDE_URL, @@ -1942,38 +1946,17 @@ def cmd_chat(args): except Exception: pass - # First-run guard: check if any provider is configured before launching - if not _has_any_provider_configured(): - print() - print( - "It looks like Hermes isn't configured yet -- no API keys or providers found." - ) - print() - print(" Run: hermes setup") - print() - from hermes_cli.setup import ( - is_interactive_stdin, - print_noninteractive_setup_guidance, - ) - - if not is_interactive_stdin(): - print_noninteractive_setup_guidance( - "No interactive TTY detected for the first-run setup prompt." - ) - sys.exit(1) - - try: - reply = input("Run setup now? [Y/n] ").strip().lower() - except (EOFError, KeyboardInterrupt): - reply = "n" - if reply in {"", "y", "yes"}: - cmd_setup(args) - return - print() - print("You can run 'hermes setup' at any time to configure.") - sys.exit(1) +def _start_chat_background_prefetch() -> None: + """Kick off the update-check/banner prefetch and the bundled-skills sync. + Update check is opt-in on Termux (it imports rich/prompt_toolkit in the + foreground and competes for CPU on single-core devices). The skills sync + is idempotent and hash-gated, so it normally runs in a daemon thread; + the ONE exception is an unseeded ~/.hermes/skills — there the banner + prefetch would race the sync and cache an empty skills index, so the + first run syncs in the foreground and drops the banner's skills cache. + """ # Start update check in background (runs while other init happens). # On Termux this imports rich/prompt_toolkit in the foreground and then # competes for CPU on single-core devices, so keep it opt-in there. @@ -2033,6 +2016,52 @@ def cmd_chat(args): target=_skills_sync_bg, name="bundled-skills-sync", daemon=True ).start() + +def cmd_chat(args): + """Run interactive chat CLI.""" + _apply_safe_mode(args) + _apply_user_config_bypass(args) + _guard_noninteractive_user_config(args) + use_tui = _resolve_use_tui(args) + + _resolve_chat_session_args(args, use_tui) + + _warn_retired_xai_models() + + # First-run guard: check if any provider is configured before launching + if not _has_any_provider_configured(): + print() + print( + "It looks like Hermes isn't configured yet -- no API keys or providers found." + ) + print() + print(" Run: hermes setup") + print() + + from hermes_cli.setup import ( + is_interactive_stdin, + print_noninteractive_setup_guidance, + ) + + if not is_interactive_stdin(): + print_noninteractive_setup_guidance( + "No interactive TTY detected for the first-run setup prompt." + ) + sys.exit(1) + + try: + reply = input("Run setup now? [Y/n] ").strip().lower() + except (EOFError, KeyboardInterrupt): + reply = "n" + if reply in {"", "y", "yes"}: + cmd_setup(args) + return + print() + print("You can run 'hermes setup' at any time to configure.") + sys.exit(1) + + _start_chat_background_prefetch() + # --yolo: bypass all dangerous command approvals. # Also set in main() before _prepare_agent_startup() — that is the # authoritative site because it runs before tool imports freeze