""" Dump command for hermes CLI. Outputs a compact, plain-text summary of the user's Hermes setup that can be copy-pasted into Discord/GitHub/Telegram for support context. No ANSI colors, no checkmarks — just data. """ import json import os import platform import subprocess import sys from pathlib import Path from hermes_cli.config import get_hermes_home, get_env_path, get_project_root, load_config from hermes_constants import display_hermes_home def _get_git_commit(project_root: Path) -> str: """Return short git commit hash, or '(unknown)'.""" try: result = subprocess.run( ["git", "rev-parse", "--short=8", "HEAD"], capture_output=True, text=True, timeout=5, cwd=str(project_root), ) if result.returncode == 0: return result.stdout.strip() except Exception: pass return "(unknown)" def _redact(value: str) -> str: """Redact all but first 4 and last 4 chars.""" if not value: return "" if len(value) < 12: return "***" return value[:4] + "..." + value[-4:] def _gateway_status() -> str: """Return a short gateway status string.""" if sys.platform.startswith("linux"): from hermes_constants import is_container if is_container(): try: from hermes_cli.gateway import find_gateway_pids pids = find_gateway_pids() if pids: return f"running (docker, pid {pids[0]})" return "stopped (docker)" except Exception: return "stopped (docker)" try: from hermes_cli.gateway import get_service_name svc = get_service_name() except Exception: svc = "hermes-gateway" try: r = subprocess.run( ["systemctl", "--user", "is-active", svc], capture_output=True, text=True, timeout=5, ) return "running (systemd)" if r.stdout.strip() == "active" else "stopped" except Exception: return "unknown" elif sys.platform == "darwin": try: from hermes_cli.gateway import get_launchd_label r = subprocess.run( ["launchctl", "list", get_launchd_label()], capture_output=True, text=True, timeout=5, ) return "loaded (launchd)" if r.returncode == 0 else "not loaded" except Exception: return "unknown" return "N/A" def _count_skills(hermes_home: Path) -> int: """Count installed skills.""" skills_dir = hermes_home / "skills" if not skills_dir.is_dir(): return 0 count = 0 for item in skills_dir.rglob("SKILL.md"): count += 1 return count def _count_mcp_servers(config: dict) -> int: """Count configured MCP servers.""" mcp = config.get("mcp", {}) servers = mcp.get("servers", {}) return len(servers) def _cron_summary(hermes_home: Path) -> str: """Return cron jobs summary.""" jobs_file = hermes_home / "cron" / "jobs.json" if not jobs_file.exists(): return "0" try: with open(jobs_file, encoding="utf-8") as f: data = json.load(f) jobs = data.get("jobs", []) active = sum(1 for j in jobs if j.get("enabled", True)) return f"{active} active / {len(jobs)} total" except Exception: return "(error reading)" def _configured_platforms() -> list[str]: """Return list of configured messaging platform names.""" checks = { "telegram": "TELEGRAM_BOT_TOKEN", "discord": "DISCORD_BOT_TOKEN", "slack": "SLACK_BOT_TOKEN", "whatsapp": "WHATSAPP_ENABLED", "signal": "SIGNAL_HTTP_URL", "email": "EMAIL_ADDRESS", "sms": "TWILIO_ACCOUNT_SID", "matrix": "MATRIX_HOMESERVER_URL", "mattermost": "MATTERMOST_URL", "homeassistant": "HASS_TOKEN", "dingtalk": "DINGTALK_CLIENT_ID", "feishu": "FEISHU_APP_ID", "wecom": "WECOM_BOT_ID", "wecom_callback": "WECOM_CALLBACK_CORP_ID", "weixin": "WEIXIN_ACCOUNT_ID", "qqbot": "QQ_APP_ID", } return [name for name, env in checks.items() if os.getenv(env)] def _memory_provider(config: dict) -> str: """Return the active memory provider name.""" mem = config.get("memory", {}) provider = mem.get("provider", "") return provider if provider else "built-in" def _get_model_and_provider(config: dict) -> tuple[str, str]: """Extract model and provider from config.""" model_cfg = config.get("model", "") if isinstance(model_cfg, dict): model = model_cfg.get("default") or model_cfg.get("model") or model_cfg.get("name") or "(not set)" provider = model_cfg.get("provider") or "(auto)" elif isinstance(model_cfg, str): model = model_cfg or "(not set)" provider = "(auto)" else: model = "(not set)" provider = "(auto)" return model, provider def _config_overrides(config: dict) -> dict[str, str]: """Find non-default config values worth reporting. Returns a flat dict of dotpath -> value for interesting overrides. """ from hermes_cli.config import DEFAULT_CONFIG overrides = {} # Sections with interesting user-facing overrides interesting_paths = [ ("agent", "max_turns"), ("agent", "gateway_timeout"), ("agent", "tool_use_enforcement"), ("terminal", "backend"), ("terminal", "docker_image"), ("terminal", "persistent_shell"), ("browser", "allow_private_urls"), ("compression", "enabled"), ("compression", "threshold"), ("display", "streaming"), ("display", "skin"), ("display", "show_reasoning"), ("smart_model_routing", "enabled"), ("privacy", "redact_pii"), ("tts", "provider"), ] for section, key in interesting_paths: default_section = DEFAULT_CONFIG.get(section, {}) user_section = config.get(section, {}) if not isinstance(default_section, dict) or not isinstance(user_section, dict): continue default_val = default_section.get(key) user_val = user_section.get(key) if user_val is not None and user_val != default_val: overrides[f"{section}.{key}"] = str(user_val) # Toolsets (if different from default) default_toolsets = DEFAULT_CONFIG.get("toolsets", []) user_toolsets = config.get("toolsets", []) if user_toolsets != default_toolsets: overrides["toolsets"] = str(user_toolsets) # Fallback providers fallbacks = config.get("fallback_providers", []) if fallbacks: overrides["fallback_providers"] = str(fallbacks) return overrides def run_dump(args): """Output a compact, copy-pasteable setup summary.""" show_keys = getattr(args, "show_keys", False) # Load env from .env file so key checks work from dotenv import load_dotenv env_path = get_env_path() if env_path.exists(): try: load_dotenv(env_path, encoding="utf-8") except UnicodeDecodeError: load_dotenv(env_path, encoding="latin-1") # Also try project .env as dev fallback load_dotenv(get_project_root() / ".env", override=False, encoding="utf-8") project_root = get_project_root() hermes_home = get_hermes_home() try: from hermes_cli import __version__, __release_date__ except ImportError: __version__ = "(unknown)" __release_date__ = "" commit = _get_git_commit(project_root) try: config = load_config() except Exception: config = {} model, provider = _get_model_and_provider(config) # Profile try: from hermes_cli.profiles import get_active_profile_name profile = get_active_profile_name() or "(default)" except Exception: profile = "(default)" # Terminal backend terminal_cfg = config.get("terminal", {}) backend = terminal_cfg.get("backend", "local") # OpenAI SDK version try: import openai openai_ver = openai.__version__ except ImportError: openai_ver = "not installed" # OS info os_info = f"{platform.system()} {platform.release()} {platform.machine()}" lines = [] lines.append("--- hermes dump ---") ver_str = f"{__version__}" if __release_date__: ver_str += f" ({__release_date__})" ver_str += f" [{commit}]" lines.append(f"version: {ver_str}") lines.append(f"os: {os_info}") lines.append(f"python: {sys.version.split()[0]}") lines.append(f"openai_sdk: {openai_ver}") lines.append(f"profile: {profile}") lines.append(f"hermes_home: {display_hermes_home()}") lines.append(f"model: {model}") lines.append(f"provider: {provider}") lines.append(f"terminal: {backend}") # API keys lines.append("") lines.append("api_keys:") api_keys = [ ("OPENROUTER_API_KEY", "openrouter"), ("OPENAI_API_KEY", "openai"), ("ANTHROPIC_API_KEY", "anthropic"), ("ANTHROPIC_TOKEN", "anthropic_token"), ("NOUS_API_KEY", "nous"), ("GLM_API_KEY", "glm/zai"), ("ZAI_API_KEY", "zai"), ("KIMI_API_KEY", "kimi"), ("MINIMAX_API_KEY", "minimax"), ("DEEPSEEK_API_KEY", "deepseek"), ("DASHSCOPE_API_KEY", "dashscope"), ("HF_TOKEN", "huggingface"), ("AI_GATEWAY_API_KEY", "ai_gateway"), ("OPENCODE_ZEN_API_KEY", "opencode_zen"), ("OPENCODE_GO_API_KEY", "opencode_go"), ("KILOCODE_API_KEY", "kilocode"), ("FIRECRAWL_API_KEY", "firecrawl"), ("TAVILY_API_KEY", "tavily"), ("BROWSERBASE_API_KEY", "browserbase"), ("FAL_KEY", "fal"), ("ELEVENLABS_API_KEY", "elevenlabs"), ("GITHUB_TOKEN", "github"), ] for env_var, label in api_keys: val = os.getenv(env_var, "") if show_keys and val: display = _redact(val) else: display = "set" if val else "not set" lines.append(f" {label:<20} {display}") # Features summary lines.append("") lines.append("features:") toolsets = config.get("toolsets", ["hermes-cli"]) lines.append(f" toolsets: {', '.join(toolsets) if toolsets else '(default)'}") lines.append(f" mcp_servers: {_count_mcp_servers(config)}") lines.append(f" memory_provider: {_memory_provider(config)}") lines.append(f" gateway: {_gateway_status()}") platforms = _configured_platforms() lines.append(f" platforms: {', '.join(platforms) if platforms else 'none'}") lines.append(f" cron_jobs: {_cron_summary(hermes_home)}") lines.append(f" skills: {_count_skills(hermes_home)}") # Config overrides (non-default values) overrides = _config_overrides(config) if overrides: lines.append("") lines.append("config_overrides:") for key, val in overrides.items(): lines.append(f" {key}: {val}") lines.append("--- end dump ---") output = "\n".join(lines) print(output)