diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 6779eb1fa..8f0f16ff9 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1777,6 +1777,44 @@ def cmd_update(args): sys.exit(1) +def _coalesce_session_name_args(argv: list) -> list: + """Join unquoted multi-word session names after -c/--continue and -r/--resume. + + When a user types ``hermes -c Pokemon Agent Dev`` without quoting the + session name, argparse sees three separate tokens. This function merges + them into a single argument so argparse receives + ``['-c', 'Pokemon Agent Dev']`` instead. + + Tokens are collected after the flag until we hit another flag (``-*``) + or a known top-level subcommand. + """ + _SUBCOMMANDS = { + "chat", "model", "gateway", "setup", "whatsapp", "login", "logout", + "status", "cron", "doctor", "config", "pairing", "skills", "tools", + "sessions", "insights", "version", "update", "uninstall", + } + _SESSION_FLAGS = {"-c", "--continue", "-r", "--resume"} + + result = [] + i = 0 + while i < len(argv): + token = argv[i] + if token in _SESSION_FLAGS: + result.append(token) + i += 1 + # Collect subsequent non-flag, non-subcommand tokens as one name + parts: list = [] + while i < len(argv) and not argv[i].startswith("-") and argv[i] not in _SUBCOMMANDS: + parts.append(argv[i]) + i += 1 + if parts: + result.append(" ".join(parts)) + else: + result.append(token) + i += 1 + return result + + def main(): """Main entry point for hermes CLI.""" parser = argparse.ArgumentParser( @@ -2515,7 +2553,11 @@ For more help on a command: # ========================================================================= # Parse and execute # ========================================================================= - args = parser.parse_args() + # 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'`` + _processed_argv = _coalesce_session_name_args(sys.argv[1:]) + args = parser.parse_args(_processed_argv) # Handle --version flag if args.version: diff --git a/tests/hermes_cli/test_coalesce_session_args.py b/tests/hermes_cli/test_coalesce_session_args.py new file mode 100644 index 000000000..32866dd5e --- /dev/null +++ b/tests/hermes_cli/test_coalesce_session_args.py @@ -0,0 +1,113 @@ +"""Tests for _coalesce_session_name_args — multi-word session name merging.""" + +import pytest +from hermes_cli.main import _coalesce_session_name_args + + +class TestCoalesceSessionNameArgs: + """Ensure unquoted multi-word session names are merged into one token.""" + + # ── -c / --continue ────────────────────────────────────────────────── + + def test_continue_multiword_unquoted(self): + """hermes -c Pokemon Agent Dev → -c 'Pokemon Agent Dev'""" + assert _coalesce_session_name_args( + ["-c", "Pokemon", "Agent", "Dev"] + ) == ["-c", "Pokemon Agent Dev"] + + def test_continue_long_form_multiword(self): + """hermes --continue Pokemon Agent Dev""" + assert _coalesce_session_name_args( + ["--continue", "Pokemon", "Agent", "Dev"] + ) == ["--continue", "Pokemon Agent Dev"] + + def test_continue_single_word(self): + """hermes -c MyProject (no merging needed)""" + assert _coalesce_session_name_args(["-c", "MyProject"]) == [ + "-c", + "MyProject", + ] + + def test_continue_already_quoted(self): + """hermes -c 'Pokemon Agent Dev' (shell already merged)""" + assert _coalesce_session_name_args( + ["-c", "Pokemon Agent Dev"] + ) == ["-c", "Pokemon Agent Dev"] + + def test_continue_bare_flag(self): + """hermes -c (no name — means 'continue latest')""" + assert _coalesce_session_name_args(["-c"]) == ["-c"] + + def test_continue_followed_by_flag(self): + """hermes -c -w (no name consumed, -w stays separate)""" + assert _coalesce_session_name_args(["-c", "-w"]) == ["-c", "-w"] + + def test_continue_multiword_then_flag(self): + """hermes -c my project -w""" + assert _coalesce_session_name_args( + ["-c", "my", "project", "-w"] + ) == ["-c", "my project", "-w"] + + def test_continue_multiword_then_subcommand(self): + """hermes -c my project chat -q hello""" + assert _coalesce_session_name_args( + ["-c", "my", "project", "chat", "-q", "hello"] + ) == ["-c", "my project", "chat", "-q", "hello"] + + # ── -r / --resume ──────────────────────────────────────────────────── + + def test_resume_multiword(self): + """hermes -r My Session Name""" + assert _coalesce_session_name_args( + ["-r", "My", "Session", "Name"] + ) == ["-r", "My Session Name"] + + def test_resume_long_form_multiword(self): + """hermes --resume My Session Name""" + assert _coalesce_session_name_args( + ["--resume", "My", "Session", "Name"] + ) == ["--resume", "My Session Name"] + + def test_resume_multiword_then_flag(self): + """hermes -r My Session -w""" + assert _coalesce_session_name_args( + ["-r", "My", "Session", "-w"] + ) == ["-r", "My Session", "-w"] + + # ── combined flags ─────────────────────────────────────────────────── + + def test_worktree_and_continue_multiword(self): + """hermes -w -c Pokemon Agent Dev (the original failing case)""" + assert _coalesce_session_name_args( + ["-w", "-c", "Pokemon", "Agent", "Dev"] + ) == ["-w", "-c", "Pokemon Agent Dev"] + + def test_continue_multiword_and_worktree(self): + """hermes -c Pokemon Agent Dev -w (order reversed)""" + assert _coalesce_session_name_args( + ["-c", "Pokemon", "Agent", "Dev", "-w"] + ) == ["-c", "Pokemon Agent Dev", "-w"] + + # ── passthrough (no session flags) ─────────────────────────────────── + + def test_no_session_flags_passthrough(self): + """hermes -w chat -q hello (nothing to merge)""" + result = _coalesce_session_name_args(["-w", "chat", "-q", "hello"]) + assert result == ["-w", "chat", "-q", "hello"] + + def test_empty_argv(self): + assert _coalesce_session_name_args([]) == [] + + # ── subcommand boundary ────────────────────────────────────────────── + + def test_stops_at_sessions_subcommand(self): + """hermes -c my project sessions list → stops before 'sessions'""" + assert _coalesce_session_name_args( + ["-c", "my", "project", "sessions", "list"] + ) == ["-c", "my project", "sessions", "list"] + + def test_stops_at_setup_subcommand(self): + """hermes -c my setup → 'setup' is a subcommand, not part of name""" + assert _coalesce_session_name_args( + ["-c", "my", "setup"] + ) == ["-c", "my", "setup"]