From b103bb4c8bc0bb86b068492404943fd6ea8c69b2 Mon Sep 17 00:00:00 2001 From: Teknium Date: Mon, 23 Feb 2026 23:52:07 +0000 Subject: [PATCH] feat: add interactive tool configuration command - Introduced a new `tools` command in the CLI for configuring enabled tools per platform. - Implemented an interactive checklist for users to enable or disable toolsets for various platforms, enhancing customization options. - Created a new `tools_config.py` file to handle the logic for toolset management and user prompts, improving code organization and user experience. --- hermes_cli/main.py | 15 ++ hermes_cli/tools_config.py | 282 +++++++++++++++++++++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 hermes_cli/tools_config.py diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 4264730c6..a75f2612c 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1001,6 +1001,21 @@ For more help on a command: skills_parser.set_defaults(func=cmd_skills) + # ========================================================================= + # tools 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." + ) + + def cmd_tools(args): + from hermes_cli.tools_config import tools_command + tools_command(args) + + tools_parser.set_defaults(func=cmd_tools) + # ========================================================================= # sessions command # ========================================================================= diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py new file mode 100644 index 000000000..104a62fe4 --- /dev/null +++ b/hermes_cli/tools_config.py @@ -0,0 +1,282 @@ +""" +Interactive tool configuration for Hermes Agent. + +`hermes tools` โ€” select a platform, then toggle toolsets on/off via checklist. +Saves per-platform tool configuration to ~/.hermes/config.yaml under +the `platform_toolsets` key. +""" + +import sys +from pathlib import Path +from typing import Dict, List, Set + +from hermes_cli.config import load_config, save_config, get_env_value +from hermes_cli.colors import Colors, color + +# Toolsets shown in the configurator, grouped for display. +# Each entry: (toolset_name, label, description) +# These map to keys in toolsets.py TOOLSETS dict. +CONFIGURABLE_TOOLSETS = [ + ("web", "๐Ÿ” Web Search & Scraping", "web_search, web_extract"), + ("browser", "๐ŸŒ Browser Automation", "navigate, click, type, scroll"), + ("terminal", "๐Ÿ’ป Terminal & Processes", "terminal, process"), + ("file", "๐Ÿ“ File Operations", "read, write, patch, search"), + ("code_execution", "โšก Code Execution", "execute_code"), + ("vision", "๐Ÿ‘๏ธ Vision / Image Analysis", "vision_analyze"), + ("image_gen", "๐ŸŽจ Image Generation", "image_generate"), + ("moa", "๐Ÿง  Mixture of Agents", "mixture_of_agents"), + ("tts", "๐Ÿ”Š Text-to-Speech", "text_to_speech"), + ("skills", "๐Ÿ“š Skills", "list, view, manage"), + ("todo", "๐Ÿ“‹ Task Planning", "todo"), + ("memory", "๐Ÿ’พ Memory", "persistent memory across sessions"), + ("session_search", "๐Ÿ”Ž Session Search", "search past conversations"), + ("clarify", "โ“ Clarifying Questions", "clarify"), + ("delegation", "๐Ÿ‘ฅ Task Delegation", "delegate_task"), + ("cronjob", "โฐ Cron Jobs", "schedule, list, remove"), + ("rl", "๐Ÿงช RL Training", "Tinker-Atropos training tools"), +] + +# Platform display config +PLATFORMS = { + "cli": {"label": "๐Ÿ–ฅ๏ธ CLI", "default_toolset": "hermes-cli"}, + "telegram": {"label": "๐Ÿ“ฑ Telegram", "default_toolset": "hermes-telegram"}, + "discord": {"label": "๐Ÿ’ฌ Discord", "default_toolset": "hermes-discord"}, + "slack": {"label": "๐Ÿ’ผ Slack", "default_toolset": "hermes-slack"}, + "whatsapp": {"label": "๐Ÿ“ฑ WhatsApp", "default_toolset": "hermes-whatsapp"}, +} + + +def _get_enabled_platforms() -> List[str]: + """Return platform keys that are configured (have tokens or are CLI).""" + enabled = ["cli"] + if get_env_value("TELEGRAM_BOT_TOKEN"): + enabled.append("telegram") + if get_env_value("DISCORD_BOT_TOKEN"): + enabled.append("discord") + if get_env_value("SLACK_BOT_TOKEN"): + enabled.append("slack") + if get_env_value("WHATSAPP_ENABLED"): + enabled.append("whatsapp") + return enabled + + +def _get_platform_tools(config: dict, platform: str) -> Set[str]: + """Resolve which individual toolset names are enabled for a platform.""" + from toolsets import resolve_toolset, TOOLSETS + + platform_toolsets = config.get("platform_toolsets", {}) + toolset_names = platform_toolsets.get(platform) + + if not toolset_names or not isinstance(toolset_names, list): + default_ts = PLATFORMS[platform]["default_toolset"] + toolset_names = [default_ts] + + # Resolve to individual tool names, then map back to which + # configurable toolsets are covered + all_tool_names = set() + for ts_name in toolset_names: + all_tool_names.update(resolve_toolset(ts_name)) + + # Map individual tool names back to configurable toolset keys + enabled_toolsets = set() + for ts_key, _, _ in CONFIGURABLE_TOOLSETS: + ts_tools = set(resolve_toolset(ts_key)) + if ts_tools and ts_tools.issubset(all_tool_names): + enabled_toolsets.add(ts_key) + + return enabled_toolsets + + +def _save_platform_tools(config: dict, platform: str, enabled_toolset_keys: Set[str]): + """Save the selected toolset keys for a platform to config.""" + config.setdefault("platform_toolsets", {}) + config["platform_toolsets"][platform] = sorted(enabled_toolset_keys) + save_config(config) + + +def _prompt_choice(question: str, choices: list, default: int = 0) -> int: + """Single-select menu (arrow keys).""" + print(color(question, Colors.YELLOW)) + + try: + from simple_term_menu import TerminalMenu + menu = TerminalMenu( + [f" {c}" for c in choices], + cursor_index=default, + menu_cursor="โ†’ ", + menu_cursor_style=("fg_green", "bold"), + menu_highlight_style=("fg_green",), + cycle_cursor=True, + clear_screen=False, + ) + idx = menu.show() + if idx is None: + sys.exit(0) + print() + return idx + except ImportError: + for i, c in enumerate(choices): + marker = "โ—" if i == default else "โ—‹" + style = Colors.GREEN if i == default else "" + print(color(f" {marker} {c}", style) if style else f" {marker} {c}") + while True: + try: + val = input(color(f" Select [1-{len(choices)}] ({default + 1}): ", Colors.DIM)) + if not val: + return default + idx = int(val) - 1 + if 0 <= idx < len(choices): + return idx + except (ValueError, KeyboardInterrupt, EOFError): + print() + sys.exit(0) + + +def _prompt_toolset_checklist(platform_label: str, enabled: Set[str]) -> Set[str]: + """Multi-select checklist of toolsets. Returns set of selected toolset keys.""" + title = color(f"Tools for {platform_label}", Colors.YELLOW) + hint = color(" Press SPACE to toggle, ENTER on Continue when done.", Colors.DIM) + print(title) + print(hint) + print() + + labels = [] + for ts_key, ts_label, ts_desc in CONFIGURABLE_TOOLSETS: + labels.append(f"{ts_label} {color(ts_desc, Colors.DIM)}") + + pre_selected_indices = [ + i for i, (ts_key, _, _) in enumerate(CONFIGURABLE_TOOLSETS) + if ts_key in enabled + ] + + try: + from simple_term_menu import TerminalMenu + + menu_items = [f" {label}" for label in labels] + [" Continue โ†’"] + preselected = [menu_items[i] for i in pre_selected_indices if i < len(menu_items)] + + menu = TerminalMenu( + menu_items, + multi_select=True, + show_multi_select_hint=False, + multi_select_cursor="[โœ“] ", + multi_select_select_on_accept=False, + multi_select_empty_ok=True, + preselected_entries=preselected if preselected else None, + menu_cursor="โ†’ ", + menu_cursor_style=("fg_green", "bold"), + menu_highlight_style=("fg_green",), + cycle_cursor=True, + clear_screen=False, + ) + + menu.show() + + if menu.chosen_menu_entries is None: + return enabled # Escape pressed, keep current + + continue_idx = len(CONFIGURABLE_TOOLSETS) + selected_indices = [i for i in (menu.chosen_menu_indices or []) if i != continue_idx] + + return {CONFIGURABLE_TOOLSETS[i][0] for i in selected_indices} + + except ImportError: + # Fallback: numbered toggle + selected = set(pre_selected_indices) + while True: + for i, label in enumerate(labels): + marker = color("[โœ“]", Colors.GREEN) if i in selected else "[ ]" + print(f" {marker} {i + 1}. {label}") + print(f" {len(labels) + 1}. {color('Continue โ†’', Colors.GREEN)}") + print() + try: + val = input(color(" Toggle # (or Enter to continue): ", Colors.DIM)).strip() + if not val: + break + idx = int(val) - 1 + if idx == len(labels): + break + if 0 <= idx < len(labels): + if idx in selected: + selected.discard(idx) + else: + selected.add(idx) + except (ValueError, KeyboardInterrupt, EOFError): + return enabled + print() + + return {CONFIGURABLE_TOOLSETS[i][0] for i in selected} + + +def tools_command(args): + """Entry point for `hermes tools`.""" + config = load_config() + enabled_platforms = _get_enabled_platforms() + + print() + print(color("โš• Hermes Tool Configuration", Colors.CYAN, Colors.BOLD)) + print(color(" Enable or disable tools per platform.", Colors.DIM)) + print() + + # Build platform choices + platform_choices = [] + platform_keys = [] + for pkey in enabled_platforms: + pinfo = PLATFORMS[pkey] + # Count currently enabled toolsets + current = _get_platform_tools(config, pkey) + count = len(current) + total = len(CONFIGURABLE_TOOLSETS) + count_str = color(f"({count}/{total} enabled)", Colors.DIM) + platform_choices.append(f"Configure {pinfo['label']} {count_str}") + platform_keys.append(pkey) + + platform_choices.append(f"{color('Done โ€” save and exit', Colors.GREEN)}") + + while True: + idx = _prompt_choice("Select a platform to configure:", platform_choices, default=0) + + # "Done" selected + if idx == len(platform_keys): + break + + pkey = platform_keys[idx] + pinfo = PLATFORMS[pkey] + + # Get current enabled toolsets for this platform + current_enabled = _get_platform_tools(config, pkey) + + # Show checklist + new_enabled = _prompt_toolset_checklist(pinfo["label"], current_enabled) + + if new_enabled != current_enabled: + _save_platform_tools(config, pkey, new_enabled) + + added = new_enabled - current_enabled + removed = current_enabled - new_enabled + + if added: + for ts in sorted(added): + label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts), ts) + print(color(f" + {label}", Colors.GREEN)) + if removed: + for ts in sorted(removed): + label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts), ts) + print(color(f" - {label}", Colors.RED)) + + print(color(f" โœ“ Saved {pinfo['label']} configuration", Colors.GREEN)) + else: + print(color(f" No changes to {pinfo['label']}", Colors.DIM)) + + print() + + # Update the choice label with new count + new_count = len(_get_platform_tools(config, pkey)) + total = len(CONFIGURABLE_TOOLSETS) + count_str = color(f"({new_count}/{total} enabled)", Colors.DIM) + platform_choices[idx] = f"Configure {pinfo['label']} {count_str}" + + print() + 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()