Implement configuration migration system and enhance CLI setup
- Introduced a configuration migration system to check for missing required environment variables and outdated config fields, prompting users for necessary inputs during updates. - Enhanced the CLI with new commands for checking and migrating configuration, improving user experience by providing clear guidance on required settings. - Updated the setup wizard to detect existing installations and offer quick setup options for missing configurations, streamlining the user onboarding process. - Improved messaging throughout the CLI to inform users about the status of their configuration and any required actions.
This commit is contained in:
parent
fef504f038
commit
3ee788dacc
3 changed files with 598 additions and 105 deletions
|
|
@ -16,7 +16,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional, List, Tuple
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
@ -98,8 +98,219 @@ DEFAULT_CONFIG = {
|
||||||
"compact": False,
|
"compact": False,
|
||||||
"personality": "kawaii",
|
"personality": "kawaii",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
# Config schema version - bump this when adding new required fields
|
||||||
|
"_config_version": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Config Migration System
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Required environment variables with metadata for migration prompts
|
||||||
|
REQUIRED_ENV_VARS = {
|
||||||
|
"OPENROUTER_API_KEY": {
|
||||||
|
"description": "OpenRouter API key (required for vision, web scraping, and tools)",
|
||||||
|
"prompt": "OpenRouter API key",
|
||||||
|
"url": "https://openrouter.ai/keys",
|
||||||
|
"required": True,
|
||||||
|
"password": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optional environment variables that enhance functionality
|
||||||
|
OPTIONAL_ENV_VARS = {
|
||||||
|
"FIRECRAWL_API_KEY": {
|
||||||
|
"description": "Firecrawl API key for web search and scraping",
|
||||||
|
"prompt": "Firecrawl API key",
|
||||||
|
"url": "https://firecrawl.dev/",
|
||||||
|
"tools": ["web_search", "web_extract"],
|
||||||
|
"password": True,
|
||||||
|
},
|
||||||
|
"BROWSERBASE_API_KEY": {
|
||||||
|
"description": "Browserbase API key for browser automation",
|
||||||
|
"prompt": "Browserbase API key",
|
||||||
|
"url": "https://browserbase.com/",
|
||||||
|
"tools": ["browser_navigate", "browser_click", "etc."],
|
||||||
|
"password": True,
|
||||||
|
},
|
||||||
|
"BROWSERBASE_PROJECT_ID": {
|
||||||
|
"description": "Browserbase project ID",
|
||||||
|
"prompt": "Browserbase project ID",
|
||||||
|
"url": "https://browserbase.com/",
|
||||||
|
"tools": ["browser_navigate", "browser_click", "etc."],
|
||||||
|
"password": False,
|
||||||
|
},
|
||||||
|
"FAL_KEY": {
|
||||||
|
"description": "FAL API key for image generation",
|
||||||
|
"prompt": "FAL API key",
|
||||||
|
"url": "https://fal.ai/",
|
||||||
|
"tools": ["image_generate"],
|
||||||
|
"password": True,
|
||||||
|
},
|
||||||
|
"OPENAI_BASE_URL": {
|
||||||
|
"description": "Custom OpenAI-compatible API endpoint URL",
|
||||||
|
"prompt": "API base URL (e.g., https://api.example.com/v1)",
|
||||||
|
"url": None,
|
||||||
|
"password": False,
|
||||||
|
},
|
||||||
|
"OPENAI_API_KEY": {
|
||||||
|
"description": "API key for custom OpenAI-compatible endpoint",
|
||||||
|
"prompt": "API key for custom endpoint",
|
||||||
|
"url": None,
|
||||||
|
"password": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_missing_env_vars(required_only: bool = False) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Check which environment variables are missing.
|
||||||
|
|
||||||
|
Returns list of dicts with var info for missing variables.
|
||||||
|
"""
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
# Check required vars
|
||||||
|
for var_name, info in REQUIRED_ENV_VARS.items():
|
||||||
|
if not get_env_value(var_name):
|
||||||
|
missing.append({"name": var_name, **info, "is_required": True})
|
||||||
|
|
||||||
|
# Check optional vars (if not required_only)
|
||||||
|
if not required_only:
|
||||||
|
for var_name, info in OPTIONAL_ENV_VARS.items():
|
||||||
|
if not get_env_value(var_name):
|
||||||
|
missing.append({"name": var_name, **info, "is_required": False})
|
||||||
|
|
||||||
|
return missing
|
||||||
|
|
||||||
|
|
||||||
|
def get_missing_config_fields() -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Check which config fields are missing or outdated.
|
||||||
|
|
||||||
|
Returns list of missing/outdated fields.
|
||||||
|
"""
|
||||||
|
config = load_config()
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
# Check for new top-level keys in DEFAULT_CONFIG
|
||||||
|
for key, default_value in DEFAULT_CONFIG.items():
|
||||||
|
if key.startswith('_'):
|
||||||
|
continue # Skip internal keys
|
||||||
|
if key not in config:
|
||||||
|
missing.append({
|
||||||
|
"key": key,
|
||||||
|
"default": default_value,
|
||||||
|
"description": f"New config section: {key}",
|
||||||
|
})
|
||||||
|
elif isinstance(default_value, dict):
|
||||||
|
# Check nested keys
|
||||||
|
for subkey, subvalue in default_value.items():
|
||||||
|
if subkey not in config.get(key, {}):
|
||||||
|
missing.append({
|
||||||
|
"key": f"{key}.{subkey}",
|
||||||
|
"default": subvalue,
|
||||||
|
"description": f"New config option: {key}.{subkey}",
|
||||||
|
})
|
||||||
|
|
||||||
|
return missing
|
||||||
|
|
||||||
|
|
||||||
|
def check_config_version() -> Tuple[int, int]:
|
||||||
|
"""
|
||||||
|
Check config version.
|
||||||
|
|
||||||
|
Returns (current_version, latest_version).
|
||||||
|
"""
|
||||||
|
config = load_config()
|
||||||
|
current = config.get("_config_version", 0)
|
||||||
|
latest = DEFAULT_CONFIG.get("_config_version", 1)
|
||||||
|
return current, latest
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Migrate config to latest version, prompting for new required fields.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interactive: If True, prompt user for missing values
|
||||||
|
quiet: If True, suppress output
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with migration results: {"env_added": [...], "config_added": [...], "warnings": [...]}
|
||||||
|
"""
|
||||||
|
results = {"env_added": [], "config_added": [], "warnings": []}
|
||||||
|
|
||||||
|
# Check config version
|
||||||
|
current_ver, latest_ver = check_config_version()
|
||||||
|
|
||||||
|
if current_ver < latest_ver and not quiet:
|
||||||
|
print(f"Config version: {current_ver} → {latest_ver}")
|
||||||
|
|
||||||
|
# Check for missing required env vars
|
||||||
|
missing_env = get_missing_env_vars(required_only=True)
|
||||||
|
|
||||||
|
if missing_env and not quiet:
|
||||||
|
print("\n⚠️ Missing required environment variables:")
|
||||||
|
for var in missing_env:
|
||||||
|
print(f" • {var['name']}: {var['description']}")
|
||||||
|
|
||||||
|
if interactive and missing_env:
|
||||||
|
print("\nLet's configure them now:\n")
|
||||||
|
for var in missing_env:
|
||||||
|
if var.get("url"):
|
||||||
|
print(f" Get your key at: {var['url']}")
|
||||||
|
|
||||||
|
if var.get("password"):
|
||||||
|
import getpass
|
||||||
|
value = getpass.getpass(f" {var['prompt']}: ")
|
||||||
|
else:
|
||||||
|
value = input(f" {var['prompt']}: ").strip()
|
||||||
|
|
||||||
|
if value:
|
||||||
|
save_env_value(var["name"], value)
|
||||||
|
results["env_added"].append(var["name"])
|
||||||
|
print(f" ✓ Saved {var['name']}")
|
||||||
|
else:
|
||||||
|
results["warnings"].append(f"Skipped {var['name']} - some features may not work")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check for missing config fields
|
||||||
|
missing_config = get_missing_config_fields()
|
||||||
|
|
||||||
|
if missing_config:
|
||||||
|
config = load_config()
|
||||||
|
|
||||||
|
for field in missing_config:
|
||||||
|
key = field["key"]
|
||||||
|
default = field["default"]
|
||||||
|
|
||||||
|
# Add with default value
|
||||||
|
if "." in key:
|
||||||
|
# Nested key
|
||||||
|
parent, child = key.split(".", 1)
|
||||||
|
if parent not in config:
|
||||||
|
config[parent] = {}
|
||||||
|
config[parent][child] = default
|
||||||
|
else:
|
||||||
|
config[key] = default
|
||||||
|
|
||||||
|
results["config_added"].append(key)
|
||||||
|
if not quiet:
|
||||||
|
print(f" ✓ Added {key} = {default}")
|
||||||
|
|
||||||
|
# Update version and save
|
||||||
|
config["_config_version"] = latest_ver
|
||||||
|
save_config(config)
|
||||||
|
elif current_ver < latest_ver:
|
||||||
|
# Just update version
|
||||||
|
config = load_config()
|
||||||
|
config["_config_version"] = latest_ver
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def load_config() -> Dict[str, Any]:
|
def load_config() -> Dict[str, Any]:
|
||||||
"""Load configuration from ~/.hermes/config.yaml."""
|
"""Load configuration from ~/.hermes/config.yaml."""
|
||||||
|
|
@ -395,6 +606,106 @@ def config_command(args):
|
||||||
elif subcmd == "env-path":
|
elif subcmd == "env-path":
|
||||||
print(get_env_path())
|
print(get_env_path())
|
||||||
|
|
||||||
|
elif subcmd == "migrate":
|
||||||
|
print()
|
||||||
|
print(color("🔄 Checking configuration for updates...", Colors.CYAN, Colors.BOLD))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check what's missing
|
||||||
|
missing_env = get_missing_env_vars(required_only=False)
|
||||||
|
missing_config = get_missing_config_fields()
|
||||||
|
current_ver, latest_ver = check_config_version()
|
||||||
|
|
||||||
|
if not missing_env and not missing_config and current_ver >= latest_ver:
|
||||||
|
print(color("✓ Configuration is up to date!", Colors.GREEN))
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Show what needs to be updated
|
||||||
|
if current_ver < latest_ver:
|
||||||
|
print(f" Config version: {current_ver} → {latest_ver}")
|
||||||
|
|
||||||
|
if missing_config:
|
||||||
|
print(f"\n {len(missing_config)} new config option(s) will be added with defaults")
|
||||||
|
|
||||||
|
required_missing = [v for v in missing_env if v.get("is_required")]
|
||||||
|
optional_missing = [v for v in missing_env if not v.get("is_required")]
|
||||||
|
|
||||||
|
if required_missing:
|
||||||
|
print(f"\n ⚠️ {len(required_missing)} required API key(s) missing:")
|
||||||
|
for var in required_missing:
|
||||||
|
print(f" • {var['name']}")
|
||||||
|
|
||||||
|
if optional_missing:
|
||||||
|
print(f"\n ℹ️ {len(optional_missing)} optional API key(s) not configured:")
|
||||||
|
for var in optional_missing:
|
||||||
|
tools = var.get("tools", [])
|
||||||
|
tools_str = f" (enables: {', '.join(tools[:2])})" if tools else ""
|
||||||
|
print(f" • {var['name']}{tools_str}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Run migration
|
||||||
|
results = migrate_config(interactive=True, quiet=False)
|
||||||
|
|
||||||
|
print()
|
||||||
|
if results["env_added"] or results["config_added"]:
|
||||||
|
print(color("✓ Configuration updated!", Colors.GREEN))
|
||||||
|
|
||||||
|
if results["warnings"]:
|
||||||
|
print()
|
||||||
|
for warning in results["warnings"]:
|
||||||
|
print(color(f" ⚠️ {warning}", Colors.YELLOW))
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
elif subcmd == "check":
|
||||||
|
# Non-interactive check for what's missing
|
||||||
|
print()
|
||||||
|
print(color("📋 Configuration Status", Colors.CYAN, Colors.BOLD))
|
||||||
|
print()
|
||||||
|
|
||||||
|
current_ver, latest_ver = check_config_version()
|
||||||
|
if current_ver >= latest_ver:
|
||||||
|
print(f" Config version: {current_ver} ✓")
|
||||||
|
else:
|
||||||
|
print(color(f" Config version: {current_ver} → {latest_ver} (update available)", Colors.YELLOW))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(color(" Required:", Colors.BOLD))
|
||||||
|
for var_name in REQUIRED_ENV_VARS:
|
||||||
|
if get_env_value(var_name):
|
||||||
|
print(f" ✓ {var_name}")
|
||||||
|
else:
|
||||||
|
print(color(f" ✗ {var_name} (missing)", Colors.RED))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(color(" Optional:", Colors.BOLD))
|
||||||
|
for var_name, info in OPTIONAL_ENV_VARS.items():
|
||||||
|
if get_env_value(var_name):
|
||||||
|
print(f" ✓ {var_name}")
|
||||||
|
else:
|
||||||
|
tools = info.get("tools", [])
|
||||||
|
tools_str = f" → {', '.join(tools[:2])}" if tools else ""
|
||||||
|
print(color(f" ○ {var_name}{tools_str}", Colors.DIM))
|
||||||
|
|
||||||
|
missing_config = get_missing_config_fields()
|
||||||
|
if missing_config:
|
||||||
|
print()
|
||||||
|
print(color(f" {len(missing_config)} new config option(s) available", Colors.YELLOW))
|
||||||
|
print(f" Run 'hermes config migrate' to add them")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(f"Unknown config command: {subcmd}")
|
print(f"Unknown config command: {subcmd}")
|
||||||
|
print()
|
||||||
|
print("Available commands:")
|
||||||
|
print(" hermes config Show current configuration")
|
||||||
|
print(" hermes config edit Open config in editor")
|
||||||
|
print(" hermes config set K V Set a config value")
|
||||||
|
print(" hermes config check Check for missing/outdated config")
|
||||||
|
print(" hermes config migrate Update config with new options")
|
||||||
|
print(" hermes config path Show config file path")
|
||||||
|
print(" hermes config env-path Show .env file path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,47 @@ def cmd_update(args):
|
||||||
print("→ Updating Node.js dependencies...")
|
print("→ Updating Node.js dependencies...")
|
||||||
subprocess.run(["npm", "install", "--silent"], cwd=PROJECT_ROOT, check=False)
|
subprocess.run(["npm", "install", "--silent"], cwd=PROJECT_ROOT, check=False)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("✓ Code updated!")
|
||||||
|
|
||||||
|
# Check for config migrations
|
||||||
|
print()
|
||||||
|
print("→ Checking configuration for new options...")
|
||||||
|
|
||||||
|
from hermes_cli.config import (
|
||||||
|
get_missing_env_vars, get_missing_config_fields,
|
||||||
|
check_config_version, migrate_config
|
||||||
|
)
|
||||||
|
|
||||||
|
missing_env = get_missing_env_vars(required_only=True)
|
||||||
|
missing_config = get_missing_config_fields()
|
||||||
|
current_ver, latest_ver = check_config_version()
|
||||||
|
|
||||||
|
needs_migration = missing_env or missing_config or current_ver < latest_ver
|
||||||
|
|
||||||
|
if needs_migration:
|
||||||
|
print()
|
||||||
|
if missing_env:
|
||||||
|
print(f" ⚠️ {len(missing_env)} new required setting(s) need configuration")
|
||||||
|
if missing_config:
|
||||||
|
print(f" ℹ️ {len(missing_config)} new config option(s) available")
|
||||||
|
|
||||||
|
print()
|
||||||
|
response = input("Would you like to configure them now? [Y/n]: ").strip().lower()
|
||||||
|
|
||||||
|
if response in ('', 'y', 'yes'):
|
||||||
|
print()
|
||||||
|
results = migrate_config(interactive=True, quiet=False)
|
||||||
|
|
||||||
|
if results["env_added"] or results["config_added"]:
|
||||||
|
print()
|
||||||
|
print("✓ Configuration updated!")
|
||||||
|
else:
|
||||||
|
print()
|
||||||
|
print("Skipped. Run 'hermes config migrate' later to configure.")
|
||||||
|
else:
|
||||||
|
print(" ✓ Configuration is up to date")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("✓ Update complete!")
|
print("✓ Update complete!")
|
||||||
print()
|
print()
|
||||||
|
|
@ -380,6 +421,12 @@ For more help on a command:
|
||||||
# config env-path
|
# config env-path
|
||||||
config_env = config_subparsers.add_parser("env-path", help="Print .env file path")
|
config_env = config_subparsers.add_parser("env-path", help="Print .env file path")
|
||||||
|
|
||||||
|
# config check
|
||||||
|
config_check = config_subparsers.add_parser("check", help="Check for missing/outdated config")
|
||||||
|
|
||||||
|
# config migrate
|
||||||
|
config_migrate = config_subparsers.add_parser("migrate", help="Update config with new options")
|
||||||
|
|
||||||
config_parser.set_defaults(func=cmd_config)
|
config_parser.set_defaults(func=cmd_config)
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,106 @@ def prompt_yes_no(question: str, default: bool = True) -> bool:
|
||||||
print_error("Please enter 'y' or 'n'")
|
print_error("Please enter 'y' or 'n'")
|
||||||
|
|
||||||
|
|
||||||
|
def _print_setup_summary(config: dict, hermes_home):
|
||||||
|
"""Print the setup completion summary."""
|
||||||
|
# Tool availability summary
|
||||||
|
print()
|
||||||
|
print_header("Tool Availability Summary")
|
||||||
|
|
||||||
|
tool_status = []
|
||||||
|
|
||||||
|
# OpenRouter (required for vision, moa)
|
||||||
|
if get_env_value('OPENROUTER_API_KEY'):
|
||||||
|
tool_status.append(("Vision (image analysis)", True, None))
|
||||||
|
tool_status.append(("Mixture of Agents", True, None))
|
||||||
|
else:
|
||||||
|
tool_status.append(("Vision (image analysis)", False, "OPENROUTER_API_KEY"))
|
||||||
|
tool_status.append(("Mixture of Agents", False, "OPENROUTER_API_KEY"))
|
||||||
|
|
||||||
|
# Firecrawl (web tools)
|
||||||
|
if get_env_value('FIRECRAWL_API_KEY'):
|
||||||
|
tool_status.append(("Web Search & Extract", True, None))
|
||||||
|
else:
|
||||||
|
tool_status.append(("Web Search & Extract", False, "FIRECRAWL_API_KEY"))
|
||||||
|
|
||||||
|
# Browserbase (browser tools)
|
||||||
|
if get_env_value('BROWSERBASE_API_KEY'):
|
||||||
|
tool_status.append(("Browser Automation", True, None))
|
||||||
|
else:
|
||||||
|
tool_status.append(("Browser Automation", False, "BROWSERBASE_API_KEY"))
|
||||||
|
|
||||||
|
# FAL (image generation)
|
||||||
|
if get_env_value('FAL_KEY'):
|
||||||
|
tool_status.append(("Image Generation", True, None))
|
||||||
|
else:
|
||||||
|
tool_status.append(("Image Generation", False, "FAL_KEY"))
|
||||||
|
|
||||||
|
# Terminal (always available if system deps met)
|
||||||
|
tool_status.append(("Terminal/Commands", True, None))
|
||||||
|
|
||||||
|
# Skills (always available if skills dir exists)
|
||||||
|
tool_status.append(("Skills Knowledge Base", True, None))
|
||||||
|
|
||||||
|
# Print status
|
||||||
|
available_count = sum(1 for _, avail, _ in tool_status if avail)
|
||||||
|
total_count = len(tool_status)
|
||||||
|
|
||||||
|
print_info(f"{available_count}/{total_count} tool categories available:")
|
||||||
|
print()
|
||||||
|
|
||||||
|
for name, available, missing_var in tool_status:
|
||||||
|
if available:
|
||||||
|
print(f" {color('✓', Colors.GREEN)} {name}")
|
||||||
|
else:
|
||||||
|
print(f" {color('✗', Colors.RED)} {name} {color(f'(missing {missing_var})', Colors.DIM)}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
disabled_tools = [(name, var) for name, avail, var in tool_status if not avail]
|
||||||
|
if disabled_tools:
|
||||||
|
print_warning("Some tools are disabled. Run 'hermes setup' again to configure them,")
|
||||||
|
print_warning("or edit ~/.hermes/.env directly to add the missing API keys.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Done banner
|
||||||
|
print()
|
||||||
|
print(color("┌─────────────────────────────────────────────────────────┐", Colors.GREEN))
|
||||||
|
print(color("│ ✓ Setup Complete! │", Colors.GREEN))
|
||||||
|
print(color("└─────────────────────────────────────────────────────────┘", Colors.GREEN))
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Show file locations prominently
|
||||||
|
print(color("📁 All your files are in ~/.hermes/:", Colors.CYAN, Colors.BOLD))
|
||||||
|
print()
|
||||||
|
print(f" {color('Settings:', Colors.YELLOW)} {get_config_path()}")
|
||||||
|
print(f" {color('API Keys:', Colors.YELLOW)} {get_env_path()}")
|
||||||
|
print(f" {color('Data:', Colors.YELLOW)} {hermes_home}/cron/, sessions/, logs/")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(color("─" * 60, Colors.DIM))
|
||||||
|
print()
|
||||||
|
print(color("📝 To edit your configuration:", Colors.CYAN, Colors.BOLD))
|
||||||
|
print()
|
||||||
|
print(f" {color('hermes config', Colors.GREEN)} View current settings")
|
||||||
|
print(f" {color('hermes config edit', Colors.GREEN)} Open config in your editor")
|
||||||
|
print(f" {color('hermes config set KEY VALUE', Colors.GREEN)}")
|
||||||
|
print(f" Set a specific value")
|
||||||
|
print()
|
||||||
|
print(f" Or edit the files directly:")
|
||||||
|
print(f" {color(f'nano {get_config_path()}', Colors.DIM)}")
|
||||||
|
print(f" {color(f'nano {get_env_path()}', Colors.DIM)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(color("─" * 60, Colors.DIM))
|
||||||
|
print()
|
||||||
|
print(color("🚀 Ready to go!", Colors.CYAN, Colors.BOLD))
|
||||||
|
print()
|
||||||
|
print(f" {color('hermes', Colors.GREEN)} Start chatting")
|
||||||
|
print(f" {color('hermes gateway', Colors.GREEN)} Start messaging gateway")
|
||||||
|
print(f" {color('hermes doctor', Colors.GREEN)} Check for issues")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
def run_setup_wizard(args):
|
def run_setup_wizard(args):
|
||||||
"""Run the interactive setup wizard."""
|
"""Run the interactive setup wizard."""
|
||||||
ensure_hermes_home()
|
ensure_hermes_home()
|
||||||
|
|
@ -159,6 +259,24 @@ def run_setup_wizard(args):
|
||||||
config = load_config()
|
config = load_config()
|
||||||
hermes_home = get_hermes_home()
|
hermes_home = get_hermes_home()
|
||||||
|
|
||||||
|
# Check if this is an existing installation with config
|
||||||
|
is_existing = get_env_value("OPENROUTER_API_KEY") is not None or get_config_path().exists()
|
||||||
|
|
||||||
|
# Import migration helpers
|
||||||
|
from hermes_cli.config import (
|
||||||
|
get_missing_env_vars, get_missing_config_fields,
|
||||||
|
check_config_version, migrate_config,
|
||||||
|
REQUIRED_ENV_VARS, OPTIONAL_ENV_VARS
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check what's missing
|
||||||
|
missing_required = [v for v in get_missing_env_vars(required_only=False) if v.get("is_required")]
|
||||||
|
missing_optional = [v for v in get_missing_env_vars(required_only=False) if not v.get("is_required")]
|
||||||
|
missing_config = get_missing_config_fields()
|
||||||
|
current_ver, latest_ver = check_config_version()
|
||||||
|
|
||||||
|
has_missing = missing_required or missing_optional or missing_config or current_ver < latest_ver
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(color("┌─────────────────────────────────────────────────────────┐", Colors.MAGENTA))
|
print(color("┌─────────────────────────────────────────────────────────┐", Colors.MAGENTA))
|
||||||
print(color("│ 🦋 Hermes Agent Setup Wizard │", Colors.MAGENTA))
|
print(color("│ 🦋 Hermes Agent Setup Wizard │", Colors.MAGENTA))
|
||||||
|
|
@ -167,8 +285,126 @@ def run_setup_wizard(args):
|
||||||
print(color("│ Press Ctrl+C at any time to exit. │", Colors.MAGENTA))
|
print(color("│ Press Ctrl+C at any time to exit. │", Colors.MAGENTA))
|
||||||
print(color("└─────────────────────────────────────────────────────────┘", Colors.MAGENTA))
|
print(color("└─────────────────────────────────────────────────────────┘", Colors.MAGENTA))
|
||||||
|
|
||||||
|
# If existing installation, show what's missing and offer quick mode
|
||||||
|
quick_mode = False
|
||||||
|
if is_existing and has_missing:
|
||||||
|
print()
|
||||||
|
print_header("Existing Installation Detected")
|
||||||
|
print_success("You already have Hermes configured!")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if missing_required:
|
||||||
|
print_warning(f" {len(missing_required)} required setting(s) missing:")
|
||||||
|
for var in missing_required:
|
||||||
|
print(f" • {var['name']}")
|
||||||
|
|
||||||
|
if missing_optional:
|
||||||
|
print_info(f" {len(missing_optional)} optional tool(s) not configured:")
|
||||||
|
for var in missing_optional[:3]: # Show first 3
|
||||||
|
tools = var.get("tools", [])
|
||||||
|
tools_str = f" → {', '.join(tools[:2])}" if tools else ""
|
||||||
|
print(f" • {var['name']}{tools_str}")
|
||||||
|
if len(missing_optional) > 3:
|
||||||
|
print(f" • ...and {len(missing_optional) - 3} more")
|
||||||
|
|
||||||
|
if missing_config:
|
||||||
|
print_info(f" {len(missing_config)} new config option(s) available")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
setup_choices = [
|
||||||
|
"Quick setup - just configure missing items",
|
||||||
|
"Full setup - reconfigure everything",
|
||||||
|
"Skip - exit setup"
|
||||||
|
]
|
||||||
|
|
||||||
|
choice = prompt_choice("What would you like to do?", setup_choices, 0)
|
||||||
|
|
||||||
|
if choice == 0:
|
||||||
|
quick_mode = True
|
||||||
|
elif choice == 2:
|
||||||
|
print()
|
||||||
|
print_info("Exiting. Run 'hermes setup' again when ready.")
|
||||||
|
return
|
||||||
|
# choice == 1 continues with full setup
|
||||||
|
|
||||||
|
elif is_existing and not has_missing:
|
||||||
|
print()
|
||||||
|
print_header("Configuration Status")
|
||||||
|
print_success("Your configuration is complete!")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not prompt_yes_no("Would you like to reconfigure anyway?", False):
|
||||||
|
print()
|
||||||
|
print_info("Exiting. Your configuration is already set up.")
|
||||||
|
print_info(f"Config: {get_config_path()}")
|
||||||
|
print_info(f"Secrets: {get_env_path()}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Quick mode: only configure missing items
|
||||||
|
if quick_mode:
|
||||||
|
print()
|
||||||
|
print_header("Quick Setup - Missing Items Only")
|
||||||
|
|
||||||
|
# Handle missing required env vars
|
||||||
|
if missing_required:
|
||||||
|
for var in missing_required:
|
||||||
|
print()
|
||||||
|
print(color(f" {var['name']}", Colors.CYAN))
|
||||||
|
print_info(f" {var.get('description', '')}")
|
||||||
|
if var.get("url"):
|
||||||
|
print_info(f" Get key at: {var['url']}")
|
||||||
|
|
||||||
|
if var.get("password"):
|
||||||
|
value = prompt(f" {var.get('prompt', var['name'])}", password=True)
|
||||||
|
else:
|
||||||
|
value = prompt(f" {var.get('prompt', var['name'])}")
|
||||||
|
|
||||||
|
if value:
|
||||||
|
save_env_value(var["name"], value)
|
||||||
|
print_success(f" Saved {var['name']}")
|
||||||
|
else:
|
||||||
|
print_warning(f" Skipped {var['name']}")
|
||||||
|
|
||||||
|
# Handle missing optional env vars
|
||||||
|
if missing_optional:
|
||||||
|
print()
|
||||||
|
print_header("Optional Tools (Quick Setup)")
|
||||||
|
|
||||||
|
for var in missing_optional:
|
||||||
|
tools = var.get("tools", [])
|
||||||
|
tools_str = f" (enables: {', '.join(tools[:2])})" if tools else ""
|
||||||
|
|
||||||
|
if prompt_yes_no(f"Configure {var['name']}{tools_str}?", False):
|
||||||
|
if var.get("url"):
|
||||||
|
print_info(f" Get key at: {var['url']}")
|
||||||
|
|
||||||
|
if var.get("password"):
|
||||||
|
value = prompt(f" {var.get('prompt', var['name'])}", password=True)
|
||||||
|
else:
|
||||||
|
value = prompt(f" {var.get('prompt', var['name'])}")
|
||||||
|
|
||||||
|
if value:
|
||||||
|
save_env_value(var["name"], value)
|
||||||
|
print_success(f" Saved")
|
||||||
|
|
||||||
|
# Handle missing config fields
|
||||||
|
if missing_config:
|
||||||
|
print()
|
||||||
|
print_info(f"Adding {len(missing_config)} new config option(s) with defaults...")
|
||||||
|
for field in missing_config:
|
||||||
|
print_success(f" Added {field['key']} = {field['default']}")
|
||||||
|
|
||||||
|
# Update config version
|
||||||
|
config["_config_version"] = latest_ver
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
# Jump to summary
|
||||||
|
_print_setup_summary(config, hermes_home)
|
||||||
|
return
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Step 0: Show paths
|
# Step 0: Show paths (full setup)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
print_header("Configuration Location")
|
print_header("Configuration Location")
|
||||||
print_info(f"Config file: {get_config_path()}")
|
print_info(f"Config file: {get_config_path()}")
|
||||||
|
|
@ -586,108 +822,7 @@ def run_setup_wizard(args):
|
||||||
print_success(" Configured ✓")
|
print_success(" Configured ✓")
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Save config
|
# Save config and show summary
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
save_config(config)
|
save_config(config)
|
||||||
|
_print_setup_summary(config, hermes_home)
|
||||||
# =========================================================================
|
|
||||||
# Tool Availability Summary
|
|
||||||
# =========================================================================
|
|
||||||
print()
|
|
||||||
print_header("Tool Availability Summary")
|
|
||||||
|
|
||||||
# Check which tools are available
|
|
||||||
tool_status = []
|
|
||||||
|
|
||||||
# OpenRouter (required for vision, moa)
|
|
||||||
if get_env_value('OPENROUTER_API_KEY'):
|
|
||||||
tool_status.append(("Vision (image analysis)", True, None))
|
|
||||||
tool_status.append(("Mixture of Agents", True, None))
|
|
||||||
else:
|
|
||||||
tool_status.append(("Vision (image analysis)", False, "OPENROUTER_API_KEY"))
|
|
||||||
tool_status.append(("Mixture of Agents", False, "OPENROUTER_API_KEY"))
|
|
||||||
|
|
||||||
# Firecrawl (web tools)
|
|
||||||
if get_env_value('FIRECRAWL_API_KEY'):
|
|
||||||
tool_status.append(("Web Search & Extract", True, None))
|
|
||||||
else:
|
|
||||||
tool_status.append(("Web Search & Extract", False, "FIRECRAWL_API_KEY"))
|
|
||||||
|
|
||||||
# Browserbase (browser tools)
|
|
||||||
if get_env_value('BROWSERBASE_API_KEY'):
|
|
||||||
tool_status.append(("Browser Automation", True, None))
|
|
||||||
else:
|
|
||||||
tool_status.append(("Browser Automation", False, "BROWSERBASE_API_KEY"))
|
|
||||||
|
|
||||||
# FAL (image generation)
|
|
||||||
if get_env_value('FAL_KEY'):
|
|
||||||
tool_status.append(("Image Generation", True, None))
|
|
||||||
else:
|
|
||||||
tool_status.append(("Image Generation", False, "FAL_KEY"))
|
|
||||||
|
|
||||||
# Terminal (always available if system deps met)
|
|
||||||
tool_status.append(("Terminal/Commands", True, None))
|
|
||||||
|
|
||||||
# Skills (always available if skills dir exists)
|
|
||||||
tool_status.append(("Skills Knowledge Base", True, None))
|
|
||||||
|
|
||||||
# Print status
|
|
||||||
available_count = sum(1 for _, avail, _ in tool_status if avail)
|
|
||||||
total_count = len(tool_status)
|
|
||||||
|
|
||||||
print_info(f"{available_count}/{total_count} tool categories available:")
|
|
||||||
print()
|
|
||||||
|
|
||||||
for name, available, missing_var in tool_status:
|
|
||||||
if available:
|
|
||||||
print(f" {color('✓', Colors.GREEN)} {name}")
|
|
||||||
else:
|
|
||||||
print(f" {color('✗', Colors.RED)} {name} {color(f'(missing {missing_var})', Colors.DIM)}")
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
disabled_tools = [(name, var) for name, avail, var in tool_status if not avail]
|
|
||||||
if disabled_tools:
|
|
||||||
print_warning("Some tools are disabled. Run 'hermes setup' again to configure them,")
|
|
||||||
print_warning("or edit ~/.hermes/.env directly to add the missing API keys.")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# Done!
|
|
||||||
# =========================================================================
|
|
||||||
print()
|
|
||||||
print(color("┌─────────────────────────────────────────────────────────┐", Colors.GREEN))
|
|
||||||
print(color("│ ✓ Setup Complete! │", Colors.GREEN))
|
|
||||||
print(color("└─────────────────────────────────────────────────────────┘", Colors.GREEN))
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Show file locations prominently
|
|
||||||
print(color("📁 All your files are in ~/.hermes/:", Colors.CYAN, Colors.BOLD))
|
|
||||||
print()
|
|
||||||
print(f" {color('Settings:', Colors.YELLOW)} {get_config_path()}")
|
|
||||||
print(f" {color('API Keys:', Colors.YELLOW)} {get_env_path()}")
|
|
||||||
print(f" {color('Data:', Colors.YELLOW)} {hermes_home}/cron/, sessions/, logs/")
|
|
||||||
print()
|
|
||||||
|
|
||||||
print(color("─" * 60, Colors.DIM))
|
|
||||||
print()
|
|
||||||
print(color("📝 To edit your configuration:", Colors.CYAN, Colors.BOLD))
|
|
||||||
print()
|
|
||||||
print(f" {color('hermes config', Colors.GREEN)} View current settings")
|
|
||||||
print(f" {color('hermes config edit', Colors.GREEN)} Open config in your editor")
|
|
||||||
print(f" {color('hermes config set KEY VALUE', Colors.GREEN)}")
|
|
||||||
print(f" Set a specific value")
|
|
||||||
print()
|
|
||||||
print(f" Or edit the files directly:")
|
|
||||||
print(f" {color(f'nano {get_config_path()}', Colors.DIM)}")
|
|
||||||
print(f" {color(f'nano {get_env_path()}', Colors.DIM)}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
print(color("─" * 60, Colors.DIM))
|
|
||||||
print()
|
|
||||||
print(color("🚀 Ready to go!", Colors.CYAN, Colors.BOLD))
|
|
||||||
print()
|
|
||||||
print(f" {color('hermes', Colors.GREEN)} Start chatting")
|
|
||||||
print(f" {color('hermes gateway', Colors.GREEN)} Start messaging gateway")
|
|
||||||
print(f" {color('hermes doctor', Colors.GREEN)} Check for issues")
|
|
||||||
print()
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue