From 3c56df9e29a42c86f378c6079ce26b34bddfbd5c Mon Sep 17 00:00:00 2001 From: Tommy-Lee Bannert Date: Fri, 1 May 2026 18:41:26 +0200 Subject: [PATCH] fix: prevent nested subparser commands from falling through to chat REPL When using nested subparsers (e.g. `hermes mcp add`), Python 3.11's argparse may leave args.func set to the parent parser's default (cmd_chat) instead of propagating the child subparser's func. This caused MCP, cron, and gateway subcommands to incorrectly enter the chat REPL. Fix: check for subcommand-specific attributes (mcp_action, cron_command, gateway_command) when args.func == cmd_chat, and dispatch to the correct handler if one is found. Closes #4184 --- hermes_cli/main.py | 51 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 72a958b573d14..7101e25066f75 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -10312,8 +10312,35 @@ def cmd_acp(args): cmd_chat(args) return - # Default to chat if no command specified - if args.command is None: + # Execute the command — check func first so nested subparsers that + # correctly set func (e.g. `hermes mcp add`) are dispatched even when + # args.command is None due to Python 3.11 argparse quirks. + if hasattr(args, "func") and args.func is not None: + if args.func != cmd_chat: + # A real subcommand was parsed — dispatch directly. + args.func(args) + return + # args.func == cmd_chat: could be a genuine bare invocation OR a + # nested-subparser bug that didn't propagate func properly. Check + # for subcommand-specific attributes that indicate a real subcommand. + _subcommand_attrs = ("mcp_action", "cron_command", "gateway_command") + if any(getattr(args, a, None) is not None for a in _subcommand_attrs): + # A nested subcommand was parsed but func defaulted to cmd_chat + # due to argparse not propagating set_defaults from the child + # subparser. Look up the correct handler. + _fallback_handlers = { + "mcp_action": "cmd_mcp", + "cron_command": "cmd_cron", + "gateway_command": "cmd_gateway", + } + for _attr, _handler_name in _fallback_handlers.items(): + if getattr(args, _attr, None) is not None: + _handler = locals().get(_handler_name) or globals().get(_handler_name) + if _handler: + _handler(args) + return + # If handler not found, fall through to func dispatch below + # Genuine no-command invocation — fill defaults for chat for attr, default in [ ("query", None), ("model", None), @@ -10326,12 +10353,22 @@ def cmd_acp(args): ]: if not hasattr(args, attr): setattr(args, attr, default) - cmd_chat(args) - return - - # Execute the command - if hasattr(args, "func"): args.func(args) + return + elif args.command is None: + for attr, default in [ + ("query", None), + ("model", None), + ("provider", None), + ("toolsets", None), + ("verbose", False), + ("resume", None), + ("continue_last", None), + ("worktree", False), + ]: + if not hasattr(args, attr): + setattr(args, attr, default) + cmd_chat(args) else: parser.print_help()