feat: add /tools disable/enable/list slash commands with session reset (#1652)

Add in-session tool management via /tools disable/enable/list, plus
hermes tools list/disable/enable CLI subcommands. Supports both
built-in toolsets (web, memory) and MCP tools (github:create_issue).

To preserve prompt caching, /tools disable/enable in a chat session
saves the change to config and resets the session cleanly — the user
is asked to confirm before the reset happens.

Also improves prefix matching: /qui now dispatches to /quit instead
of showing ambiguous when longer skill commands like /quint-pipeline
are installed.

Based on PR #1520 by @YanSte.

Co-authored-by: Yannick Stephan <YanSte@users.noreply.github.com>
This commit is contained in:
Teknium 2026-03-17 02:05:26 -07:00 committed by GitHub
parent f2414bfd45
commit 49043b7b7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 606 additions and 7 deletions

78
cli.py
View file

@ -2481,7 +2481,69 @@ class HermesCLI:
print(f" Total: {len(tools)} tools ヽ(^o^)")
print()
def _handle_tools_command(self, cmd: str):
"""Handle /tools [list|disable|enable] slash commands.
/tools (no args) shows the tool list.
/tools list shows enabled/disabled status per toolset.
/tools disable/enable saves the change to config and resets
the session so the new tool set takes effect cleanly (no
prompt-cache breakage mid-conversation).
"""
import shlex
from argparse import Namespace
from hermes_cli.tools_config import tools_disable_enable_command
try:
parts = shlex.split(cmd)
except ValueError:
parts = cmd.split()
subcommand = parts[1] if len(parts) > 1 else ""
if subcommand not in ("list", "disable", "enable"):
self.show_tools()
return
if subcommand == "list":
tools_disable_enable_command(
Namespace(tools_action="list", platform="cli"))
return
names = parts[2:]
if not names:
print(f"(._.) Usage: /tools {subcommand} <name> [name ...]")
print(f" Built-in toolset: /tools {subcommand} web")
print(f" MCP tool: /tools {subcommand} github:create_issue")
return
# Confirm session reset before applying
verb = "Disable" if subcommand == "disable" else "Enable"
label = ", ".join(names)
_cprint(f"{_GOLD}{verb} {label}?{_RST}")
_cprint(f"{_DIM}This will save to config and reset your session so the "
f"change takes effect cleanly.{_RST}")
try:
answer = input(" Continue? [y/N] ").strip().lower()
except (EOFError, KeyboardInterrupt):
print()
_cprint(f"{_DIM}Cancelled.{_RST}")
return
if answer not in ("y", "yes"):
_cprint(f"{_DIM}Cancelled.{_RST}")
return
tools_disable_enable_command(
Namespace(tools_action=subcommand, names=names, platform="cli"))
# Reset session so the new tool config is picked up from a clean state
from hermes_cli.tools_config import _get_platform_tools
from hermes_cli.config import load_config
self.enabled_toolsets = _get_platform_tools(load_config(), "cli")
self.new_session()
_cprint(f"{_DIM}Session reset. New tool configuration is active.{_RST}")
def show_toolsets(self):
"""Display available toolsets with kawaii ASCII art."""
all_toolsets = get_all_toolsets()
@ -3279,7 +3341,7 @@ class HermesCLI:
elif canonical == "help":
self.show_help()
elif canonical == "tools":
self.show_tools()
self._handle_tools_command(cmd_original)
elif canonical == "toolsets":
self.show_toolsets()
elif canonical == "config":
@ -3610,6 +3672,18 @@ class HermesCLI:
typed_base = cmd_lower.split()[0]
all_known = set(COMMANDS) | set(_skill_commands)
matches = [c for c in all_known if c.startswith(typed_base)]
if len(matches) > 1:
# Prefer an exact match (typed the full command name)
exact = [c for c in matches if c == typed_base]
if len(exact) == 1:
matches = exact
else:
# Prefer the unique shortest match:
# /qui → /quit (5) wins over /quint-pipeline (15)
min_len = min(len(c) for c in matches)
shortest = [c for c in matches if len(c) == min_len]
if len(shortest) == 1:
matches = shortest
if len(matches) == 1:
# Expand the prefix to the full command name, preserving arguments.
# Guard against redispatching the same token to avoid infinite

View file

@ -92,8 +92,8 @@ COMMAND_REGISTRY: list[CommandDef] = [
args_hint="[on|off|tts|status]", subcommands=("on", "off", "tts", "status")),
# Tools & Skills
CommandDef("tools", "List available tools", "Tools & Skills",
cli_only=True),
CommandDef("tools", "Manage tools: /tools [list|disable|enable] [name...]", "Tools & Skills",
args_hint="[list|disable|enable] [name...]", cli_only=True),
CommandDef("toolsets", "List available toolsets", "Tools & Skills",
cli_only=True),
CommandDef("skills", "Search, install, inspect, or manage skills",

View file

@ -3147,17 +3147,66 @@ For more help on a command:
tools_parser = subparsers.add_parser(
"tools",
help="Configure which tools are enabled per platform",
description="Interactive tool configuration — enable/disable tools for CLI, Telegram, Discord, etc."
description=(
"Enable, disable, or list tools for CLI, Telegram, Discord, etc.\n\n"
"Built-in toolsets use plain names (e.g. web, memory).\n"
"MCP tools use server:tool notation (e.g. github:create_issue).\n\n"
"Run 'hermes tools' with no subcommand for the interactive configuration UI."
),
)
tools_parser.add_argument(
"--summary",
action="store_true",
help="Print a summary of enabled tools per platform and exit"
)
tools_sub = tools_parser.add_subparsers(dest="tools_action")
# hermes tools list [--platform cli]
tools_list_p = tools_sub.add_parser(
"list",
help="Show all tools and their enabled/disabled status",
)
tools_list_p.add_argument(
"--platform", default="cli",
help="Platform to show (default: cli)",
)
# hermes tools disable <name...> [--platform cli]
tools_disable_p = tools_sub.add_parser(
"disable",
help="Disable toolsets or MCP tools",
)
tools_disable_p.add_argument(
"names", nargs="+", metavar="NAME",
help="Toolset name (e.g. web) or MCP tool in server:tool form",
)
tools_disable_p.add_argument(
"--platform", default="cli",
help="Platform to apply to (default: cli)",
)
# hermes tools enable <name...> [--platform cli]
tools_enable_p = tools_sub.add_parser(
"enable",
help="Enable toolsets or MCP tools",
)
tools_enable_p.add_argument(
"names", nargs="+", metavar="NAME",
help="Toolset name or MCP tool in server:tool form",
)
tools_enable_p.add_argument(
"--platform", default="cli",
help="Platform to apply to (default: cli)",
)
def cmd_tools(args):
from hermes_cli.tools_config import tools_command
tools_command(args)
action = getattr(args, "tools_action", None)
if action in ("list", "disable", "enable"):
from hermes_cli.tools_config import tools_disable_enable_command
tools_disable_enable_command(args)
else:
from hermes_cli.tools_config import tools_command
tools_command(args)
tools_parser.set_defaults(func=cmd_tools)
# =========================================================================

View file

@ -1088,3 +1088,114 @@ def tools_command(args=None, first_install: bool = False, config: dict = None):
print(color(" Tool configuration saved to ~/.hermes/config.yaml", Colors.DIM))
print(color(" Changes take effect on next 'hermes' or gateway restart.", Colors.DIM))
print()
# ─── Non-interactive disable/enable ──────────────────────────────────────────
def _apply_toolset_change(config: dict, platform: str, toolset_names: List[str], action: str):
"""Add or remove built-in toolsets for a platform."""
enabled = _get_platform_tools(config, platform)
if action == "disable":
updated = enabled - set(toolset_names)
else:
updated = enabled | set(toolset_names)
_save_platform_tools(config, platform, updated)
def _apply_mcp_change(config: dict, targets: List[str], action: str) -> Set[str]:
"""Add or remove specific MCP tools from a server's exclude list.
Returns the set of server names that were not found in config.
"""
failed_servers: Set[str] = set()
mcp_servers = config.get("mcp_servers") or {}
for target in targets:
server_name, tool_name = target.split(":", 1)
if server_name not in mcp_servers:
failed_servers.add(server_name)
continue
tools_cfg = mcp_servers[server_name].setdefault("tools", {})
exclude = list(tools_cfg.get("exclude") or [])
if action == "disable":
if tool_name not in exclude:
exclude.append(tool_name)
else:
exclude = [t for t in exclude if t != tool_name]
tools_cfg["exclude"] = exclude
return failed_servers
def _print_tools_list(enabled_toolsets: set, mcp_servers: dict, platform: str = "cli"):
"""Print a summary of enabled/disabled toolsets and MCP tool filters."""
print(f"Built-in toolsets ({platform}):")
for ts_key, label, _ in CONFIGURABLE_TOOLSETS:
status = (color("✓ enabled", Colors.GREEN) if ts_key in enabled_toolsets
else color("✗ disabled", Colors.RED))
print(f" {status} {ts_key} {color(label, Colors.DIM)}")
if mcp_servers:
print()
print("MCP servers:")
for srv_name, srv_cfg in mcp_servers.items():
tools_cfg = srv_cfg.get("tools") or {}
exclude = tools_cfg.get("exclude") or []
include = tools_cfg.get("include") or []
if include:
_print_info(f"{srv_name} [include only: {', '.join(include)}]")
elif exclude:
_print_info(f"{srv_name} [excluded: {color(', '.join(exclude), Colors.YELLOW)}]")
else:
_print_info(f"{srv_name} {color('all tools enabled', Colors.DIM)}")
def tools_disable_enable_command(args):
"""Enable, disable, or list tools for a platform.
Built-in toolsets use plain names (e.g. ``web``, ``memory``).
MCP tools use ``server:tool`` notation (e.g. ``github:create_issue``).
"""
action = args.tools_action
platform = getattr(args, "platform", "cli")
config = load_config()
if platform not in PLATFORMS:
_print_error(f"Unknown platform '{platform}'. Valid: {', '.join(PLATFORMS)}")
return
if action == "list":
_print_tools_list(_get_platform_tools(config, platform),
config.get("mcp_servers") or {}, platform)
return
targets: List[str] = args.names
toolset_targets = [t for t in targets if ":" not in t]
mcp_targets = [t for t in targets if ":" in t]
valid_toolsets = {ts_key for ts_key, _, _ in CONFIGURABLE_TOOLSETS}
unknown_toolsets = [t for t in toolset_targets if t not in valid_toolsets]
if unknown_toolsets:
for name in unknown_toolsets:
_print_error(f"Unknown toolset '{name}'")
toolset_targets = [t for t in toolset_targets if t in valid_toolsets]
if toolset_targets:
_apply_toolset_change(config, platform, toolset_targets, action)
failed_servers: Set[str] = set()
if mcp_targets:
failed_servers = _apply_mcp_change(config, mcp_targets, action)
for srv in failed_servers:
_print_error(f"MCP server '{srv}' not found in config")
save_config(config)
successful = [
t for t in targets
if t not in unknown_toolsets and (":" not in t or t.split(":")[0] not in failed_servers)
]
if successful:
verb = "Disabled" if action == "disable" else "Enabled"
_print_success(f"{verb}: {', '.join(successful)}")

View file

@ -0,0 +1,207 @@
"""Tests for hermes tools disable/enable/list command (backend)."""
from argparse import Namespace
from unittest.mock import patch
from hermes_cli.tools_config import tools_disable_enable_command
# ── Built-in toolset disable ────────────────────────────────────────────────
class TestToolsDisableBuiltin:
def test_disable_removes_toolset_from_platform(self):
config = {"platform_toolsets": {"cli": ["web", "memory", "terminal"]}}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config") as mock_save:
tools_disable_enable_command(Namespace(tools_action="disable", names=["web"], platform="cli"))
saved = mock_save.call_args[0][0]
assert "web" not in saved["platform_toolsets"]["cli"]
assert "memory" in saved["platform_toolsets"]["cli"]
def test_disable_multiple_toolsets(self):
config = {"platform_toolsets": {"cli": ["web", "memory", "terminal"]}}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config") as mock_save:
tools_disable_enable_command(Namespace(tools_action="disable", names=["web", "memory"], platform="cli"))
saved = mock_save.call_args[0][0]
assert "web" not in saved["platform_toolsets"]["cli"]
assert "memory" not in saved["platform_toolsets"]["cli"]
assert "terminal" in saved["platform_toolsets"]["cli"]
def test_disable_already_absent_is_idempotent(self):
config = {"platform_toolsets": {"cli": ["memory"]}}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config") as mock_save:
tools_disable_enable_command(Namespace(tools_action="disable", names=["web"], platform="cli"))
saved = mock_save.call_args[0][0]
assert "web" not in saved["platform_toolsets"]["cli"]
# ── Built-in toolset enable ─────────────────────────────────────────────────
class TestToolsEnableBuiltin:
def test_enable_adds_toolset_to_platform(self):
config = {"platform_toolsets": {"cli": ["memory"]}}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config") as mock_save:
tools_disable_enable_command(Namespace(tools_action="enable", names=["web"], platform="cli"))
saved = mock_save.call_args[0][0]
assert "web" in saved["platform_toolsets"]["cli"]
def test_enable_already_present_is_idempotent(self):
config = {"platform_toolsets": {"cli": ["web"]}}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config") as mock_save:
tools_disable_enable_command(Namespace(tools_action="enable", names=["web"], platform="cli"))
saved = mock_save.call_args[0][0]
assert saved["platform_toolsets"]["cli"].count("web") == 1
# ── MCP tool disable ────────────────────────────────────────────────────────
class TestToolsDisableMcp:
def test_disable_adds_to_exclude_list(self):
config = {"mcp_servers": {"github": {"command": "npx"}}}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config") as mock_save:
tools_disable_enable_command(
Namespace(tools_action="disable", names=["github:create_issue"], platform="cli")
)
saved = mock_save.call_args[0][0]
assert "create_issue" in saved["mcp_servers"]["github"]["tools"]["exclude"]
def test_disable_already_excluded_is_idempotent(self):
config = {"mcp_servers": {"github": {"tools": {"exclude": ["create_issue"]}}}}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config") as mock_save:
tools_disable_enable_command(
Namespace(tools_action="disable", names=["github:create_issue"], platform="cli")
)
saved = mock_save.call_args[0][0]
assert saved["mcp_servers"]["github"]["tools"]["exclude"].count("create_issue") == 1
def test_disable_unknown_server_prints_error(self, capsys):
config = {"mcp_servers": {}}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config"):
tools_disable_enable_command(
Namespace(tools_action="disable", names=["unknown:tool"], platform="cli")
)
out = capsys.readouterr().out
assert "MCP server 'unknown' not found in config" in out
# ── MCP tool enable ──────────────────────────────────────────────────────────
class TestToolsEnableMcp:
def test_enable_removes_from_exclude_list(self):
config = {"mcp_servers": {"github": {"tools": {"exclude": ["create_issue", "delete_branch"]}}}}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config") as mock_save:
tools_disable_enable_command(
Namespace(tools_action="enable", names=["github:create_issue"], platform="cli")
)
saved = mock_save.call_args[0][0]
assert "create_issue" not in saved["mcp_servers"]["github"]["tools"]["exclude"]
assert "delete_branch" in saved["mcp_servers"]["github"]["tools"]["exclude"]
# ── Mixed targets ────────────────────────────────────────────────────────────
class TestToolsMixedTargets:
def test_disable_builtin_and_mcp_together(self):
config = {
"platform_toolsets": {"cli": ["web", "memory"]},
"mcp_servers": {"github": {"command": "npx"}},
}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config") as mock_save:
tools_disable_enable_command(Namespace(
tools_action="disable",
names=["web", "github:create_issue"],
platform="cli",
))
saved = mock_save.call_args[0][0]
assert "web" not in saved["platform_toolsets"]["cli"]
assert "create_issue" in saved["mcp_servers"]["github"]["tools"]["exclude"]
# ── List output ──────────────────────────────────────────────────────────────
class TestToolsList:
def test_list_shows_enabled_toolsets(self, capsys):
config = {"platform_toolsets": {"cli": ["web", "memory"]}}
with patch("hermes_cli.tools_config.load_config", return_value=config):
tools_disable_enable_command(Namespace(tools_action="list", platform="cli"))
out = capsys.readouterr().out
assert "web" in out
assert "memory" in out
def test_list_shows_mcp_excluded_tools(self, capsys):
config = {
"mcp_servers": {"github": {"tools": {"exclude": ["create_issue"]}}},
}
with patch("hermes_cli.tools_config.load_config", return_value=config):
tools_disable_enable_command(Namespace(tools_action="list", platform="cli"))
out = capsys.readouterr().out
assert "github" in out
assert "create_issue" in out
# ── Validation ───────────────────────────────────────────────────────────────
class TestToolsValidation:
def test_unknown_platform_prints_error(self, capsys):
config = {}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config"):
tools_disable_enable_command(
Namespace(tools_action="disable", names=["web"], platform="invalid_platform")
)
out = capsys.readouterr().out
assert "Unknown platform 'invalid_platform'" in out
def test_unknown_toolset_prints_error(self, capsys):
config = {"platform_toolsets": {"cli": ["web"]}}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config"):
tools_disable_enable_command(
Namespace(tools_action="disable", names=["nonexistent_toolset"], platform="cli")
)
out = capsys.readouterr().out
assert "Unknown toolset 'nonexistent_toolset'" in out
def test_unknown_toolset_does_not_corrupt_config(self):
config = {"platform_toolsets": {"cli": ["web", "memory"]}}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config") as mock_save:
tools_disable_enable_command(
Namespace(tools_action="disable", names=["nonexistent_toolset"], platform="cli")
)
saved = mock_save.call_args[0][0]
assert "web" in saved["platform_toolsets"]["cli"]
assert "memory" in saved["platform_toolsets"]["cli"]
def test_mixed_valid_and_invalid_applies_valid_only(self):
config = {"platform_toolsets": {"cli": ["web", "memory"]}}
with patch("hermes_cli.tools_config.load_config", return_value=config), \
patch("hermes_cli.tools_config.save_config") as mock_save:
tools_disable_enable_command(
Namespace(tools_action="disable", names=["web", "bad_toolset"], platform="cli")
)
saved = mock_save.call_args[0][0]
assert "web" not in saved["platform_toolsets"]["cli"]
assert "memory" in saved["platform_toolsets"]["cli"]

View file

@ -121,3 +121,40 @@ class TestSlashCommandPrefixMatching:
mock_help.assert_called_once()
printed = " ".join(str(c) for c in cli_obj.console.print.call_args_list)
assert "Ambiguous" not in printed
def test_shortest_match_preferred_over_longer_skill(self):
"""/qui should dispatch to /quit (5 chars) not report ambiguous with /quint-pipeline (15 chars)."""
cli_obj = _make_cli()
fake_skill = {"/quint-pipeline": {"name": "Quint Pipeline", "description": "test"}}
import cli as cli_mod
with patch.object(cli_mod, '_skill_commands', fake_skill):
# /quit is caught by the exact "/quit" branch → process_command returns False
result = cli_obj.process_command("/qui")
# Returns False because /quit was dispatched (exits chat loop)
assert result is False
printed = " ".join(str(c) for c in cli_obj.console.print.call_args_list)
assert "Ambiguous" not in printed
def test_tied_shortest_matches_still_ambiguous(self):
"""/re matches /reset and /retry (both 6 chars) — no unique shortest, stays ambiguous."""
cli_obj = _make_cli()
printed = []
import cli as cli_mod
with patch.object(cli_mod, '_cprint', side_effect=lambda t: printed.append(t)):
cli_obj.process_command("/re")
combined = " ".join(printed)
assert "Ambiguous" in combined or "Did you mean" in combined
def test_exact_typed_name_dispatches_over_longer_match(self):
"""/help typed with /help-extra skill installed → exact match wins."""
cli_obj = _make_cli()
fake_skill = {"/help-extra": {"name": "Help Extra", "description": ""}}
import cli as cli_mod
with patch.object(cli_mod, '_skill_commands', fake_skill), \
patch.object(cli_obj, 'show_help') as mock_help:
cli_obj.process_command("/help")
mock_help.assert_called_once()
printed = " ".join(str(c) for c in cli_obj.console.print.call_args_list)
assert "Ambiguous" not in printed

View file

@ -0,0 +1,121 @@
"""Tests for /tools slash command handler in the interactive CLI."""
from unittest.mock import MagicMock, patch, call
from cli import HermesCLI
def _make_cli(enabled_toolsets=None):
"""Build a minimal HermesCLI stub without running __init__."""
cli_obj = HermesCLI.__new__(HermesCLI)
cli_obj.enabled_toolsets = set(enabled_toolsets or ["web", "memory"])
cli_obj._command_running = False
cli_obj.console = MagicMock()
return cli_obj
# ── /tools (no subcommand) ──────────────────────────────────────────────────
class TestToolsSlashNoSubcommand:
def test_bare_tools_shows_tool_list(self):
cli_obj = _make_cli()
with patch.object(cli_obj, "show_tools") as mock_show:
cli_obj._handle_tools_command("/tools")
mock_show.assert_called_once()
def test_unknown_subcommand_falls_back_to_show_tools(self):
cli_obj = _make_cli()
with patch.object(cli_obj, "show_tools") as mock_show:
cli_obj._handle_tools_command("/tools foobar")
mock_show.assert_called_once()
# ── /tools list ─────────────────────────────────────────────────────────────
class TestToolsSlashList:
def test_list_calls_backend(self, capsys):
cli_obj = _make_cli()
with patch("hermes_cli.tools_config.load_config",
return_value={"platform_toolsets": {"cli": ["web"]}}), \
patch("hermes_cli.tools_config.save_config"):
cli_obj._handle_tools_command("/tools list")
out = capsys.readouterr().out
assert "web" in out
def test_list_does_not_modify_enabled_toolsets(self):
"""List is read-only — self.enabled_toolsets must not change."""
cli_obj = _make_cli(["web", "memory"])
with patch("hermes_cli.tools_config.load_config",
return_value={"platform_toolsets": {"cli": ["web"]}}):
cli_obj._handle_tools_command("/tools list")
assert cli_obj.enabled_toolsets == {"web", "memory"}
# ── /tools disable (session reset) ──────────────────────────────────────────
class TestToolsSlashDisableWithReset:
def test_disable_confirms_then_resets_session(self):
cli_obj = _make_cli(["web", "memory"])
with patch("hermes_cli.tools_config.load_config",
return_value={"platform_toolsets": {"cli": ["web", "memory"]}}), \
patch("hermes_cli.tools_config.save_config"), \
patch("hermes_cli.tools_config._get_platform_tools", return_value={"memory"}), \
patch("hermes_cli.config.load_config", return_value={}), \
patch.object(cli_obj, "new_session") as mock_reset, \
patch("builtins.input", return_value="y"):
cli_obj._handle_tools_command("/tools disable web")
mock_reset.assert_called_once()
assert "web" not in cli_obj.enabled_toolsets
def test_disable_cancelled_does_not_reset(self):
cli_obj = _make_cli(["web", "memory"])
with patch.object(cli_obj, "new_session") as mock_reset, \
patch("builtins.input", return_value="n"):
cli_obj._handle_tools_command("/tools disable web")
mock_reset.assert_not_called()
# Toolsets unchanged
assert cli_obj.enabled_toolsets == {"web", "memory"}
def test_disable_eof_cancels(self):
cli_obj = _make_cli(["web", "memory"])
with patch.object(cli_obj, "new_session") as mock_reset, \
patch("builtins.input", side_effect=EOFError):
cli_obj._handle_tools_command("/tools disable web")
mock_reset.assert_not_called()
def test_disable_missing_name_prints_usage(self, capsys):
cli_obj = _make_cli()
cli_obj._handle_tools_command("/tools disable")
out = capsys.readouterr().out
assert "Usage" in out
# ── /tools enable (session reset) ───────────────────────────────────────────
class TestToolsSlashEnableWithReset:
def test_enable_confirms_then_resets_session(self):
cli_obj = _make_cli(["memory"])
with patch("hermes_cli.tools_config.load_config",
return_value={"platform_toolsets": {"cli": ["memory"]}}), \
patch("hermes_cli.tools_config.save_config"), \
patch("hermes_cli.tools_config._get_platform_tools", return_value={"memory", "web"}), \
patch("hermes_cli.config.load_config", return_value={}), \
patch.object(cli_obj, "new_session") as mock_reset, \
patch("builtins.input", return_value="y"):
cli_obj._handle_tools_command("/tools enable web")
mock_reset.assert_called_once()
assert "web" in cli_obj.enabled_toolsets
def test_enable_missing_name_prints_usage(self, capsys):
cli_obj = _make_cli()
cli_obj._handle_tools_command("/tools enable")
out = capsys.readouterr().out
assert "Usage" in out