From 98d550e035b6489cd8fefa9a5f531c89ce261eef Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 9 Jun 2026 16:13:13 +1000 Subject: [PATCH] feat(debug): support /debug [nous|local] in the CLI/TUI slash command The --nous flag was only wired into the argparse `hermes debug share` subcommand. The /debug slash command (classic CLI + TUI, both via process_command -> _handle_debug_command) built a hardcoded args namespace with no `nous` attribute, so it always took the default paste.rs path. Pass cmd_original through to _handle_debug_command and parse an optional destination word: /debug -> public paste (default, unchanged) /debug nous -> Nous-internal S3 /debug local -> stdout, no upload local wins over nous (never touches the network); unknown words fall back to the default. Add args_hint="[nous|local]" so help/autocomplete surface it. New TestDebugSlashCommand covers the parsing + dispatch. --- cli.py | 2 +- hermes_cli/cli_commands_mixin.py | 19 ++++++++-- hermes_cli/commands.py | 3 +- tests/hermes_cli/test_debug.py | 59 ++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/cli.py b/cli.py index f809f797b..538b54101 100644 --- a/cli.py +++ b/cli.py @@ -8385,7 +8385,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin): elif canonical == "copy": self._handle_copy_command(cmd_original) elif canonical == "debug": - self._handle_debug_command() + self._handle_debug_command(cmd_original) elif canonical == "update": if self._handle_update_command(): return False diff --git a/hermes_cli/cli_commands_mixin.py b/hermes_cli/cli_commands_mixin.py index eefce8246..8685e622d 100644 --- a/hermes_cli/cli_commands_mixin.py +++ b/hermes_cli/cli_commands_mixin.py @@ -2575,12 +2575,25 @@ class CLICommandsMixin: else: _cprint(f" {_ACCENT}✓ {feature_name} set to {label} (session only){_RST}") - def _handle_debug_command(self): - """Handle /debug — upload debug report + logs and print paste URLs.""" + def _handle_debug_command(self, cmd_original: str = ""): + """Handle /debug — upload debug report + logs and print share URLs. + + Accepts optional destination words after the command: + + - ``/debug`` → upload to the public paste service (default) + - ``/debug nous`` → upload to Nous-internal storage (private, staff-only) + - ``/debug local`` → render the report to stdout, no upload + + ``nous`` and ``local`` are mutually exclusive; if both are given, + ``local`` wins (it never touches the network). + """ from hermes_cli.debug import run_debug_share from types import SimpleNamespace - args = SimpleNamespace(lines=200, expire=7, local=False) + words = {w.lower() for w in cmd_original.split()[1:]} + local = "local" in words + nous = "nous" in words and not local + args = SimpleNamespace(lines=200, expire=7, local=local, nous=nous) run_debug_share(args) def _handle_update_command(self) -> bool: diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 4e46a0636..8e69f640d 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -246,7 +246,8 @@ COMMAND_REGISTRY: list[CommandDef] = [ cli_only=True, args_hint=""), CommandDef("update", "Update Hermes Agent to the latest version", "Info"), CommandDef("version", "Show Hermes Agent version", "Info", aliases=("v",)), - CommandDef("debug", "Upload debug report (system info + logs) and get shareable links", "Info"), + CommandDef("debug", "Upload debug report (system info + logs) and get shareable links", "Info", + args_hint="[nous|local]"), # Exit CommandDef("quit", "Exit the CLI (use --delete to also remove session history)", "Exit", diff --git a/tests/hermes_cli/test_debug.py b/tests/hermes_cli/test_debug.py index 9d751ebb2..47999dc86 100644 --- a/tests/hermes_cli/test_debug.py +++ b/tests/hermes_cli/test_debug.py @@ -1570,3 +1570,62 @@ class TestRunDebugShareNous: run_debug_share(self._args()) paste.assert_not_called() + +class TestDebugSlashCommand: + """`/debug [nous|local]` parsing in the CLI/TUI handler. + + The classic CLI and the TUI slash worker both dispatch through + ``HermesCLI.process_command`` → ``_handle_debug_command(cmd_original)``, + which parses an optional destination word and builds the args namespace + handed to ``run_debug_share``. + """ + + def _handler(self): + from hermes_cli.cli_commands_mixin import CLICommandsMixin + + class _Stub(CLICommandsMixin): + pass + + return _Stub()._handle_debug_command + + def _captured(self, cmd_original): + captured = {} + + def _fake_run(args): + captured.update(vars(args)) + + with patch("hermes_cli.debug.run_debug_share", _fake_run): + self._handler()(cmd_original) + return captured + + def test_bare_debug_defaults_to_paste(self): + c = self._captured("/debug") + assert c["nous"] is False and c["local"] is False + assert c["lines"] == 200 and c["expire"] == 7 + + def test_nous_word_sets_nous(self): + c = self._captured("/debug nous") + assert c["nous"] is True and c["local"] is False + + def test_local_word_sets_local(self): + c = self._captured("/debug local") + assert c["local"] is True and c["nous"] is False + + def test_word_parsing_is_case_insensitive(self): + c = self._captured("/debug NOUS") + assert c["nous"] is True + + def test_local_wins_over_nous(self): + # local never touches the network, so it takes precedence. + c = self._captured("/debug nous local") + assert c["local"] is True and c["nous"] is False + + def test_unknown_word_falls_back_to_default(self): + c = self._captured("/debug paste") + assert c["nous"] is False and c["local"] is False + + def test_no_arg_default_keyword(self): + # Calling with no cmd_original (legacy callers) must still work. + c = self._captured("") + assert c["nous"] is False and c["local"] is False +