From 060039d1697ea561729ca40fa42946355628c5a0 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:39:56 -0700 Subject: [PATCH] refactor(cli): split main() into _build_cli_parser/_parse_cli_args/_default_to_chat helpers main() is now a ~110-line orchestrator: startup prologue, parser build, container routing, bpo-9338-safe parse, --version/--yolo/--oneshot, chat default, dispatch. The two identical plugin add_parser blocks collapse into _attach_plugin_cli_command; the two default-to-chat attr loops merge. Parser tree, --help output and set_defaults are unchanged (399 parsers byte-diffed). --- hermes_cli/main.py | 739 +++++++++++++++------------------------------ 1 file changed, 248 insertions(+), 491 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index b8c6890de7..e640f16346 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -13453,6 +13453,250 @@ def _advertise_agent_env() -> None: os.environ.setdefault("HERMES_AGENT", "true") +def _attach_plugin_cli_command(subparsers, cmd_info) -> None: + """Register one plugin-provided top-level command from its descriptor.""" + plugin_parser = subparsers.add_parser( + cmd_info["name"], + help=cmd_info["help"], + description=cmd_info.get("description", ""), + formatter_class=__import__("argparse").RawDescriptionHelpFormatter, + ) + cmd_info["setup_fn"](plugin_parser) + if cmd_info.get("handler_fn") is not None: + plugin_parser.set_defaults(func=cmd_info["handler_fn"]) + + +def _register_plugin_cli_commands(subparsers) -> None: + """Plugin CLI commands — dynamically registered by memory/general plugins. + + Plugins provide a register_cli(subparser) function that builds their own + argparse tree; no hardcoded plugin commands in main.py. Skipped when the + invocation already targets a known built-in subcommand (``hermes --help``, + ``hermes logs``, ...) — eagerly importing every bundled plugin module + (google.cloud.pubsub_v1, aiohttp, grpc, PIL …) costs 500-650ms. + """ + if not _plugin_cli_discovery_needed(): + return + try: + from plugins.memory import discover_plugin_cli_commands + from hermes_cli.plugins import discover_plugins, get_plugin_manager + + seen_plugin_commands = set() + for cmd_info in discover_plugin_cli_commands(): + _attach_plugin_cli_command(subparsers, cmd_info) + seen_plugin_commands.add(cmd_info["name"]) + + discover_plugins() + # A bundled platform whose top-level CLI command is the one being + # invoked is still only a deferred entry at this point; import it + # so its register_cli_command side effect runs before we read + # _cli_commands (issue #54678). + _resolve_deferred_platform_cli_command(_first_positional_argv()) + for cmd_info in get_plugin_manager()._cli_commands.values(): + if cmd_info["name"] not in seen_plugin_commands: + _attach_plugin_cli_command(subparsers, cmd_info) + except Exception as _exc: + logging.getLogger(__name__).debug("Plugin CLI discovery failed: %s", _exc) + + +def _build_cli_parser(): + """Build the full ``hermes`` argparse tree. Returns ``(parser, subparsers)``. + + Registration ORDER is the order subcommands appear in ``hermes --help``; + keep it stable. Each group lives in ``hermes_cli/subcommands/.py`` + (handlers injected so those modules never import main) or exposes its own + ``register_cli``/``build_parser`` from a sibling module. + """ + from hermes_cli._parser import build_top_level_parser + + parser, subparsers, chat_parser = build_top_level_parser() + chat_parser.set_defaults(func=cmd_chat) + + build_model_parser(subparsers, cmd_model=cmd_model) + build_moa_parser(subparsers) + build_fallback_parser(subparsers) + build_worktree_parser(subparsers) + build_browser_parser(subparsers) + build_secrets_parser(subparsers) + # OUTBOUND iron-proxy egress firewall; ``hermes proxy`` (gateway group) is + # the separate INBOUND OAuth-aggregator reverse proxy. + build_egress_parser(subparsers) + build_migrate_parser(subparsers) + build_gateway_parser( + subparsers, cmd_gateway=cmd_gateway, cmd_proxy=cmd_proxy, cmd_gateway_enroll=cmd_gateway_enroll + ) + + # LSP is optional infrastructure — never let a registration failure + # break the CLI overall. + try: + from agent.lsp.cli import register_subparser as _lsp_register + _lsp_register(subparsers) + except Exception as _lsp_err: # noqa: BLE001 + logger.debug("LSP CLI registration failed: %s", _lsp_err) + + build_setup_parser(subparsers, cmd_setup=cmd_setup) + build_whatsapp_parser(subparsers, cmd_whatsapp=cmd_whatsapp) + build_whatsapp_cloud_parser(subparsers, cmd_whatsapp_cloud=cmd_whatsapp_cloud) + build_slack_parser(subparsers, cmd_slack=cmd_slack) + + # send command — pipe shell-script output to any configured platform + from hermes_cli.send_cmd import register_send_subparser + register_send_subparser(subparsers) + + build_login_parser(subparsers, cmd_login=cmd_login) + build_logout_parser(subparsers, cmd_logout=cmd_logout) + build_auth_parser(subparsers, cmd_auth=cmd_auth) + build_status_parser(subparsers, cmd_status=cmd_status) + build_pause_parser(subparsers) + build_cron_parser(subparsers, cmd_cron=cmd_cron) + build_sync_parser(subparsers, cmd_sync=cmd_sync) + build_webhook_parser(subparsers, cmd_webhook=cmd_webhook) + + # peer command — bot-to-bot DMs across machines (peer Hermes gateways) + from hermes_cli.subcommands.peer import build_peer_parser + build_peer_parser(subparsers) + + # portal command — Nous Portal status + Tool Gateway routing + from hermes_cli.portal_cli import add_parser as _add_portal_parser + _add_portal_parser(subparsers) + + # kanban command — multi-profile collaboration board + from hermes_cli.kanban import build_parser as _build_kanban_parser + _build_kanban_parser(subparsers).set_defaults(func=cmd_kanban) + + # project command — named, multi-folder workspaces + from hermes_cli.projects_cmd import build_parser as _build_project_parser + _build_project_parser(subparsers).set_defaults(func=cmd_project) + + build_hooks_parser(subparsers, cmd_hooks=cmd_hooks) + build_doctor_parser(subparsers, cmd_doctor=cmd_doctor) + build_verify_parser(subparsers, cmd_verify=cmd_verify) + build_security_parser(subparsers, cmd_security=cmd_security) + build_approvals_parser(subparsers, cmd_approvals=cmd_approvals) + build_dump_parser(subparsers, cmd_dump=cmd_dump) + build_debug_parser(subparsers, cmd_debug=cmd_debug) + build_backup_parser(subparsers, cmd_backup=cmd_backup) + build_checkpoints_parser(subparsers) + build_import_cmd_parser(subparsers, cmd_import=cmd_import) + build_import_agent_parser(subparsers, cmd_import_agent=cmd_import_agent) + build_config_parser(subparsers, cmd_config=cmd_config) + build_skin_parser(subparsers, cmd_skin=cmd_skin) + build_console_parser(subparsers, cmd_console=cmd_console) + build_pairing_parser(subparsers, cmd_pairing=cmd_pairing) + build_skills_parser(subparsers, cmd_skills=cmd_skills) + build_bundles_parser(subparsers) + build_plugins_parser(subparsers, cmd_plugins=cmd_plugins) + + _register_plugin_cli_commands(subparsers) + + build_curator_parser(subparsers) + build_pets_parser(subparsers) + build_journey_parser(subparsers) + build_memory_parser(subparsers, cmd_memory=cmd_memory) + build_tools_parser(subparsers, cmd_tools=cmd_tools) + build_computer_use_parser(subparsers) + build_mcp_parser(subparsers, cmd_mcp=cmd_mcp) + # Lazy indirection: sessions_cmd is only imported when the subcommand + # runs, and monkeypatches on hermes_cli.main.cmd_sessions keep working. + build_sessions_parser(subparsers, cmd_sessions=lambda a, **kw: _self().cmd_sessions(a, **kw)) + build_insights_parser(subparsers, cmd_insights=cmd_insights) + build_monitoring_parser(subparsers, cmd_monitoring=cmd_monitoring) + build_claw_parser(subparsers, cmd_claw=cmd_claw) + # NOTE: the `hermes version` subcommand was removed — `hermes --version` + # / `-V` now carries the full output including update status. + build_update_parser(subparsers, cmd_update=cmd_update) + build_uninstall_parser(subparsers, cmd_uninstall=cmd_uninstall) + build_acp_parser(subparsers, cmd_acp=cmd_acp) + build_profile_parser(subparsers, cmd_profile=cmd_profile) + build_completion_parser(subparsers, cmd_completion=cmd_completion, parser=parser) + build_dashboard_parser( + subparsers, + cmd_dashboard=cmd_dashboard, + cmd_dashboard_register=cmd_dashboard_register, + ) + # desktop (a.k.a. gui): the canonical name is "desktop"; "gui" is kept as + # a deprecated alias for one release. The Hermes-Setup.exe success screen + # tells users to run `hermes desktop`, so that name must be the one shown + # in --help (argparse promotes the primary name; aliases stay hidden). + build_gui_parser(subparsers, cmd_gui=cmd_gui) + build_logs_parser(subparsers, cmd_logs=cmd_logs) + build_prompt_size_parser(subparsers, cmd_prompt_size=cmd_prompt_size) + return parser, subparsers + + +def _parse_cli_args(parser, subparsers, argv): + """Parse ``argv`` with the bpo-9338 subparser-routing workaround. + + argv is first pre-processed so unquoted multi-word session names after + -c / -r merge into one token (``hermes -c Pokemon Agent Dev`` → + ``hermes -c 'Pokemon Agent Dev'``). + + On some Python versions (notably <3.11), argparse fails to route + subcommand tokens when the parent parser has nargs='?' optional arguments + (--continue): "unrecognized arguments: model" even though 'model' is a + registered subcommand. Fix: when argv contains a token matching a known + subcommand, set subparsers.required=True to force deterministic routing. + If that fails (e.g. 'hermes -c model' where 'model' is consumed as the + session name for --continue), fall back to the default behaviour. + """ + import io as _io + + _processed_argv = _coalesce_session_name_args(argv) + _known_cmds = ( + set(subparsers.choices.keys()) if hasattr(subparsers, "choices") else set() + ) + _has_cmd_token = any( + t in _known_cmds for t in _processed_argv if not t.startswith("-") + ) + if not _has_cmd_token: + subparsers.required = False + return parser.parse_args(_processed_argv) + + subparsers.required = True + _saved_stderr = sys.stderr + try: + sys.stderr = _io.StringIO() + args = parser.parse_args(_processed_argv) + sys.stderr = _saved_stderr + except SystemExit as exc: + sys.stderr = _saved_stderr + # Help/version flags (exit code 0) already printed output — + # re-raise immediately to avoid a second parse_args printing + # the same help text again (#10230). + if exc.code == 0: + raise + # Subcommand name was consumed as a flag value (e.g. -c model). + # Fall back to optional subparsers so argparse handles it normally. + subparsers.required = False + args = parser.parse_args(_processed_argv) + return args + + +def _default_to_chat(args) -> None: + """No subcommand given: run chat, filling the chat-parser attrs it expects.""" + # Top-level --resume / --continue is a shortcut to chat. + if args.resume or args.continue_last: + args.command = "chat" + for attr, default in [ + ("query", None), + ("model", None), + ("provider", None), + ("toolsets", None), + ("verbose", None), + ("resume", None), + ("continue_last", None), + ("worktree", False), + ]: + if not hasattr(args, attr): + setattr(args, attr, default) + cmd_chat(args) + + +def cmd_import_agent(args): + from hermes_cli.agent_import import import_agent_command + import_agent_command(args) + + def main(): """Main entry point for hermes CLI.""" # Cosmetic: make the process show up as 'hermes' instead of 'python3.11' @@ -13518,425 +13762,8 @@ def main(): if _try_fast_chat_launch(): return - from hermes_cli._parser import build_top_level_parser + parser, subparsers = _build_cli_parser() - parser, subparsers, chat_parser = build_top_level_parser() - chat_parser.set_defaults(func=cmd_chat) - - # ========================================================================= - # model command (parser built in hermes_cli/subcommands/model.py) - # ========================================================================= - build_model_parser(subparsers, cmd_model=cmd_model) - - build_moa_parser(subparsers) - - # ========================================================================= - # fallback command — manage the fallback provider chain - # ========================================================================= - build_fallback_parser(subparsers) - - # ========================================================================= - # worktree command — audit/reclaim accumulated git worktrees + branches - # ========================================================================= - build_worktree_parser(subparsers) - - - # ========================================================================= - # browser command — real-profile helpers (agent-invoked, user-approved) - # ========================================================================= - build_browser_parser(subparsers) - - - # ========================================================================= - # secrets command — external secret managers (Bitwarden, 1Password) - # ========================================================================= - build_secrets_parser(subparsers) - - # ========================================================================= - # egress command — iron-proxy outbound credential-injection firewall - # ========================================================================= - build_egress_parser(subparsers) - - # ========================================================================= - # migrate command - # ========================================================================= - build_migrate_parser(subparsers) - - # ========================================================================= - # gateway + proxy commands (parsers built in hermes_cli/subcommands/gateway.py) - # ========================================================================= - build_gateway_parser( - subparsers, cmd_gateway=cmd_gateway, cmd_proxy=cmd_proxy, cmd_gateway_enroll=cmd_gateway_enroll - ) - - # ========================================================================= - # lsp command - # ========================================================================= - try: - from agent.lsp.cli import register_subparser as _lsp_register - _lsp_register(subparsers) - except Exception as _lsp_err: # noqa: BLE001 - # LSP is optional infrastructure — never let a registration - # failure break the CLI overall. - logger.debug("LSP CLI registration failed: %s", _lsp_err) - - # ========================================================================= - # setup command (parser built in hermes_cli/subcommands/setup.py) - # ========================================================================= - build_setup_parser(subparsers, cmd_setup=cmd_setup) - - - # ========================================================================= - # whatsapp command (parser built in hermes_cli/subcommands/whatsapp.py) - # ========================================================================= - build_whatsapp_parser(subparsers, cmd_whatsapp=cmd_whatsapp) - - # ========================================================================= - # whatsapp-cloud command (official Meta Cloud API; complement to Baileys) - # ========================================================================= - build_whatsapp_cloud_parser(subparsers, cmd_whatsapp_cloud=cmd_whatsapp_cloud) - - # ========================================================================= - # slack command (parser built in hermes_cli/subcommands/slack.py) - # ========================================================================= - build_slack_parser(subparsers, cmd_slack=cmd_slack) - - # ========================================================================= - # send command — pipe shell-script output to any configured platform - # ========================================================================= - from hermes_cli.send_cmd import register_send_subparser - register_send_subparser(subparsers) - - # ========================================================================= - # login command (parser built in hermes_cli/subcommands/login.py) - # ========================================================================= - build_login_parser(subparsers, cmd_login=cmd_login) - - # ========================================================================= - # logout command (parser built in hermes_cli/subcommands/logout.py) - # ========================================================================= - build_logout_parser(subparsers, cmd_logout=cmd_logout) - - # ========================================================================= - # auth command (parser built in hermes_cli/subcommands/auth.py) - # ========================================================================= - build_auth_parser(subparsers, cmd_auth=cmd_auth) - - # ========================================================================= - # status command (parser built in hermes_cli/subcommands/status.py) - # ========================================================================= - build_status_parser(subparsers, cmd_status=cmd_status) - - # ========================================================================= - # pause / resume commands (parser built in hermes_cli/subcommands/pause.py) - # ========================================================================= - build_pause_parser(subparsers) - - # ========================================================================= - # cron command (parser built in hermes_cli/subcommands/cron.py) - # ========================================================================= - build_cron_parser(subparsers, cmd_cron=cmd_cron) - build_sync_parser(subparsers, cmd_sync=cmd_sync) - - # ========================================================================= - # webhook command (parser built in hermes_cli/subcommands/webhook.py) - # ========================================================================= - build_webhook_parser(subparsers, cmd_webhook=cmd_webhook) - - # ========================================================================= - # peer command — bot-to-bot DMs across machines (peer Hermes gateways) - # ========================================================================= - from hermes_cli.subcommands.peer import build_peer_parser - - build_peer_parser(subparsers) - - # ========================================================================= - # portal command — Nous Portal status + Tool Gateway routing - # ========================================================================= - from hermes_cli.portal_cli import add_parser as _add_portal_parser - _add_portal_parser(subparsers) - - # ========================================================================= - # kanban command — multi-profile collaboration board - # ========================================================================= - from hermes_cli.kanban import build_parser as _build_kanban_parser - - kanban_parser = _build_kanban_parser(subparsers) - kanban_parser.set_defaults(func=cmd_kanban) - - # ========================================================================= - # project command — named, multi-folder workspaces - # ========================================================================= - from hermes_cli.projects_cmd import build_parser as _build_project_parser - - project_parser = _build_project_parser(subparsers) - project_parser.set_defaults(func=cmd_project) - - # ========================================================================= - # hooks command — shell-hook inspection and management - # ========================================================================= - # hooks command (parser built in hermes_cli/subcommands/hooks.py) - # ========================================================================= - build_hooks_parser(subparsers, cmd_hooks=cmd_hooks) - - # ========================================================================= - # doctor command (parser built in hermes_cli/subcommands/doctor.py) - # ========================================================================= - build_doctor_parser(subparsers, cmd_doctor=cmd_doctor) - - # ========================================================================= - # verify command (parser built in hermes_cli/subcommands/verify.py) - # ========================================================================= - build_verify_parser(subparsers, cmd_verify=cmd_verify) - - # ========================================================================= - # security command — on-demand supply-chain audit - # ========================================================================= - # security command (parser built in hermes_cli/subcommands/security.py) - # ========================================================================= - build_security_parser(subparsers, cmd_security=cmd_security) - - # ========================================================================= - # approvals command (parser built in hermes_cli/subcommands/approvals.py) - # ========================================================================= - build_approvals_parser(subparsers, cmd_approvals=cmd_approvals) - - # ========================================================================= - # dump command (parser built in hermes_cli/subcommands/dump.py) - # ========================================================================= - build_dump_parser(subparsers, cmd_dump=cmd_dump) - - # ========================================================================= - # debug command (parser built in hermes_cli/subcommands/debug.py) - # ========================================================================= - build_debug_parser(subparsers, cmd_debug=cmd_debug) - - # ========================================================================= - # backup command (parser built in hermes_cli/subcommands/backup.py) - # ========================================================================= - build_backup_parser(subparsers, cmd_backup=cmd_backup) - - # ========================================================================= - # checkpoints command - # ========================================================================= - build_checkpoints_parser(subparsers) - - # ========================================================================= - # import command (parser built in hermes_cli/subcommands/import_cmd.py) - # ========================================================================= - build_import_cmd_parser(subparsers, cmd_import=cmd_import) - - # ========================================================================= - # import-agent command (parser: hermes_cli/subcommands/import_agent.py) - # ========================================================================= - def cmd_import_agent(args): - from hermes_cli.agent_import import import_agent_command - import_agent_command(args) - - build_import_agent_parser(subparsers, cmd_import_agent=cmd_import_agent) - - # ========================================================================= - # config command (parser built in hermes_cli/subcommands/config.py) - # ========================================================================= - build_config_parser(subparsers, cmd_config=cmd_config) - - # ========================================================================= - # skin command (parser built in hermes_cli/subcommands/skin.py) - # ========================================================================= - build_skin_parser(subparsers, cmd_skin=cmd_skin) - - # ========================================================================= - # console command (parser built in hermes_cli/subcommands/console.py) - # ========================================================================= - build_console_parser(subparsers, cmd_console=cmd_console) - - # ========================================================================= - # pairing command (parser built in hermes_cli/subcommands/pairing.py) - # ========================================================================= - build_pairing_parser(subparsers, cmd_pairing=cmd_pairing) - - # ========================================================================= - # skills command (parser built in hermes_cli/subcommands/skills.py) - # ========================================================================= - build_skills_parser(subparsers, cmd_skills=cmd_skills) - - # ========================================================================= - # bundles command — skill bundles (alias / for multiple skills) - # ========================================================================= - build_bundles_parser(subparsers) - - # ========================================================================= - # plugins command (parser built in hermes_cli/subcommands/plugins.py) - # ========================================================================= - build_plugins_parser(subparsers, cmd_plugins=cmd_plugins) - - # ========================================================================= - # Plugin CLI commands — dynamically registered by memory/general plugins. - # Plugins provide a register_cli(subparser) function that builds their - # own argparse tree. No hardcoded plugin commands in main.py. - # - # Skipped when the invocation is already targeting a known built-in - # subcommand — ``hermes --help``, ``hermes logs``, - # etc. This avoids eagerly importing every bundled plugin module - # (google.cloud.pubsub_v1, aiohttp, grpc, PIL …) which costs - # 500-650ms on typical installs. - # ========================================================================= - if _plugin_cli_discovery_needed(): - try: - from plugins.memory import discover_plugin_cli_commands - from hermes_cli.plugins import discover_plugins, get_plugin_manager - - seen_plugin_commands = set() - for cmd_info in discover_plugin_cli_commands(): - plugin_parser = subparsers.add_parser( - cmd_info["name"], - help=cmd_info["help"], - description=cmd_info.get("description", ""), - formatter_class=__import__("argparse").RawDescriptionHelpFormatter, - ) - cmd_info["setup_fn"](plugin_parser) - if cmd_info.get("handler_fn") is not None: - plugin_parser.set_defaults(func=cmd_info["handler_fn"]) - seen_plugin_commands.add(cmd_info["name"]) - - discover_plugins() - # A bundled platform whose top-level CLI command is the one being - # invoked is still only a deferred entry at this point; import it - # so its register_cli_command side effect runs before we read - # _cli_commands (issue #54678). - _resolve_deferred_platform_cli_command(_first_positional_argv()) - for cmd_info in get_plugin_manager()._cli_commands.values(): - if cmd_info["name"] in seen_plugin_commands: - continue - plugin_parser = subparsers.add_parser( - cmd_info["name"], - help=cmd_info["help"], - description=cmd_info.get("description", ""), - formatter_class=__import__("argparse").RawDescriptionHelpFormatter, - ) - cmd_info["setup_fn"](plugin_parser) - if cmd_info.get("handler_fn") is not None: - plugin_parser.set_defaults(func=cmd_info["handler_fn"]) - except Exception as _exc: - logging.getLogger(__name__).debug("Plugin CLI discovery failed: %s", _exc) - - # ========================================================================= - # curator command — background skill maintenance - # ========================================================================= - build_curator_parser(subparsers) - - # ========================================================================= - # pets command — petdex animated mascots (CLI / TUI / desktop display) - # ========================================================================= - build_pets_parser(subparsers) - - # ========================================================================= - # journey command — learned skills + memories over time, in the terminal - # ========================================================================= - build_journey_parser(subparsers) - - # ========================================================================= - # memory command (parser built in hermes_cli/subcommands/memory.py) - # ========================================================================= - build_memory_parser(subparsers, cmd_memory=cmd_memory) - - # ========================================================================= - # tools command (parser built in hermes_cli/subcommands/tools.py) - # ========================================================================= - build_tools_parser(subparsers, cmd_tools=cmd_tools) - - # ========================================================================= - # computer-use command — manage Computer Use (cua-driver) - # ========================================================================= - build_computer_use_parser(subparsers) - # ========================================================================= - # mcp command (parser built in hermes_cli/subcommands/mcp.py) - # ========================================================================= - build_mcp_parser(subparsers, cmd_mcp=cmd_mcp) - - # ========================================================================= - # sessions command - # ========================================================================= - build_sessions_parser(subparsers, cmd_sessions=lambda a, **kw: _self().cmd_sessions(a, **kw)) - - # ========================================================================= - # insights command (parser built in hermes_cli/subcommands/insights.py) - # ========================================================================= - build_insights_parser(subparsers, cmd_insights=cmd_insights) - build_monitoring_parser(subparsers, cmd_monitoring=cmd_monitoring) - - # ========================================================================= - # claw command (parser built in hermes_cli/subcommands/claw.py) - # ========================================================================= - build_claw_parser(subparsers, cmd_claw=cmd_claw) - - # NOTE: the `hermes version` subcommand was removed — `hermes --version` - # / `-V` now carries the full output including update status. - - # ========================================================================= - # update command (parser built in hermes_cli/subcommands/update.py) - # ========================================================================= - build_update_parser(subparsers, cmd_update=cmd_update) - - # ========================================================================= - # uninstall command (parser built in hermes_cli/subcommands/uninstall.py) - # ========================================================================= - build_uninstall_parser(subparsers, cmd_uninstall=cmd_uninstall) - - # ========================================================================= - # acp command (parser built in hermes_cli/subcommands/acp.py) - # ========================================================================= - build_acp_parser(subparsers, cmd_acp=cmd_acp) - - # ========================================================================= - # profile command (parser built in hermes_cli/subcommands/profile.py) - # ========================================================================= - build_profile_parser(subparsers, cmd_profile=cmd_profile) - - # ========================================================================= - # completion command - # ========================================================================= - build_completion_parser(subparsers, cmd_completion=cmd_completion, parser=parser) - - # ========================================================================= - # dashboard command (parser built in hermes_cli/subcommands/dashboard.py) - # ========================================================================= - build_dashboard_parser( - subparsers, - cmd_dashboard=cmd_dashboard, - cmd_dashboard_register=cmd_dashboard_register, - ) - - - # ========================================================================= - # desktop (a.k.a. gui) command - # - # The canonical name is "desktop"; "gui" is kept as a deprecated alias - # for one release. The Hermes-Setup.exe success screen tells users to - # run `hermes desktop` from a terminal, so the canonical name needs - # to be the one that appears in --help (argparse promotes the primary - # name; aliases stay hidden). - # ========================================================================= - # gui command (parser built in hermes_cli/subcommands/gui.py) - # ========================================================================= - build_gui_parser(subparsers, cmd_gui=cmd_gui) - - # ========================================================================= - # logs command (parser built in hermes_cli/subcommands/logs.py) - # ========================================================================= - build_logs_parser(subparsers, cmd_logs=cmd_logs) - - # ========================================================================= - # prompt-size command (parser built in hermes_cli/subcommands/prompt_size.py) - # ========================================================================= - build_prompt_size_parser(subparsers, cmd_prompt_size=cmd_prompt_size) - - # ========================================================================= - # Parse and execute - # ========================================================================= - # Pre-process argv so unquoted multi-word session names after -c / -r - # are merged into a single token before argparse sees them. - # e.g. ``hermes -c Pokemon Agent Dev`` → ``hermes -c 'Pokemon Agent Dev'`` # ── Container-aware routing ──────────────────────────────────────── # When NixOS container mode is active, route ALL subcommands into # the managed container. This MUST run before parse_args() so that @@ -13951,50 +13778,8 @@ def main(): # and raises OSError on failure (which propagates as a traceback). sys.exit(1) - _processed_argv = _coalesce_session_name_args(sys.argv[1:]) + args = _parse_cli_args(parser, subparsers, sys.argv[1:]) - # ── Defensive subparser routing (bpo-9338 workaround) ─────────── - # On some Python versions (notably <3.11), argparse fails to route - # subcommand tokens when the parent parser has nargs='?' optional - # arguments (--continue). The symptom: "unrecognized arguments: model" - # even though 'model' is a registered subcommand. - # - # Fix: when argv contains a token matching a known subcommand, set - # subparsers.required=True to force deterministic routing. If that - # fails (e.g. 'hermes -c model' where 'model' is consumed as the - # session name for --continue), fall back to the default behaviour. - import io as _io - - _known_cmds = ( - set(subparsers.choices.keys()) if hasattr(subparsers, "choices") else set() - ) - _has_cmd_token = any( - t in _known_cmds for t in _processed_argv if not t.startswith("-") - ) - - if _has_cmd_token: - subparsers.required = True - _saved_stderr = sys.stderr - try: - sys.stderr = _io.StringIO() - args = parser.parse_args(_processed_argv) - sys.stderr = _saved_stderr - except SystemExit as exc: - sys.stderr = _saved_stderr - # Help/version flags (exit code 0) already printed output — - # re-raise immediately to avoid a second parse_args printing - # the same help text again (#10230). - if exc.code == 0: - raise - # Subcommand name was consumed as a flag value (e.g. -c model). - # Fall back to optional subparsers so argparse handles it normally. - subparsers.required = False - args = parser.parse_args(_processed_argv) - else: - subparsers.required = False - args = parser.parse_args(_processed_argv) - - # Handle --version flag if args.version: cmd_version(args) return @@ -14028,37 +13813,9 @@ def main(): usage_file=getattr(args, "usage_file", None), ) - # Handle top-level --resume / --continue as shortcut to chat - if (args.resume or args.continue_last) and args.command is None: - args.command = "chat" - for attr, default in [ - ("query", None), - ("model", None), - ("provider", None), - ("toolsets", None), - ("verbose", None), - ("worktree", False), - ]: - if not hasattr(args, attr): - setattr(args, attr, default) - cmd_chat(args) - return - - # Default to chat if no command specified + # No subcommand (optionally with top-level --resume / --continue) → chat. if args.command is None: - for attr, default in [ - ("query", None), - ("model", None), - ("provider", None), - ("toolsets", None), - ("verbose", None), - ("resume", None), - ("continue_last", None), - ("worktree", False), - ]: - if not hasattr(args, attr): - setattr(args, attr, default) - cmd_chat(args) + _default_to_chat(args) return # Execute the command. Propagate the handler's return code as the