From 3ee788dacc79b5938b8558f1f0b459ca3ded5b48 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Mon, 2 Feb 2026 19:39:23 -0800 Subject: [PATCH] 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. --- hermes_cli/config.py | 313 ++++++++++++++++++++++++++++++++++++++- hermes_cli/main.py | 47 ++++++ hermes_cli/setup.py | 343 ++++++++++++++++++++++++++++++------------- 3 files changed, 598 insertions(+), 105 deletions(-) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index ad6423581..6efcaa7f8 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -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) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index a16fd7f1c..c51ab8d61 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -170,6 +170,47 @@ def cmd_update(args): print("→ Updating Node.js dependencies...") 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("✓ Update complete!") print() @@ -380,6 +421,12 @@ For more help on a command: # config env-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) # ========================================================================= diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 946021a2a..4b4e5f3b0 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -152,6 +152,106 @@ def prompt_yes_no(question: str, default: bool = True) -> bool: 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): """Run the interactive setup wizard.""" ensure_hermes_home() @@ -159,6 +259,24 @@ def run_setup_wizard(args): config = load_config() 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(color("┌─────────────────────────────────────────────────────────┐", 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("└─────────────────────────────────────────────────────────┘", 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_info(f"Config file: {get_config_path()}") @@ -586,108 +822,7 @@ def run_setup_wizard(args): print_success(" Configured ✓") # ========================================================================= - # Save config + # Save config and show summary # ========================================================================= save_config(config) - - # ========================================================================= - # 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() + _print_setup_summary(config, hermes_home)