""" Doctor command for hermes CLI. Diagnoses issues with Hermes Agent setup. """ import os import sys import subprocess import shutil from pathlib import Path PROJECT_ROOT = Path(__file__).parent.parent.resolve() # ANSI colors class Colors: RESET = "\033[0m" BOLD = "\033[1m" DIM = "\033[2m" RED = "\033[31m" GREEN = "\033[32m" YELLOW = "\033[33m" CYAN = "\033[36m" def color(text: str, *codes) -> str: if not sys.stdout.isatty(): return text return "".join(codes) + text + Colors.RESET def check_ok(text: str, detail: str = ""): print(f" {color('✓', Colors.GREEN)} {text}" + (f" {color(detail, Colors.DIM)}" if detail else "")) def check_warn(text: str, detail: str = ""): print(f" {color('⚠', Colors.YELLOW)} {text}" + (f" {color(detail, Colors.DIM)}" if detail else "")) def check_fail(text: str, detail: str = ""): print(f" {color('✗', Colors.RED)} {text}" + (f" {color(detail, Colors.DIM)}" if detail else "")) def check_info(text: str): print(f" {color('→', Colors.CYAN)} {text}") def run_doctor(args): """Run diagnostic checks.""" should_fix = getattr(args, 'fix', False) issues = [] print() print(color("┌─────────────────────────────────────────────────────────┐", Colors.CYAN)) print(color("│ 🩺 Hermes Doctor │", Colors.CYAN)) print(color("└─────────────────────────────────────────────────────────┘", Colors.CYAN)) # ========================================================================= # Check: Python version # ========================================================================= print() print(color("◆ Python Environment", Colors.CYAN, Colors.BOLD)) py_version = sys.version_info if py_version >= (3, 11): check_ok(f"Python {py_version.major}.{py_version.minor}.{py_version.micro}") elif py_version >= (3, 10): check_ok(f"Python {py_version.major}.{py_version.minor}.{py_version.micro}") check_warn("Python 3.11+ recommended for RL Training tools (tinker requires >= 3.11)") elif py_version >= (3, 8): check_warn(f"Python {py_version.major}.{py_version.minor}.{py_version.micro}", "(3.10+ recommended)") else: check_fail(f"Python {py_version.major}.{py_version.minor}.{py_version.micro}", "(3.10+ required)") issues.append("Upgrade Python to 3.10+") # Check if in virtual environment in_venv = sys.prefix != sys.base_prefix if in_venv: check_ok("Virtual environment active") else: check_warn("Not in virtual environment", "(recommended)") # ========================================================================= # Check: Required packages # ========================================================================= print() print(color("◆ Required Packages", Colors.CYAN, Colors.BOLD)) required_packages = [ ("openai", "OpenAI SDK"), ("rich", "Rich (terminal UI)"), ("dotenv", "python-dotenv"), ("yaml", "PyYAML"), ("httpx", "HTTPX"), ] optional_packages = [ ("croniter", "Croniter (cron expressions)"), ("browserbase", "Browserbase SDK"), ("telegram", "python-telegram-bot"), ("discord", "discord.py"), ] for module, name in required_packages: try: __import__(module) check_ok(name) except ImportError: check_fail(name, "(missing)") issues.append(f"Install {name}: uv pip install {module}") for module, name in optional_packages: try: __import__(module) check_ok(name, "(optional)") except ImportError: check_warn(name, "(optional, not installed)") # ========================================================================= # Check: Configuration files # ========================================================================= print() print(color("◆ Configuration Files", Colors.CYAN, Colors.BOLD)) env_path = PROJECT_ROOT / '.env' if env_path.exists(): check_ok(".env file exists") # Check for common issues content = env_path.read_text() if "OPENROUTER_API_KEY" in content or "ANTHROPIC_API_KEY" in content: check_ok("API key configured") else: check_warn("No API key found in .env") issues.append("Run 'hermes setup' to configure API keys") else: check_fail(".env file missing") check_info("Run 'hermes setup' to create one") issues.append("Run 'hermes setup' to create .env") config_path = PROJECT_ROOT / 'cli-config.yaml' if config_path.exists(): check_ok("cli-config.yaml exists") else: check_warn("cli-config.yaml not found", "(using defaults)") # ========================================================================= # Check: Directory structure # ========================================================================= print() print(color("◆ Directory Structure", Colors.CYAN, Colors.BOLD)) hermes_home = Path.home() / ".hermes" if hermes_home.exists(): check_ok("~/.hermes directory exists") else: check_warn("~/.hermes not found", "(will be created on first use)") logs_dir = PROJECT_ROOT / "logs" if logs_dir.exists(): check_ok("logs/ directory exists") else: check_warn("logs/ not found", "(will be created on first use)") # ========================================================================= # Check: External tools # ========================================================================= print() print(color("◆ External Tools", Colors.CYAN, Colors.BOLD)) # Git if shutil.which("git"): check_ok("git") else: check_warn("git not found", "(optional)") # ripgrep (optional, for faster file search) if shutil.which("rg"): check_ok("ripgrep (rg)", "(faster file search)") else: check_warn("ripgrep (rg) not found", "(file search uses grep fallback)") check_info("Install for faster search: sudo apt install ripgrep") # Docker (optional) terminal_env = os.getenv("TERMINAL_ENV", "local") if terminal_env == "docker": if shutil.which("docker"): # Check if docker daemon is running result = subprocess.run(["docker", "info"], capture_output=True) if result.returncode == 0: check_ok("docker", "(daemon running)") else: check_fail("docker daemon not running") issues.append("Start Docker daemon") else: check_fail("docker not found", "(required for TERMINAL_ENV=docker)") issues.append("Install Docker or change TERMINAL_ENV") else: if shutil.which("docker"): check_ok("docker", "(optional)") else: check_warn("docker not found", "(optional)") # SSH (if using ssh backend) if terminal_env == "ssh": ssh_host = os.getenv("TERMINAL_SSH_HOST") if ssh_host: # Try to connect result = subprocess.run( ["ssh", "-o", "ConnectTimeout=5", "-o", "BatchMode=yes", ssh_host, "echo ok"], capture_output=True, text=True ) if result.returncode == 0: check_ok(f"SSH connection to {ssh_host}") else: check_fail(f"SSH connection to {ssh_host}") issues.append(f"Check SSH configuration for {ssh_host}") else: check_fail("TERMINAL_SSH_HOST not set", "(required for TERMINAL_ENV=ssh)") issues.append("Set TERMINAL_SSH_HOST in .env") # ========================================================================= # Check: API connectivity # ========================================================================= print() print(color("◆ API Connectivity", Colors.CYAN, Colors.BOLD)) openrouter_key = os.getenv("OPENROUTER_API_KEY") if openrouter_key: try: import httpx response = httpx.get( "https://openrouter.ai/api/v1/models", headers={"Authorization": f"Bearer {openrouter_key}"}, timeout=10 ) if response.status_code == 200: check_ok("OpenRouter API") elif response.status_code == 401: check_fail("OpenRouter API", "(invalid API key)") issues.append("Check OPENROUTER_API_KEY in .env") else: check_fail("OpenRouter API", f"(HTTP {response.status_code})") except Exception as e: check_fail("OpenRouter API", f"({e})") issues.append("Check network connectivity") else: check_warn("OpenRouter API", "(not configured)") anthropic_key = os.getenv("ANTHROPIC_API_KEY") if anthropic_key: try: import httpx response = httpx.get( "https://api.anthropic.com/v1/models", headers={ "x-api-key": anthropic_key, "anthropic-version": "2023-06-01" }, timeout=10 ) if response.status_code == 200: check_ok("Anthropic API") elif response.status_code == 401: check_fail("Anthropic API", "(invalid API key)") else: # Note: Anthropic may not have /models endpoint check_warn("Anthropic API", "(couldn't verify)") except Exception as e: check_warn("Anthropic API", f"({e})") # ========================================================================= # Check: Submodules # ========================================================================= print() print(color("◆ Submodules", Colors.CYAN, Colors.BOLD)) # mini-swe-agent (terminal tool backend) mini_swe_dir = PROJECT_ROOT / "mini-swe-agent" if mini_swe_dir.exists() and (mini_swe_dir / "pyproject.toml").exists(): try: __import__("minisweagent") check_ok("mini-swe-agent", "(terminal backend)") except ImportError: check_warn("mini-swe-agent found but not installed", "(run: uv pip install -e ./mini-swe-agent)") issues.append("Install mini-swe-agent: uv pip install -e ./mini-swe-agent") else: check_warn("mini-swe-agent not found", "(run: git submodule update --init --recursive)") # tinker-atropos (RL training backend) tinker_dir = PROJECT_ROOT / "tinker-atropos" if tinker_dir.exists() and (tinker_dir / "pyproject.toml").exists(): if py_version >= (3, 11): try: __import__("tinker_atropos") check_ok("tinker-atropos", "(RL training backend)") except ImportError: check_warn("tinker-atropos found but not installed", "(run: uv pip install -e ./tinker-atropos)") issues.append("Install tinker-atropos: uv pip install -e ./tinker-atropos") else: check_warn("tinker-atropos requires Python 3.11+", f"(current: {py_version.major}.{py_version.minor})") else: check_warn("tinker-atropos not found", "(run: git submodule update --init --recursive)") # ========================================================================= # Check: Tool Availability # ========================================================================= print() print(color("◆ Tool Availability", Colors.CYAN, Colors.BOLD)) try: # Add project root to path for imports sys.path.insert(0, str(PROJECT_ROOT)) from model_tools import check_tool_availability, TOOLSET_REQUIREMENTS available, unavailable = check_tool_availability() for tid in available: info = TOOLSET_REQUIREMENTS.get(tid, {}) check_ok(info.get("name", tid)) for item in unavailable: if item["missing_vars"]: vars_str = ", ".join(item["missing_vars"]) check_warn(item["name"], f"(missing {vars_str})") else: check_warn(item["name"], "(system dependency not met)") # Count disabled tools with API key requirements api_disabled = [u for u in unavailable if u["missing_vars"]] if api_disabled: issues.append("Run 'hermes setup' to configure missing API keys for full tool access") except Exception as e: check_warn("Could not check tool availability", f"({e})") # ========================================================================= # Summary # ========================================================================= print() if issues: print(color("─" * 60, Colors.YELLOW)) print(color(f" Found {len(issues)} issue(s) to address:", Colors.YELLOW, Colors.BOLD)) print() for i, issue in enumerate(issues, 1): print(f" {i}. {issue}") print() if should_fix: print(color(" Attempting auto-fix is not yet implemented.", Colors.DIM)) print(color(" Please resolve issues manually.", Colors.DIM)) else: print(color("─" * 60, Colors.GREEN)) print(color(" All checks passed! 🎉", Colors.GREEN, Colors.BOLD)) print()