feat: unify hermes tools and hermes setup tools into single flow
Both 'hermes tools' and 'hermes setup tools' now use the same unified flow in tools_config.py: 1. Select platform (CLI, Telegram, Discord, etc.) 2. Toggle all 18 toolsets on/off in checklist 3. Newly enabled tools that need API keys → provider-aware config (e.g., TTS shows Edge/OpenAI/ElevenLabs picker) 4. Already-configured tools that stay enabled → silent, no prompts 5. Menu option: 'Reconfigure an existing tool' for updating providers or API keys on tools that are already set up Key changes: - Move TOOL_CATEGORIES, provider config, and post-setup hooks from setup.py to tools_config.py - Replace flat _check_and_prompt_requirements() with provider-aware _configure_toolset() that uses TOOL_CATEGORIES - Add _reconfigure_tool() flow for updating existing configs - setup.py's setup_tools() now delegates to tools_command() - tools_command() menu adds 'Reconfigure' option alongside platforms - Only prompt for API keys on tools that are NEWLY toggled on AND don't already have keys configured No breaking changes. All 2013 tests pass.
This commit is contained in:
@@ -460,191 +460,9 @@ def _prompt_container_resources(config: dict):
|
||||
pass
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tool Categories — category-first UX for tool configuration
|
||||
# =============================================================================
|
||||
# Each category represents a tool type. Within each category, users choose
|
||||
# a provider. This avoids showing "OpenAI Voice" and "ElevenLabs" as separate
|
||||
# tools — instead they see "Text-to-Speech" then pick a provider.
|
||||
|
||||
TOOL_CATEGORIES = [
|
||||
{
|
||||
"name": "Text-to-Speech",
|
||||
"icon": "🎤",
|
||||
"description": "Convert text to voice messages",
|
||||
"providers": [
|
||||
{
|
||||
"name": "Microsoft Edge TTS",
|
||||
"tag": "Free - no API key needed",
|
||||
"env_vars": [],
|
||||
"tts_provider": "edge",
|
||||
},
|
||||
{
|
||||
"name": "OpenAI TTS",
|
||||
"tag": "Premium - high quality voices",
|
||||
"env_vars": [
|
||||
{"key": "VOICE_TOOLS_OPENAI_KEY", "prompt": "OpenAI API key", "url": "https://platform.openai.com/api-keys"},
|
||||
],
|
||||
"tts_provider": "openai",
|
||||
},
|
||||
{
|
||||
"name": "ElevenLabs",
|
||||
"tag": "Premium - most natural voices",
|
||||
"env_vars": [
|
||||
{"key": "ELEVENLABS_API_KEY", "prompt": "ElevenLabs API key", "url": "https://elevenlabs.io/app/settings/api-keys"},
|
||||
],
|
||||
"tts_provider": "elevenlabs",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Web Search & Extract",
|
||||
"icon": "🔍",
|
||||
"description": "Search the web and extract content from URLs",
|
||||
"providers": [
|
||||
{
|
||||
"name": "Firecrawl Cloud",
|
||||
"tag": "Recommended - hosted service",
|
||||
"env_vars": [
|
||||
{"key": "FIRECRAWL_API_KEY", "prompt": "Firecrawl API key", "url": "https://firecrawl.dev"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Firecrawl Self-Hosted",
|
||||
"tag": "Free - run your own instance",
|
||||
"env_vars": [
|
||||
{"key": "FIRECRAWL_API_URL", "prompt": "Your Firecrawl instance URL (e.g., http://localhost:3002)"},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Image Generation",
|
||||
"icon": "🎨",
|
||||
"description": "Generate images from text prompts (FLUX 2 Pro + upscaling)",
|
||||
"providers": [
|
||||
{
|
||||
"name": "FAL.ai",
|
||||
"tag": "FLUX 2 Pro with auto-upscaling",
|
||||
"env_vars": [
|
||||
{"key": "FAL_KEY", "prompt": "FAL API key", "url": "https://fal.ai/dashboard/keys"},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Browser Automation",
|
||||
"icon": "🌐",
|
||||
"description": "Control a cloud browser for web interactions",
|
||||
"providers": [
|
||||
{
|
||||
"name": "Browserbase",
|
||||
"tag": "Cloud browser with stealth mode",
|
||||
"env_vars": [
|
||||
{"key": "BROWSERBASE_API_KEY", "prompt": "Browserbase API key", "url": "https://browserbase.com"},
|
||||
{"key": "BROWSERBASE_PROJECT_ID", "prompt": "Browserbase project ID"},
|
||||
],
|
||||
"post_setup": "browserbase",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Smart Home",
|
||||
"icon": "🏠",
|
||||
"description": "Control Home Assistant lights, switches, and devices",
|
||||
"providers": [
|
||||
{
|
||||
"name": "Home Assistant",
|
||||
"tag": "REST API integration",
|
||||
"env_vars": [
|
||||
{"key": "HASS_TOKEN", "prompt": "Home Assistant Long-Lived Access Token"},
|
||||
{"key": "HASS_URL", "prompt": "Home Assistant URL", "default": "http://homeassistant.local:8123"},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "RL Training",
|
||||
"icon": "🧪",
|
||||
"description": "Run reinforcement learning training jobs",
|
||||
"requires_python": (3, 11),
|
||||
"providers": [
|
||||
{
|
||||
"name": "Tinker / Atropos",
|
||||
"tag": "RL training platform",
|
||||
"env_vars": [
|
||||
{"key": "TINKER_API_KEY", "prompt": "Tinker API key", "url": "https://tinker-console.thinkingmachines.ai/keys"},
|
||||
{"key": "WANDB_API_KEY", "prompt": "WandB API key", "url": "https://wandb.ai/authorize"},
|
||||
],
|
||||
"post_setup": "rl_training",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "GitHub Integration",
|
||||
"icon": "🔧",
|
||||
"description": "Higher rate limits for Skills Hub + PR publishing",
|
||||
"providers": [
|
||||
{
|
||||
"name": "GitHub Personal Access Token",
|
||||
"tag": "For skill search, install, and publishing",
|
||||
"env_vars": [
|
||||
{"key": "GITHUB_TOKEN", "prompt": "GitHub Token (ghp_...)", "url": "https://github.com/settings/tokens"},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _run_post_setup(post_setup_key: str):
|
||||
"""Run post-setup hooks for tools that need extra installation steps."""
|
||||
if post_setup_key == "browserbase":
|
||||
import shutil
|
||||
node_modules = PROJECT_ROOT / "node_modules" / "agent-browser"
|
||||
if not node_modules.exists() and shutil.which("npm"):
|
||||
print_info(" Installing Node.js dependencies for browser tools...")
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
["npm", "install", "--silent"],
|
||||
capture_output=True, text=True, cwd=str(PROJECT_ROOT)
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print_success(" Node.js dependencies installed")
|
||||
else:
|
||||
print_warning(" npm install failed — run manually: cd ~/.hermes/hermes-agent && npm install")
|
||||
elif not node_modules.exists():
|
||||
print_warning(" Node.js not found — browser tools require: npm install (in the hermes-agent directory)")
|
||||
|
||||
elif post_setup_key == "rl_training":
|
||||
try:
|
||||
__import__("tinker_atropos")
|
||||
except ImportError:
|
||||
tinker_dir = PROJECT_ROOT / "tinker-atropos"
|
||||
if tinker_dir.exists() and (tinker_dir / "pyproject.toml").exists():
|
||||
print_info(" Installing tinker-atropos submodule...")
|
||||
import subprocess
|
||||
import shutil
|
||||
uv_bin = shutil.which("uv")
|
||||
if uv_bin:
|
||||
result = subprocess.run(
|
||||
[uv_bin, "pip", "install", "-e", str(tinker_dir)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
else:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "-e", str(tinker_dir)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print_success(" tinker-atropos installed")
|
||||
else:
|
||||
print_warning(" tinker-atropos install failed — run manually:")
|
||||
print_info(' uv pip install -e "./tinker-atropos"')
|
||||
else:
|
||||
print_warning(" tinker-atropos submodule not found — run:")
|
||||
print_info(" git submodule update --init --recursive")
|
||||
print_info(' uv pip install -e "./tinker-atropos"')
|
||||
# Tool categories and provider config are now in tools_config.py (shared
|
||||
# between `hermes tools` and `hermes setup tools`).
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -1805,187 +1623,17 @@ def setup_gateway(config: dict):
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Section 5: Tool Configuration (Category-First UX)
|
||||
# Section 5: Tool Configuration (delegates to unified tools_config.py)
|
||||
# =============================================================================
|
||||
|
||||
def setup_tools(config: dict):
|
||||
"""Configure tools with a category-first UX.
|
||||
"""Configure tools — delegates to the unified tools_command() in tools_config.py.
|
||||
|
||||
Instead of showing flat list of API keys, this shows tool categories
|
||||
(TTS, Web Search, Image Gen, etc.) and lets users pick a provider
|
||||
within each category.
|
||||
Both `hermes setup tools` and `hermes tools` use the same flow:
|
||||
platform selection → toolset toggles → provider/API key configuration.
|
||||
"""
|
||||
print_header("Tool Configuration")
|
||||
print_info("Select which tools you'd like to enable.")
|
||||
print_info("For tools with multiple providers, you'll choose one next.")
|
||||
print_info("You can always reconfigure later with 'hermes setup tools'.")
|
||||
print()
|
||||
|
||||
# Build checklist from TOOL_CATEGORIES
|
||||
# NOTE: Do NOT use color() / ANSI codes in menu labels —
|
||||
# simple_term_menu miscalculates widths and causes garbled redraws.
|
||||
checklist_labels = []
|
||||
for cat in TOOL_CATEGORIES:
|
||||
icon = cat.get("icon", "")
|
||||
name = cat["name"]
|
||||
desc = cat.get("description", "")
|
||||
|
||||
# Check if already configured — plain text only (no ANSI codes)
|
||||
configured = _is_tool_configured(cat)
|
||||
status = " [configured]" if configured else ""
|
||||
|
||||
checklist_labels.append(f"{icon} {name} - {desc}{status}")
|
||||
|
||||
# Pre-select tools that are already configured
|
||||
pre_selected = [i for i, cat in enumerate(TOOL_CATEGORIES) if _is_tool_configured(cat)]
|
||||
|
||||
selected_indices = prompt_checklist(
|
||||
"Which tools would you like to enable?",
|
||||
checklist_labels,
|
||||
pre_selected=pre_selected,
|
||||
)
|
||||
|
||||
# For each selected tool, configure its provider
|
||||
for idx in selected_indices:
|
||||
cat = TOOL_CATEGORIES[idx]
|
||||
_configure_tool_category(cat, config)
|
||||
|
||||
save_config(config)
|
||||
print()
|
||||
print_success("Tool configuration complete!")
|
||||
|
||||
|
||||
def _is_tool_configured(cat: dict) -> bool:
|
||||
"""Check if a tool category has at least one provider configured."""
|
||||
for provider in cat["providers"]:
|
||||
env_vars = provider.get("env_vars", [])
|
||||
if not env_vars:
|
||||
# No env vars needed (e.g., Edge TTS) — check if it's the active provider
|
||||
if provider.get("tts_provider"):
|
||||
from hermes_cli.config import load_config as _lc
|
||||
cfg = _lc()
|
||||
if cfg.get("tts", {}).get("provider") == provider["tts_provider"]:
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
elif all(get_env_value(v["key"]) for v in env_vars):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _configure_tool_category(cat: dict, config: dict):
|
||||
"""Configure a single tool category — pick provider and enter API keys."""
|
||||
icon = cat.get("icon", "")
|
||||
name = cat["name"]
|
||||
providers = cat["providers"]
|
||||
|
||||
# Check Python version requirement
|
||||
if cat.get("requires_python"):
|
||||
req = cat["requires_python"]
|
||||
if sys.version_info < req:
|
||||
print()
|
||||
print(color(f" ─── {icon} {name} ───", Colors.CYAN))
|
||||
print_error(f" Requires Python {req[0]}.{req[1]}+ (current: {sys.version_info.major}.{sys.version_info.minor})")
|
||||
print_info(" Upgrade Python and reinstall to enable this tool.")
|
||||
return
|
||||
|
||||
if len(providers) == 1:
|
||||
# Single provider — just configure it directly
|
||||
provider = providers[0]
|
||||
print()
|
||||
print(color(f" ─── {icon} {name} ({provider['name']}) ───", Colors.CYAN))
|
||||
if provider.get("tag"):
|
||||
print_info(f" {provider['tag']}")
|
||||
_configure_provider(provider, config, cat)
|
||||
else:
|
||||
# Multiple providers — let user choose
|
||||
print()
|
||||
print(color(f" ─── {icon} {name} — Choose a provider ───", Colors.CYAN))
|
||||
print()
|
||||
|
||||
# NOTE: Do NOT use color() / ANSI codes in menu labels —
|
||||
# simple_term_menu miscalculates widths and causes garbled redraws.
|
||||
provider_choices = []
|
||||
for p in providers:
|
||||
tag = f" ({p['tag']})" if p.get("tag") else ""
|
||||
configured = ""
|
||||
env_vars = p.get("env_vars", [])
|
||||
if not env_vars or all(get_env_value(v["key"]) for v in env_vars):
|
||||
# Check TTS provider match for edge
|
||||
if p.get("tts_provider"):
|
||||
if config.get("tts", {}).get("provider") == p["tts_provider"]:
|
||||
configured = " [active]"
|
||||
elif not env_vars:
|
||||
configured = " [active]"
|
||||
else:
|
||||
configured = " [configured]"
|
||||
provider_choices.append(f"{p['name']}{tag}{configured}")
|
||||
|
||||
# Detect current provider as default
|
||||
default_provider_idx = 0
|
||||
for i, p in enumerate(providers):
|
||||
if p.get("tts_provider") and config.get("tts", {}).get("provider") == p["tts_provider"]:
|
||||
default_provider_idx = i
|
||||
break
|
||||
env_vars = p.get("env_vars", [])
|
||||
if env_vars and all(get_env_value(v["key"]) for v in env_vars):
|
||||
default_provider_idx = i
|
||||
break
|
||||
|
||||
provider_idx = prompt_choice("Select provider:", provider_choices, default_provider_idx)
|
||||
provider = providers[provider_idx]
|
||||
|
||||
_configure_provider(provider, config, cat)
|
||||
|
||||
|
||||
def _configure_provider(provider: dict, config: dict, cat: dict):
|
||||
"""Configure a single provider — prompt for API keys and set config values."""
|
||||
env_vars = provider.get("env_vars", [])
|
||||
|
||||
# Set TTS provider in config if applicable
|
||||
if provider.get("tts_provider"):
|
||||
config.setdefault("tts", {})["provider"] = provider["tts_provider"]
|
||||
|
||||
if not env_vars:
|
||||
# No API keys needed (e.g., Edge TTS)
|
||||
print_success(f" {provider['name']} — no configuration needed!")
|
||||
return
|
||||
|
||||
# Prompt for each required env var
|
||||
all_configured = True
|
||||
for var in env_vars:
|
||||
existing = get_env_value(var["key"])
|
||||
if existing:
|
||||
print_success(f" {var['key']}: already configured")
|
||||
if prompt_yes_no(f" Update {var.get('prompt', var['key'])}?", False):
|
||||
value = prompt(f" {var.get('prompt', var['key'])}", password=True)
|
||||
if value:
|
||||
save_env_value(var["key"], value)
|
||||
print_success(" Updated")
|
||||
else:
|
||||
url = var.get("url", "")
|
||||
if url:
|
||||
print_info(f" Get yours at: {url}")
|
||||
|
||||
default_val = var.get("default", "")
|
||||
if default_val:
|
||||
value = prompt(f" {var.get('prompt', var['key'])}", default_val)
|
||||
else:
|
||||
value = prompt(f" {var.get('prompt', var['key'])}", password=True)
|
||||
|
||||
if value:
|
||||
save_env_value(var["key"], value)
|
||||
print_success(f" ✓ Saved")
|
||||
else:
|
||||
print_warning(f" Skipped")
|
||||
all_configured = False
|
||||
|
||||
# Run post-setup hooks if needed
|
||||
if provider.get("post_setup") and all_configured:
|
||||
_run_post_setup(provider["post_setup"])
|
||||
|
||||
if all_configured:
|
||||
print_success(f" {provider['name']} configured!")
|
||||
from hermes_cli.tools_config import tools_command
|
||||
tools_command()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"""
|
||||
Interactive tool configuration for Hermes Agent.
|
||||
Unified tool configuration for Hermes Agent.
|
||||
|
||||
`hermes tools` and `hermes setup tools` both enter this module.
|
||||
Select a platform → toggle toolsets on/off → for newly enabled tools
|
||||
that need API keys, run through provider-aware configuration.
|
||||
|
||||
`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.
|
||||
"""
|
||||
@@ -12,9 +15,63 @@ from typing import Dict, List, Set
|
||||
|
||||
import os
|
||||
|
||||
from hermes_cli.config import load_config, save_config, get_env_value, save_env_value
|
||||
from hermes_cli.config import (
|
||||
load_config, save_config, get_env_value, save_env_value,
|
||||
get_hermes_home,
|
||||
)
|
||||
from hermes_cli.colors import Colors, color
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
||||
|
||||
|
||||
# ─── UI Helpers (shared with setup.py) ────────────────────────────────────────
|
||||
|
||||
def _print_info(text: str):
|
||||
print(color(f" {text}", Colors.DIM))
|
||||
|
||||
def _print_success(text: str):
|
||||
print(color(f"✓ {text}", Colors.GREEN))
|
||||
|
||||
def _print_warning(text: str):
|
||||
print(color(f"⚠ {text}", Colors.YELLOW))
|
||||
|
||||
def _print_error(text: str):
|
||||
print(color(f"✗ {text}", Colors.RED))
|
||||
|
||||
def _prompt(question: str, default: str = None, password: bool = False) -> str:
|
||||
if default:
|
||||
display = f"{question} [{default}]: "
|
||||
else:
|
||||
display = f"{question}: "
|
||||
try:
|
||||
if password:
|
||||
import getpass
|
||||
value = getpass.getpass(color(display, Colors.YELLOW))
|
||||
else:
|
||||
value = input(color(display, Colors.YELLOW))
|
||||
return value.strip() or default or ""
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
return default or ""
|
||||
|
||||
def _prompt_yes_no(question: str, default: bool = True) -> bool:
|
||||
default_str = "Y/n" if default else "y/N"
|
||||
while True:
|
||||
try:
|
||||
value = input(color(f"{question} [{default_str}]: ", Colors.YELLOW)).strip().lower()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
return default
|
||||
if not value:
|
||||
return default
|
||||
if value in ('y', 'yes'):
|
||||
return True
|
||||
if value in ('n', 'no'):
|
||||
return False
|
||||
|
||||
|
||||
# ─── Toolset Registry ─────────────────────────────────────────────────────────
|
||||
|
||||
# Toolsets shown in the configurator, grouped for display.
|
||||
# Each entry: (toolset_name, label, description)
|
||||
# These map to keys in toolsets.py TOOLSETS dict.
|
||||
@@ -49,6 +106,181 @@ PLATFORMS = {
|
||||
}
|
||||
|
||||
|
||||
# ─── Tool Categories (provider-aware configuration) ──────────────────────────
|
||||
# Maps toolset keys to their provider options. When a toolset is newly enabled,
|
||||
# we use this to show provider selection and prompt for the right API keys.
|
||||
# Toolsets not in this map either need no config or use the simple fallback.
|
||||
|
||||
TOOL_CATEGORIES = {
|
||||
"tts": {
|
||||
"name": "Text-to-Speech",
|
||||
"icon": "🔊",
|
||||
"providers": [
|
||||
{
|
||||
"name": "Microsoft Edge TTS",
|
||||
"tag": "Free - no API key needed",
|
||||
"env_vars": [],
|
||||
"tts_provider": "edge",
|
||||
},
|
||||
{
|
||||
"name": "OpenAI TTS",
|
||||
"tag": "Premium - high quality voices",
|
||||
"env_vars": [
|
||||
{"key": "VOICE_TOOLS_OPENAI_KEY", "prompt": "OpenAI API key", "url": "https://platform.openai.com/api-keys"},
|
||||
],
|
||||
"tts_provider": "openai",
|
||||
},
|
||||
{
|
||||
"name": "ElevenLabs",
|
||||
"tag": "Premium - most natural voices",
|
||||
"env_vars": [
|
||||
{"key": "ELEVENLABS_API_KEY", "prompt": "ElevenLabs API key", "url": "https://elevenlabs.io/app/settings/api-keys"},
|
||||
],
|
||||
"tts_provider": "elevenlabs",
|
||||
},
|
||||
],
|
||||
},
|
||||
"web": {
|
||||
"name": "Web Search & Extract",
|
||||
"icon": "🔍",
|
||||
"providers": [
|
||||
{
|
||||
"name": "Firecrawl Cloud",
|
||||
"tag": "Recommended - hosted service",
|
||||
"env_vars": [
|
||||
{"key": "FIRECRAWL_API_KEY", "prompt": "Firecrawl API key", "url": "https://firecrawl.dev"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Firecrawl Self-Hosted",
|
||||
"tag": "Free - run your own instance",
|
||||
"env_vars": [
|
||||
{"key": "FIRECRAWL_API_URL", "prompt": "Your Firecrawl instance URL (e.g., http://localhost:3002)"},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"image_gen": {
|
||||
"name": "Image Generation",
|
||||
"icon": "🎨",
|
||||
"providers": [
|
||||
{
|
||||
"name": "FAL.ai",
|
||||
"tag": "FLUX 2 Pro with auto-upscaling",
|
||||
"env_vars": [
|
||||
{"key": "FAL_KEY", "prompt": "FAL API key", "url": "https://fal.ai/dashboard/keys"},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"browser": {
|
||||
"name": "Browser Automation",
|
||||
"icon": "🌐",
|
||||
"providers": [
|
||||
{
|
||||
"name": "Browserbase",
|
||||
"tag": "Cloud browser with stealth mode",
|
||||
"env_vars": [
|
||||
{"key": "BROWSERBASE_API_KEY", "prompt": "Browserbase API key", "url": "https://browserbase.com"},
|
||||
{"key": "BROWSERBASE_PROJECT_ID", "prompt": "Browserbase project ID"},
|
||||
],
|
||||
"post_setup": "browserbase",
|
||||
},
|
||||
],
|
||||
},
|
||||
"homeassistant": {
|
||||
"name": "Smart Home",
|
||||
"icon": "🏠",
|
||||
"providers": [
|
||||
{
|
||||
"name": "Home Assistant",
|
||||
"tag": "REST API integration",
|
||||
"env_vars": [
|
||||
{"key": "HASS_TOKEN", "prompt": "Home Assistant Long-Lived Access Token"},
|
||||
{"key": "HASS_URL", "prompt": "Home Assistant URL", "default": "http://homeassistant.local:8123"},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"rl": {
|
||||
"name": "RL Training",
|
||||
"icon": "🧪",
|
||||
"requires_python": (3, 11),
|
||||
"providers": [
|
||||
{
|
||||
"name": "Tinker / Atropos",
|
||||
"tag": "RL training platform",
|
||||
"env_vars": [
|
||||
{"key": "TINKER_API_KEY", "prompt": "Tinker API key", "url": "https://tinker-console.thinkingmachines.ai/keys"},
|
||||
{"key": "WANDB_API_KEY", "prompt": "WandB API key", "url": "https://wandb.ai/authorize"},
|
||||
],
|
||||
"post_setup": "rl_training",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
# Simple env-var requirements for toolsets NOT in TOOL_CATEGORIES.
|
||||
# Used as a fallback for tools like vision/moa that just need an API key.
|
||||
TOOLSET_ENV_REQUIREMENTS = {
|
||||
"vision": [("OPENROUTER_API_KEY", "https://openrouter.ai/keys")],
|
||||
"moa": [("OPENROUTER_API_KEY", "https://openrouter.ai/keys")],
|
||||
}
|
||||
|
||||
|
||||
# ─── Post-Setup Hooks ─────────────────────────────────────────────────────────
|
||||
|
||||
def _run_post_setup(post_setup_key: str):
|
||||
"""Run post-setup hooks for tools that need extra installation steps."""
|
||||
import shutil
|
||||
if post_setup_key == "browserbase":
|
||||
node_modules = PROJECT_ROOT / "node_modules" / "agent-browser"
|
||||
if not node_modules.exists() and shutil.which("npm"):
|
||||
_print_info(" Installing Node.js dependencies for browser tools...")
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
["npm", "install", "--silent"],
|
||||
capture_output=True, text=True, cwd=str(PROJECT_ROOT)
|
||||
)
|
||||
if result.returncode == 0:
|
||||
_print_success(" Node.js dependencies installed")
|
||||
else:
|
||||
_print_warning(" npm install failed - run manually: cd ~/.hermes/hermes-agent && npm install")
|
||||
elif not node_modules.exists():
|
||||
_print_warning(" Node.js not found - browser tools require: npm install (in hermes-agent directory)")
|
||||
|
||||
elif post_setup_key == "rl_training":
|
||||
try:
|
||||
__import__("tinker_atropos")
|
||||
except ImportError:
|
||||
tinker_dir = PROJECT_ROOT / "tinker-atropos"
|
||||
if tinker_dir.exists() and (tinker_dir / "pyproject.toml").exists():
|
||||
_print_info(" Installing tinker-atropos submodule...")
|
||||
import subprocess
|
||||
uv_bin = shutil.which("uv")
|
||||
if uv_bin:
|
||||
result = subprocess.run(
|
||||
[uv_bin, "pip", "install", "-e", str(tinker_dir)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
else:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "-e", str(tinker_dir)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
_print_success(" tinker-atropos installed")
|
||||
else:
|
||||
_print_warning(" tinker-atropos install failed - run manually:")
|
||||
_print_info(' uv pip install -e "./tinker-atropos"')
|
||||
else:
|
||||
_print_warning(" tinker-atropos submodule not found - run:")
|
||||
_print_info(" git submodule update --init --recursive")
|
||||
_print_info(' uv pip install -e "./tinker-atropos"')
|
||||
|
||||
|
||||
# ─── Platform / Toolset Helpers ───────────────────────────────────────────────
|
||||
|
||||
def _get_enabled_platforms() -> List[str]:
|
||||
"""Return platform keys that are configured (have tokens or are CLI)."""
|
||||
enabled = ["cli"]
|
||||
@@ -97,6 +329,28 @@ def _save_platform_tools(config: dict, platform: str, enabled_toolset_keys: Set[
|
||||
save_config(config)
|
||||
|
||||
|
||||
def _toolset_has_keys(ts_key: str) -> bool:
|
||||
"""Check if a toolset's required API keys are configured."""
|
||||
# Check TOOL_CATEGORIES first (provider-aware)
|
||||
cat = TOOL_CATEGORIES.get(ts_key)
|
||||
if cat:
|
||||
for provider in cat["providers"]:
|
||||
env_vars = provider.get("env_vars", [])
|
||||
if not env_vars:
|
||||
return True # Free provider (e.g., Edge TTS)
|
||||
if all(get_env_value(v["key"]) for v in env_vars):
|
||||
return True
|
||||
return False
|
||||
|
||||
# Fallback to simple requirements
|
||||
requirements = TOOLSET_ENV_REQUIREMENTS.get(ts_key, [])
|
||||
if not requirements:
|
||||
return True
|
||||
return all(get_env_value(var) for var, _ in requirements)
|
||||
|
||||
|
||||
# ─── Menu Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _prompt_choice(question: str, choices: list, default: int = 0) -> int:
|
||||
"""Single-select menu (arrow keys)."""
|
||||
print(color(question, Colors.YELLOW))
|
||||
@@ -114,7 +368,7 @@ def _prompt_choice(question: str, choices: list, default: int = 0) -> int:
|
||||
)
|
||||
idx = menu.show()
|
||||
if idx is None:
|
||||
sys.exit(0)
|
||||
return default
|
||||
print()
|
||||
return idx
|
||||
except (ImportError, NotImplementedError):
|
||||
@@ -132,15 +386,7 @@ def _prompt_choice(question: str, choices: list, default: int = 0) -> int:
|
||||
return idx
|
||||
except (ValueError, KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def _toolset_has_keys(ts_key: str) -> bool:
|
||||
"""Check if a toolset's required API keys are configured."""
|
||||
requirements = TOOLSET_ENV_REQUIREMENTS.get(ts_key, [])
|
||||
if not requirements:
|
||||
return True
|
||||
return all(get_env_value(var) for var, _ in requirements)
|
||||
return default
|
||||
|
||||
|
||||
def _prompt_toolset_checklist(platform_label: str, enabled: Set[str]) -> Set[str]:
|
||||
@@ -150,8 +396,8 @@ def _prompt_toolset_checklist(platform_label: str, enabled: Set[str]) -> Set[str
|
||||
labels = []
|
||||
for ts_key, ts_label, ts_desc in CONFIGURABLE_TOOLSETS:
|
||||
suffix = ""
|
||||
if not _toolset_has_keys(ts_key) and TOOLSET_ENV_REQUIREMENTS.get(ts_key):
|
||||
suffix = " ⚠ no API key"
|
||||
if not _toolset_has_keys(ts_key) and (TOOL_CATEGORIES.get(ts_key) or TOOLSET_ENV_REQUIREMENTS.get(ts_key)):
|
||||
suffix = " [no API key]"
|
||||
labels.append(f"{ts_label} ({ts_desc}){suffix}")
|
||||
|
||||
pre_selected_indices = [
|
||||
@@ -302,77 +548,294 @@ def _prompt_toolset_checklist(platform_label: str, enabled: Set[str]) -> Set[str
|
||||
return {CONFIGURABLE_TOOLSETS[i][0] for i in selected}
|
||||
|
||||
|
||||
# Map toolset keys to the env vars they require and where to get them
|
||||
TOOLSET_ENV_REQUIREMENTS = {
|
||||
"web": [("FIRECRAWL_API_KEY", "https://firecrawl.dev/")],
|
||||
"browser": [("BROWSERBASE_API_KEY", "https://browserbase.com/"),
|
||||
("BROWSERBASE_PROJECT_ID", None)],
|
||||
"vision": [("OPENROUTER_API_KEY", "https://openrouter.ai/keys")],
|
||||
"image_gen": [("FAL_KEY", "https://fal.ai/")],
|
||||
"moa": [("OPENROUTER_API_KEY", "https://openrouter.ai/keys")],
|
||||
"tts": [], # Edge TTS is free, no key needed
|
||||
"rl": [("TINKER_API_KEY", "https://tinker-console.thinkingmachines.ai/keys"),
|
||||
("WANDB_API_KEY", "https://wandb.ai/authorize")],
|
||||
"homeassistant": [("HASS_TOKEN", "Home Assistant > Profile > Long-Lived Access Tokens"),
|
||||
("HASS_URL", None)],
|
||||
}
|
||||
# ─── Provider-Aware Configuration ────────────────────────────────────────────
|
||||
|
||||
def _configure_toolset(ts_key: str, config: dict):
|
||||
"""Configure a toolset - provider selection + API keys.
|
||||
|
||||
Uses TOOL_CATEGORIES for provider-aware config, falls back to simple
|
||||
env var prompts for toolsets not in TOOL_CATEGORIES.
|
||||
"""
|
||||
cat = TOOL_CATEGORIES.get(ts_key)
|
||||
|
||||
if cat:
|
||||
_configure_tool_category(ts_key, cat, config)
|
||||
else:
|
||||
# Simple fallback for vision, moa, etc.
|
||||
_configure_simple_requirements(ts_key)
|
||||
|
||||
|
||||
def _check_and_prompt_requirements(newly_enabled: Set[str]):
|
||||
"""Check if newly enabled toolsets have missing API keys and offer to set them up."""
|
||||
for ts_key in sorted(newly_enabled):
|
||||
requirements = TOOLSET_ENV_REQUIREMENTS.get(ts_key, [])
|
||||
if not requirements:
|
||||
continue
|
||||
def _configure_tool_category(ts_key: str, cat: dict, config: dict):
|
||||
"""Configure a tool category with provider selection."""
|
||||
icon = cat.get("icon", "")
|
||||
name = cat["name"]
|
||||
providers = cat["providers"]
|
||||
|
||||
missing = [(var, url) for var, url in requirements if not get_env_value(var)]
|
||||
if not missing:
|
||||
continue
|
||||
|
||||
ts_label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts_key), ts_key)
|
||||
print()
|
||||
print(color(f" ⚠ {ts_label} requires configuration:", Colors.YELLOW))
|
||||
|
||||
for var, url in missing:
|
||||
if url:
|
||||
print(color(f" {var}", Colors.CYAN) + color(f" ({url})", Colors.DIM))
|
||||
else:
|
||||
print(color(f" {var}", Colors.CYAN))
|
||||
|
||||
print()
|
||||
try:
|
||||
response = input(color(" Set up now? [Y/n] ", Colors.YELLOW)).strip().lower()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
# Check Python version requirement
|
||||
if cat.get("requires_python"):
|
||||
req = cat["requires_python"]
|
||||
if sys.version_info < req:
|
||||
print()
|
||||
continue
|
||||
_print_error(f" {name} requires Python {req[0]}.{req[1]}+ (current: {sys.version_info.major}.{sys.version_info.minor})")
|
||||
_print_info(" Upgrade Python and reinstall to enable this tool.")
|
||||
return
|
||||
|
||||
if response in ("", "y", "yes"):
|
||||
for var, url in missing:
|
||||
if url:
|
||||
print(color(f" Get key at: {url}", Colors.DIM))
|
||||
try:
|
||||
import getpass
|
||||
value = getpass.getpass(color(f" {var}: ", Colors.YELLOW))
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
break
|
||||
if value.strip():
|
||||
save_env_value(var, value.strip())
|
||||
print(color(f" ✓ Saved", Colors.GREEN))
|
||||
if len(providers) == 1:
|
||||
# Single provider - configure directly
|
||||
provider = providers[0]
|
||||
print()
|
||||
print(color(f" --- {icon} {name} ({provider['name']}) ---", Colors.CYAN))
|
||||
if provider.get("tag"):
|
||||
_print_info(f" {provider['tag']}")
|
||||
_configure_provider(provider, config)
|
||||
else:
|
||||
# Multiple providers - let user choose
|
||||
print()
|
||||
print(color(f" --- {icon} {name} - Choose a provider ---", Colors.CYAN))
|
||||
print()
|
||||
|
||||
# Plain text labels only (no ANSI codes in menu items)
|
||||
provider_choices = []
|
||||
for p in providers:
|
||||
tag = f" ({p['tag']})" if p.get("tag") else ""
|
||||
configured = ""
|
||||
env_vars = p.get("env_vars", [])
|
||||
if not env_vars or all(get_env_value(v["key"]) for v in env_vars):
|
||||
if p.get("tts_provider") and config.get("tts", {}).get("provider") == p["tts_provider"]:
|
||||
configured = " [active]"
|
||||
elif not env_vars:
|
||||
configured = " [active]" if config.get("tts", {}).get("provider", "edge") == p.get("tts_provider", "") else ""
|
||||
else:
|
||||
print(color(f" Skipped", Colors.DIM))
|
||||
configured = " [configured]"
|
||||
provider_choices.append(f"{p['name']}{tag}{configured}")
|
||||
|
||||
# Detect current provider as default
|
||||
default_idx = 0
|
||||
for i, p in enumerate(providers):
|
||||
if p.get("tts_provider") and config.get("tts", {}).get("provider") == p["tts_provider"]:
|
||||
default_idx = i
|
||||
break
|
||||
env_vars = p.get("env_vars", [])
|
||||
if env_vars and all(get_env_value(v["key"]) for v in env_vars):
|
||||
default_idx = i
|
||||
break
|
||||
|
||||
provider_idx = _prompt_choice(" Select provider:", provider_choices, default_idx)
|
||||
_configure_provider(providers[provider_idx], config)
|
||||
|
||||
|
||||
def _configure_provider(provider: dict, config: dict):
|
||||
"""Configure a single provider - prompt for API keys and set config."""
|
||||
env_vars = provider.get("env_vars", [])
|
||||
|
||||
# Set TTS provider in config if applicable
|
||||
if provider.get("tts_provider"):
|
||||
config.setdefault("tts", {})["provider"] = provider["tts_provider"]
|
||||
|
||||
if not env_vars:
|
||||
_print_success(f" {provider['name']} - no configuration needed!")
|
||||
return
|
||||
|
||||
# Prompt for each required env var
|
||||
all_configured = True
|
||||
for var in env_vars:
|
||||
existing = get_env_value(var["key"])
|
||||
if existing:
|
||||
_print_success(f" {var['key']}: already configured")
|
||||
# Don't ask to update - this is a new enable flow.
|
||||
# Reconfigure is handled separately.
|
||||
else:
|
||||
print(color(" Skipped — configure later with 'hermes setup'", Colors.DIM))
|
||||
url = var.get("url", "")
|
||||
if url:
|
||||
_print_info(f" Get yours at: {url}")
|
||||
|
||||
default_val = var.get("default", "")
|
||||
if default_val:
|
||||
value = _prompt(f" {var.get('prompt', var['key'])}", default_val)
|
||||
else:
|
||||
value = _prompt(f" {var.get('prompt', var['key'])}", password=True)
|
||||
|
||||
if value:
|
||||
save_env_value(var["key"], value)
|
||||
_print_success(f" Saved")
|
||||
else:
|
||||
_print_warning(f" Skipped")
|
||||
all_configured = False
|
||||
|
||||
# Run post-setup hooks if needed
|
||||
if provider.get("post_setup") and all_configured:
|
||||
_run_post_setup(provider["post_setup"])
|
||||
|
||||
if all_configured:
|
||||
_print_success(f" {provider['name']} configured!")
|
||||
|
||||
|
||||
def tools_command(args):
|
||||
"""Entry point for `hermes tools`."""
|
||||
def _configure_simple_requirements(ts_key: str):
|
||||
"""Simple fallback for toolsets that just need env vars (no provider selection)."""
|
||||
requirements = TOOLSET_ENV_REQUIREMENTS.get(ts_key, [])
|
||||
if not requirements:
|
||||
return
|
||||
|
||||
missing = [(var, url) for var, url in requirements if not get_env_value(var)]
|
||||
if not missing:
|
||||
return
|
||||
|
||||
ts_label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts_key), ts_key)
|
||||
print()
|
||||
print(color(f" {ts_label} requires configuration:", Colors.YELLOW))
|
||||
|
||||
for var, url in missing:
|
||||
if url:
|
||||
_print_info(f" Get key at: {url}")
|
||||
value = _prompt(f" {var}", password=True)
|
||||
if value and value.strip():
|
||||
save_env_value(var, value.strip())
|
||||
_print_success(f" Saved")
|
||||
else:
|
||||
_print_warning(f" Skipped")
|
||||
|
||||
|
||||
def _reconfigure_tool(config: dict):
|
||||
"""Let user reconfigure an existing tool's provider or API key."""
|
||||
# Build list of configurable tools that are currently set up
|
||||
configurable = []
|
||||
for ts_key, ts_label, _ in CONFIGURABLE_TOOLSETS:
|
||||
cat = TOOL_CATEGORIES.get(ts_key)
|
||||
reqs = TOOLSET_ENV_REQUIREMENTS.get(ts_key)
|
||||
if cat or reqs:
|
||||
if _toolset_has_keys(ts_key):
|
||||
configurable.append((ts_key, ts_label))
|
||||
|
||||
if not configurable:
|
||||
_print_info("No configured tools to reconfigure.")
|
||||
return
|
||||
|
||||
choices = [label for _, label in configurable]
|
||||
choices.append("Cancel")
|
||||
|
||||
idx = _prompt_choice(" Which tool would you like to reconfigure?", choices, len(choices) - 1)
|
||||
|
||||
if idx >= len(configurable):
|
||||
return # Cancel
|
||||
|
||||
ts_key, ts_label = configurable[idx]
|
||||
cat = TOOL_CATEGORIES.get(ts_key)
|
||||
|
||||
if cat:
|
||||
_configure_tool_category_for_reconfig(ts_key, cat, config)
|
||||
else:
|
||||
_reconfigure_simple_requirements(ts_key)
|
||||
|
||||
save_config(config)
|
||||
|
||||
|
||||
def _configure_tool_category_for_reconfig(ts_key: str, cat: dict, config: dict):
|
||||
"""Reconfigure a tool category - provider selection + API key update."""
|
||||
icon = cat.get("icon", "")
|
||||
name = cat["name"]
|
||||
providers = cat["providers"]
|
||||
|
||||
if len(providers) == 1:
|
||||
provider = providers[0]
|
||||
print()
|
||||
print(color(f" --- {icon} {name} ({provider['name']}) ---", Colors.CYAN))
|
||||
_reconfigure_provider(provider, config)
|
||||
else:
|
||||
print()
|
||||
print(color(f" --- {icon} {name} - Choose a provider ---", Colors.CYAN))
|
||||
print()
|
||||
|
||||
provider_choices = []
|
||||
for p in providers:
|
||||
tag = f" ({p['tag']})" if p.get("tag") else ""
|
||||
configured = ""
|
||||
env_vars = p.get("env_vars", [])
|
||||
if not env_vars or all(get_env_value(v["key"]) for v in env_vars):
|
||||
if p.get("tts_provider") and config.get("tts", {}).get("provider") == p["tts_provider"]:
|
||||
configured = " [active]"
|
||||
elif not env_vars:
|
||||
configured = ""
|
||||
else:
|
||||
configured = " [configured]"
|
||||
provider_choices.append(f"{p['name']}{tag}{configured}")
|
||||
|
||||
default_idx = 0
|
||||
for i, p in enumerate(providers):
|
||||
if p.get("tts_provider") and config.get("tts", {}).get("provider") == p["tts_provider"]:
|
||||
default_idx = i
|
||||
break
|
||||
env_vars = p.get("env_vars", [])
|
||||
if env_vars and all(get_env_value(v["key"]) for v in env_vars):
|
||||
default_idx = i
|
||||
break
|
||||
|
||||
provider_idx = _prompt_choice(" Select provider:", provider_choices, default_idx)
|
||||
_reconfigure_provider(providers[provider_idx], config)
|
||||
|
||||
|
||||
def _reconfigure_provider(provider: dict, config: dict):
|
||||
"""Reconfigure a provider - update API keys."""
|
||||
env_vars = provider.get("env_vars", [])
|
||||
|
||||
if provider.get("tts_provider"):
|
||||
config.setdefault("tts", {})["provider"] = provider["tts_provider"]
|
||||
_print_success(f" TTS provider set to: {provider['tts_provider']}")
|
||||
|
||||
if not env_vars:
|
||||
_print_success(f" {provider['name']} - no configuration needed!")
|
||||
return
|
||||
|
||||
for var in env_vars:
|
||||
existing = get_env_value(var["key"])
|
||||
if existing:
|
||||
_print_info(f" {var['key']}: configured ({existing[:8]}...)")
|
||||
url = var.get("url", "")
|
||||
if url:
|
||||
_print_info(f" Get yours at: {url}")
|
||||
default_val = var.get("default", "")
|
||||
value = _prompt(f" {var.get('prompt', var['key'])} (Enter to keep current)", password=not default_val)
|
||||
if value and value.strip():
|
||||
save_env_value(var["key"], value.strip())
|
||||
_print_success(f" Updated")
|
||||
else:
|
||||
_print_info(f" Kept current")
|
||||
|
||||
|
||||
def _reconfigure_simple_requirements(ts_key: str):
|
||||
"""Reconfigure simple env var requirements."""
|
||||
requirements = TOOLSET_ENV_REQUIREMENTS.get(ts_key, [])
|
||||
if not requirements:
|
||||
return
|
||||
|
||||
ts_label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts_key), ts_key)
|
||||
print()
|
||||
print(color(f" {ts_label}:", Colors.CYAN))
|
||||
|
||||
for var, url in requirements:
|
||||
existing = get_env_value(var)
|
||||
if existing:
|
||||
_print_info(f" {var}: configured ({existing[:8]}...)")
|
||||
if url:
|
||||
_print_info(f" Get key at: {url}")
|
||||
value = _prompt(f" {var} (Enter to keep current)", password=True)
|
||||
if value and value.strip():
|
||||
save_env_value(var, value.strip())
|
||||
_print_success(f" Updated")
|
||||
else:
|
||||
_print_info(f" Kept current")
|
||||
|
||||
|
||||
# ─── Main Entry Point ─────────────────────────────────────────────────────────
|
||||
|
||||
def tools_command(args=None):
|
||||
"""Entry point for `hermes tools` and `hermes setup 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(color(" Tools that need API keys will be configured when enabled.", Colors.DIM))
|
||||
print()
|
||||
|
||||
# Build platform choices
|
||||
@@ -380,22 +843,28 @@ def tools_command(args):
|
||||
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)
|
||||
platform_choices.append(f"Configure {pinfo['label']} ({count}/{total} enabled)")
|
||||
platform_keys.append(pkey)
|
||||
|
||||
platform_choices.append("Done — save and exit")
|
||||
platform_choices.append("Reconfigure an existing tool's provider or API key")
|
||||
platform_choices.append("Done")
|
||||
|
||||
while True:
|
||||
idx = _prompt_choice("Select a platform to configure:", platform_choices, default=0)
|
||||
idx = _prompt_choice("Select an option:", platform_choices, default=0)
|
||||
|
||||
# "Done" selected
|
||||
if idx == len(platform_keys):
|
||||
if idx == len(platform_keys) + 1:
|
||||
break
|
||||
|
||||
# "Reconfigure" selected
|
||||
if idx == len(platform_keys):
|
||||
_reconfigure_tool(config)
|
||||
print()
|
||||
continue
|
||||
|
||||
pkey = platform_keys[idx]
|
||||
pinfo = PLATFORMS[pkey]
|
||||
|
||||
@@ -418,11 +887,15 @@ def tools_command(args):
|
||||
label = next((l for k, l, _ in CONFIGURABLE_TOOLSETS if k == ts), ts)
|
||||
print(color(f" - {label}", Colors.RED))
|
||||
|
||||
# Prompt for missing API keys on newly enabled toolsets
|
||||
# Configure newly enabled toolsets that need API keys
|
||||
if added:
|
||||
_check_and_prompt_requirements(added)
|
||||
for ts_key in sorted(added):
|
||||
if TOOL_CATEGORIES.get(ts_key) or TOOLSET_ENV_REQUIREMENTS.get(ts_key):
|
||||
if not _toolset_has_keys(ts_key):
|
||||
_configure_toolset(ts_key, config)
|
||||
|
||||
_save_platform_tools(config, pkey, new_enabled)
|
||||
save_config(config)
|
||||
print(color(f" ✓ Saved {pinfo['label']} configuration", Colors.GREEN))
|
||||
else:
|
||||
print(color(f" No changes to {pinfo['label']}", Colors.DIM))
|
||||
|
||||
Reference in New Issue
Block a user