mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 01:21:43 +00:00
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 subprocess
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
|
||||
import yaml
|
||||
|
||||
|
|
@ -98,8 +98,219 @@ DEFAULT_CONFIG = {
|
|||
"compact": False,
|
||||
"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]:
|
||||
"""Load configuration from ~/.hermes/config.yaml."""
|
||||
|
|
@ -395,6 +606,106 @@ def config_command(args):
|
|||
elif subcmd == "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:
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue