mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-30 06:41:51 +00:00
* remove Vercel AI Gateway provider and Vercel Sandbox terminal backend Both Vercel-hosted integrations are removed end-to-end. Users on the AI Gateway should switch to OpenRouter or one of the other aggregators (Nous Portal, Kilo Code). Users on the Vercel Sandbox backend should switch to Docker, Modal, Daytona, or SSH. What's removed: - `plugins/model-providers/ai-gateway/` provider plugin - `hermes_cli/vercel_auth.py` Vercel-Sandbox auth helper - `tools/environments/vercel_sandbox.py` terminal backend - `ai-gateway` provider wiring across auth, doctor, setup, models, config, status, providers, main, web_server, model_normalize, dump - `vercel_sandbox` backend wiring across terminal_tool, file_tools, code_execution_tool, file_operations, approval, skills_tool, environments/local, credential_files, lazy_deps, prompt_builder, cli, gateway/run - `AI_GATEWAY_BASE_URL` constant, `_AI_GATEWAY_HEADERS` auxiliary-client header set, run_agent base-URL header/reasoning special-cases - `[vercel]` pyproject extra and `vercel`/`vercel-workers` from uv.lock - env vars: `AI_GATEWAY_API_KEY`, `AI_GATEWAY_BASE_URL`, `VERCEL_TOKEN`, `VERCEL_PROJECT_ID`, `VERCEL_TEAM_ID`, `VERCEL_OIDC_TOKEN`, `TERMINAL_VERCEL_RUNTIME` - Tests: deletes test_ai_gateway_models.py and test_vercel_sandbox_environment.py; scrubs references across 23 surviving test files (no entire tests deleted unless they were dedicated to AI Gateway / Sandbox) - Docs: provider tables, env-var reference, setup guides, security notes, tool config, terminal-backend tables — English plus zh-Hans i18n parity - `hermes-agent` skill: provider table entry and remote-backend list What stays (intentional): - `popular-web-designs/templates/vercel.md` — CSS design reference, unrelated to Vercel-the-AI-product - `x-vercel-id` in `stream_diag.py` headers — generic Vercel CDN response header, useful diag signal on any Vercel-hosted endpoint - `vercel-labs/agent-browser` URL in browser config — lightpanda browser project, different OSS effort - `userStories.json` historical contributor entry mentioning Vercel Sandbox — archive, not active docs Validation: - 1153 tests in the 22 targeted files pass (`scripts/run_tests.sh`) - Full repo `py_compile` clean - Live import of every touched module + invariant check (no `ai-gateway` in `PROVIDER_REGISTRY`, no `_AI_GATEWAY_HEADERS`, no `vercel_sandbox` in `_REMOTE_TERMINAL_BACKENDS`) * test: convert profile-count check from change-detector to invariant The hardcoded "== 34" assertion broke when ai-gateway was removed. Per AGENTS.md change-detector-test guidance, assert the relationship (registry count >= number of plugin dirs) instead of a literal count. Counts shift when providers are added/removed; that's expected.
327 lines
10 KiB
Python
327 lines
10 KiB
Python
"""
|
|
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_cli.env_loader import load_hermes_dotenv
|
|
from hermes_constants import display_hermes_home
|
|
from agent.skill_utils import is_excluded_skill_path
|
|
|
|
|
|
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.
|
|
|
|
Thin wrapper over :func:`agent.redact.mask_secret`. Returns ``""`` for
|
|
an empty value (matches the historical behavior of this helper —
|
|
``hermes dump`` formats empty values as blank, not as ``"(not set)"``).
|
|
"""
|
|
from agent.redact import mask_secret
|
|
return mask_secret(value)
|
|
|
|
|
|
def _gateway_status() -> str:
|
|
"""Return a short gateway status string."""
|
|
try:
|
|
from hermes_cli.gateway import get_gateway_runtime_snapshot
|
|
|
|
snapshot = get_gateway_runtime_snapshot()
|
|
if snapshot.running:
|
|
mode = snapshot.manager
|
|
if snapshot.has_process_service_mismatch:
|
|
mode = "manual"
|
|
return f"running ({mode}, pid {snapshot.gateway_pids[0]})"
|
|
if snapshot.service_installed and not snapshot.service_running:
|
|
return f"stopped ({snapshot.manager})"
|
|
return f"stopped ({snapshot.manager})"
|
|
except Exception:
|
|
return "unknown" if sys.platform.startswith(("linux", "darwin")) else "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"):
|
|
if is_excluded_skill_path(item):
|
|
continue
|
|
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"),
|
|
("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
|
|
env_path = get_env_path()
|
|
load_hermes_dotenv(
|
|
hermes_home=env_path.parent,
|
|
project_env=get_project_root() / ".env",
|
|
)
|
|
|
|
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"),
|
|
("GOOGLE_API_KEY", "google/gemini"),
|
|
("GEMINI_API_KEY", "gemini"),
|
|
("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"),
|
|
("NVIDIA_API_KEY", "nvidia"),
|
|
("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)
|