refactor(hermes_cli): doctor — extract _apikey_request/_bits/_link_venv, collapse journal-mode & certifi branches, drop blank-after-import lines

This commit is contained in:
Teknium
2026-09-02 21:15:59 -07:00
parent 2684412ed5
commit d6029d372f
9 changed files with 99 additions and 167 deletions

View File

@@ -81,7 +81,6 @@ def _check_auth_providers(should_fix: bool) -> Finding:
f = Finding()
try:
from hermes_cli.auth import get_nous_auth_status_local, get_codex_auth_status, get_minimax_oauth_auth_status
# Read-only display: refresh-free snapshot — doctor must never trigger an OAuth refresh.
_login_row("Nous Portal auth", get_nous_auth_status_local())
codex_status = get_codex_auth_status()

View File

@@ -68,12 +68,8 @@ def collect_deprecated_env_vars(env_map: dict | None) -> list[tuple[str, str]]:
def collect_relay_plugin_cutover_findings(raw_config: dict | None, env_map: dict | None) -> list[tuple[str, str]]:
"""Return actionable findings for the removed Hermes Relay plugin."""
from hermes_cli.relay_plugin_cutover import (
LEGACY_RELAY_EXPORT_ENV_VARS,
RELAY_PLUGINS_CONFIG_ENV,
configured_legacy_relay_env_vars,
legacy_relay_plugin_keys,
LEGACY_RELAY_EXPORT_ENV_VARS, RELAY_PLUGINS_CONFIG_ENV, configured_legacy_relay_env_vars, legacy_relay_plugin_keys,
)
findings: list[tuple[str, str]] = []
plugins = raw_config.get("plugins") if isinstance(raw_config, dict) else None
if isinstance(plugins, dict):
@@ -135,7 +131,6 @@ def _check_mcp_security(should_fix: bool, f: Finding) -> None:
"""Flag mcp_servers entries with suspicious stdio commands."""
from hermes_cli.config import load_config
from hermes_cli.mcp_security import validate_mcp_server_entry
servers = load_config().get("mcp_servers") or {}
suspicious = 0
if isinstance(servers, dict):
@@ -219,11 +214,10 @@ def _known_provider_ids(cfg: dict) -> tuple[set, list, object, object, object]:
from hermes_cli.config import is_provider_enabled
known.update(str(name).strip().lower() for name, prov_cfg in user_providers.items()
if str(name).strip() and is_provider_enabled(prov_cfg))
if aliases is not None:
for entry in custom_providers:
name = str(entry.get("name") or "").strip() if isinstance(entry, dict) else ""
if name:
known.update(aliases(name, str(entry.get("provider_key") or "").strip()))
for entry in custom_providers if aliases is not None else ():
name = str(entry.get("name") or "").strip() if isinstance(entry, dict) else ""
if name:
known.update(aliases(name, str(entry.get("provider_key") or "").strip()))
return known, custom_providers, resolve_auth, normalize, resolve_full
@@ -240,11 +234,8 @@ def _provider_has_credentials(runtime_provider: str) -> bool:
checks elsewhere, and get_auth_status() returns a bare {logged_in: False} for anything it doesn't dispatch."""
if runtime_provider == "openrouter":
from hermes_cli.config import get_env_value
return bool(str(get_env_value("OPENROUTER_API_KEY") or "").strip()
or str(get_env_value("OPENAI_API_KEY") or "").strip())
return any(str(get_env_value(k) or "").strip() for k in ("OPENROUTER_API_KEY", "OPENAI_API_KEY"))
from hermes_cli.auth import PROVIDER_REGISTRY, get_auth_status
pconfig = PROVIDER_REGISTRY.get(runtime_provider)
if pconfig and getattr(pconfig, "auth_type", "") == "api_key":
status = get_auth_status(runtime_provider) or {}
@@ -264,12 +255,11 @@ def _validate_model_config(config_path, issues: list) -> None:
known_providers, custom_providers, resolve_auth, normalize, resolve_full = _known_provider_ids(cfg)
valid_provider_ids = set(known_providers)
accept = {provider} if provider else set()
if normalize is not None:
for known_provider in known_providers:
try:
valid_provider_ids.add(normalize(known_provider))
except Exception:
continue
for known_provider in known_providers if normalize is not None else ():
try:
valid_provider_ids.add(normalize(known_provider))
except Exception:
continue
runtime_provider = catalog_provider = provider
if provider and provider not in {"auto", "custom"}:
@@ -340,11 +330,10 @@ def _check_config_file(should_fix: bool) -> Finding:
example_config = PROJECT_ROOT / 'cli-config.yaml.example'
if example_config.exists():
shutil.copy2(str(example_config), str(config_path))
check_ok(f"Created {_DHH}/config.yaml from cli-config.yaml.example")
else:
from hermes_cli.config import DEFAULT_CONFIG, save_config
save_config(DEFAULT_CONFIG)
check_ok(f"Created {_DHH}/config.yaml from defaults")
check_ok(f"Created {_DHH}/config.yaml from {'cli-config.yaml.example' if example_config.exists() else 'defaults'}")
f.fixed += 1
else:
check_warn("config.yaml not found", "(using defaults)")
@@ -481,7 +470,6 @@ def _check_config_drift(should_fix: bool) -> Finding:
def _check_xai_retirement(should_fix: bool, f: Finding) -> None:
from hermes_cli.config import load_config
from hermes_cli.xai_retirement import MIGRATION_GUIDE_URL, find_retired_xai_refs, format_issue
retired_refs = find_retired_xai_refs(load_config())
if not retired_refs:
check_ok("No retired xAI models in config")

View File

@@ -189,26 +189,7 @@ def _probe_apikey_provider(pname, env_vars, default_url, base_env, supports_heal
return _row(pname, "ok", "(key configured)", label=label)
try:
import httpx
base = os.getenv(base_env, "") if base_env else ""
# Kimi Code keys (sk-kimi-) → api.kimi.com/coding/v1 (OpenAI-compat surface exposing /models).
if not base and key.startswith("sk-kimi-"):
base = "https://api.kimi.com/coding/v1"
# Anthropic-compat endpoints (/anthropic, api.kimi.com/coding with no /v1) don't support
# /models — rewrite to the OpenAI-compat /v1 surface for health checks.
if base and base.rstrip("/").endswith("/anthropic"):
from agent.auxiliary_client import _to_openai_base_url
base = _to_openai_base_url(base)
if base_url_host_matches(base, "api.kimi.com") and base.rstrip("/").endswith("/coding"):
base = base.rstrip("/") + "/v1"
url = (base.rstrip("/") + "/models") if base else default_url
headers = {"Authorization": f"Bearer {key}", "User-Agent": _HERMES_USER_AGENT}
if base_url_host_matches(base, "api.kimi.com"):
headers["User-Agent"] = "claude-code/0.1.0"
# Google's Generative Language API rejects ``Authorization: Bearer <api-key>`` with 401
# ACCESS_TOKEN_TYPE_UNSUPPORTED (reserved for OAuth 2 tokens); plain keys use ``x-goog-api-key``.
if url and base_url_host_matches(url, "generativelanguage.googleapis.com"):
headers.pop("Authorization", None)
headers["x-goog-api-key"] = key
base, url, headers = _apikey_request(key, base_env, default_url)
r = httpx.get(url, headers=headers, timeout=10)
if pname == "Alibaba/DashScope" and not base and r.status_code == 401:
r = httpx.get("https://dashscope.aliyuncs.com/compatible-mode/v1/models", headers=headers, timeout=10)
@@ -221,6 +202,31 @@ def _probe_apikey_provider(pname, env_vars, default_url, base_env, supports_heal
return _row(pname, "warn", f"(HTTP {r.status_code})", label=label)
def _apikey_request(key: str, base_env, default_url) -> tuple:
"""(effective base, models URL, headers) for a generic Bearer-auth probe, with the per-vendor rewrites."""
base = os.getenv(base_env, "") if base_env else ""
# Kimi Code keys (sk-kimi-) → api.kimi.com/coding/v1 (OpenAI-compat surface exposing /models).
if not base and key.startswith("sk-kimi-"):
base = "https://api.kimi.com/coding/v1"
# Anthropic-compat endpoints (/anthropic, api.kimi.com/coding with no /v1) don't support
# /models — rewrite to the OpenAI-compat /v1 surface for health checks.
if base and base.rstrip("/").endswith("/anthropic"):
from agent.auxiliary_client import _to_openai_base_url
base = _to_openai_base_url(base)
if base_url_host_matches(base, "api.kimi.com") and base.rstrip("/").endswith("/coding"):
base = base.rstrip("/") + "/v1"
url = (base.rstrip("/") + "/models") if base else default_url
headers = {"Authorization": f"Bearer {key}", "User-Agent": _HERMES_USER_AGENT}
if base_url_host_matches(base, "api.kimi.com"):
headers["User-Agent"] = "claude-code/0.1.0"
# Google's Generative Language API rejects ``Authorization: Bearer <api-key>`` with 401
# ACCESS_TOKEN_TYPE_UNSUPPORTED (reserved for OAuth 2 tokens); plain keys use ``x-goog-api-key``.
if url and base_url_host_matches(url, "generativelanguage.googleapis.com"):
headers.pop("Authorization", None)
headers["x-goog-api-key"] = key
return base, url, headers
def _probe_bedrock() -> ProbeResult:
name = "AWS Bedrock"
try:
@@ -263,8 +269,7 @@ def _probe_azure_entra() -> ProbeResult:
model_cfg = cfg.get("model") if isinstance(cfg, dict) else {}
if not isinstance(model_cfg, dict):
return _skip(name)
cfg_provider = str(model_cfg.get("provider") or "").strip().lower()
auth_mode = str(model_cfg.get("auth_mode") or "").strip().lower()
cfg_provider, auth_mode = (str(model_cfg.get(k) or "").strip().lower() for k in ("provider", "auth_mode"))
if cfg_provider != "azure-foundry" or auth_mode != "entra_id":
return _skip(name)
except Exception:
@@ -278,9 +283,7 @@ def _probe_azure_entra() -> ProbeResult:
return _row(name, "warn", f"(adapter import failed: {exc})", [f"Azure Foundry adapter import failed: {exc}"], label=label)
if not has_azure_identity_installed():
return _row(name, "warn", "(azure-identity not installed)",
[f"Install azure-identity: {sys.executable} -m pip install azure-identity"], label=label)
return _row(name, "warn", "(azure-identity not installed)", [f"Install azure-identity: {sys.executable} -m pip install azure-identity"], label=label)
entra_cfg = model_cfg.get("entra") or {}
scope = (str(entra_cfg.get("scope") or "").strip() if isinstance(entra_cfg, dict) else "") or SCOPE_AI_AZURE_DEFAULT
info = describe_active_credential(config=EntraIdentityConfig(scope=scope), timeout_seconds=10.0)

View File

@@ -42,7 +42,6 @@ class ProbeResult:
def _load_config() -> dict:
try:
from hermes_cli.config import load_config
return load_config() or {}
except Exception:
return {}
@@ -51,19 +50,16 @@ def _load_config() -> dict:
def _http_get(url: str, headers: Optional[dict] = None, timeout: Optional[float] = None):
"""Single HTTP GET seam for all metadata probes."""
import httpx
return httpx.get(url, headers=headers or {}, timeout=timeout)
def _browser_available() -> bool:
"""Is the local browser automation backend (agent-browser) installed?"""
import shutil
if shutil.which("agent-browser"):
return True
try:
from hermes_cli.doctor import HERMES_HOME, PROJECT_ROOT
if (PROJECT_ROOT / "node_modules" / "agent-browser").exists():
return True
for candidate in (HERMES_HOME / "node" / "bin", HERMES_HOME / "node", HERMES_HOME / "node_modules" / ".bin"):
@@ -75,11 +71,7 @@ def _browser_available() -> bool:
# probes above. Mirror the rung hermes_cli.doctor uses so this probe can't diverge from it, including
# the Termux carve-out (bare npx is too fragile to advertise as ready there).
try:
from tools.browser_tool import (
_find_agent_browser,
_is_npx_agent_browser_sentinel,
_requires_real_termux_browser_install,
)
from tools.browser_tool import _find_agent_browser, _is_npx_agent_browser_sentinel, _requires_real_termux_browser_install
browser_cmd = _find_agent_browser(validate=False)
except Exception:
return False
@@ -93,7 +85,6 @@ def _launch_browser_probe(timeout: float) -> tuple:
from playwright.sync_api import sync_playwright
except ImportError:
return (False, "playwright not installed")
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, timeout=timeout * 1000)
try:
@@ -106,7 +97,6 @@ def _launch_browser_probe(timeout: float) -> tuple:
def _probe_mcp_server(name: str, config: dict, timeout: float):
"""initialize + tools/list against one configured MCP server."""
from hermes_cli.mcp_config import _probe_single_server
return _probe_single_server(name, config, connect_timeout=timeout)

View File

@@ -32,14 +32,11 @@ def _sqlite_upgrade_hint(install_method: str | None = None) -> str:
"""Return an actionable SQLite upgrade hint for this install layout."""
from hermes_cli.doctor import PROJECT_ROOT, detect_install_method
method = install_method or detect_install_method(PROJECT_ROOT)
if method == "docker":
action = f"run `{recommended_update_command_for_method(method)}`, then recreate all Hermes containers"
elif is_nix_install_method(method):
action = recommended_update_command_for_method(method) # prose guidance, not a shell command
elif method == "apt":
action = f"run `{recommended_update_command_for_method(method)}`"
cmd = recommended_update_command_for_method(method)
if is_nix_install_method(method):
action = cmd # prose guidance, not a shell command
else:
action = "run `hermes update`"
action = {"docker": f"run `{cmd}`, then recreate all Hermes containers", "apt": f"run `{cmd}`"}.get(method, "run `hermes update`")
return f"({action}; fixed versions: 3.51.3+ / 3.50.7 / 3.44.6 — see https://sqlite.org/wal.html#walresetbug)"
@@ -116,19 +113,17 @@ def _report_database_journal_modes(hermes_home: Path | None = None, version_info
continue
mode, error = _read_journal_mode(path)
size = _format_db_size(path)
if error is not None:
if vulnerable:
check_warn(f"{name}: journal mode could not be read", f"({error}; cannot rule out WAL exposure)")
else:
check_info(f"{name}: journal mode could not be read ({error})")
if error is not None and vulnerable:
check_warn(f"{name}: journal mode could not be read", f"({error}; cannot rule out WAL exposure)")
elif error is not None:
check_info(f"{name}: journal mode could not be read ({error})")
elif mode == "wal" and vulnerable:
exposed.append(name)
check_warn(f"{name} is in WAL mode ({size})", "(exposed to the WAL-reset bug until SQLite is upgraded)")
elif mode == "wal":
if vulnerable:
exposed.append(name)
check_warn(f"{name} is in WAL mode ({size})", "(exposed to the WAL-reset bug until SQLite is upgraded)")
else:
check_info(f"{name}: WAL journal mode ({size})")
check_info(f"{name}: WAL journal mode ({size})")
else:
check_info(f"{name}: rollback journal mode ({size}, not exposed)" if vulnerable else f"{name}: rollback journal mode ({size})")
check_info(f"{name}: rollback journal mode ({size}{', not exposed' if vulnerable else ''})")
if exposed:
check_info(f"To clear the exposure: {_wal_reset_repair_hint()}")
@@ -188,8 +183,7 @@ def _check_s6_supervision(issues: list[str]) -> None:
profiles = mgr.list_profile_gateways()
if not profiles:
check_info("No per-profile gateways registered yet — create one with `hermes profile create <name>`")
return
return check_info("No per-profile gateways registered yet — create one with `hermes profile create <name>`")
up_count = sum(1 for p in profiles if mgr.is_running(f"gateway-{p}"))
check_ok(f"Per-profile gateways: {up_count}/{len(profiles)} supervised up"
+ (f" ({', '.join(sorted(profiles))})" if len(profiles) <= 8 else ""))
@@ -207,11 +201,8 @@ def check_certificates(should_fix: bool = False, issues: "list | None" = None) -
except Exception as e:
check_warn("SSL certificate check skipped", str(e))
return
def add_issue(msg: str) -> None:
if issues is not None:
issues.append(msg)
if issues is None:
issues = []
try:
verify_ca_bundle_with_fallback()
check_ok("SSL CA certificate bundle is valid")
@@ -225,20 +216,19 @@ def check_certificates(should_fix: bool = False, issues: "list | None" = None) -
check_fail("SSL CA certificate bundle is broken", first_error)
pip_cmd = f"{sys.executable} -m pip install --force-reinstall certifi"
if not should_fix:
add_issue(f"Repair the CA bundle: run `hermes doctor --fix`, or `{pip_cmd}`")
issues.append(f"Repair the CA bundle: run `hermes doctor --fix`, or `{pip_cmd}`")
return
print(" → Repairing: force-reinstalling certifi...")
try:
result = subprocess.run([sys.executable, "-m", "pip", "install", "--force-reinstall", "certifi"],
capture_output=True, text=True, timeout=300)
failure = ("certifi reinstall failed", (result.stderr or result.stdout or "")[-500:]) if result.returncode != 0 else None
except Exception as exc:
check_fail("certifi repair could not run pip", str(exc))
add_issue(f"Reinstall certifi manually: {pip_cmd}")
return
if result.returncode != 0:
check_fail("certifi reinstall failed", (result.stderr or result.stdout or "")[-500:])
add_issue(f"Reinstall certifi manually: {pip_cmd}")
failure = ("certifi repair could not run pip", str(exc))
if failure:
check_fail(*failure)
issues.append(f"Reinstall certifi manually: {pip_cmd}")
return
# Drop cached certifi modules so where() resolves the fresh install without a restart.
@@ -252,11 +242,8 @@ def check_certificates(should_fix: bool = False, issues: "list | None" = None) -
check_ok("SSL CA certificate bundle repaired (certifi reinstalled)")
except SSLConfigurationError as e:
check_fail("SSL CA certificate bundle still broken after reinstall", str(e))
add_issue(
"certifi reinstall did not restore the CA bundle — check for a "
"custom CA env var (SSL_CERT_FILE/REQUESTS_CA_BUNDLE) pointing "
"at a missing file, or recreate the venv."
)
issues.append("certifi reinstall did not restore the CA bundle — check for a custom CA env var "
"(SSL_CERT_FILE/REQUESTS_CA_BUNDLE) pointing at a missing file, or recreate the venv.")
def _check_gateway_service_linger(issues: list[str]) -> None:
@@ -291,9 +278,7 @@ def check_macos_tcc_grants() -> None:
Disk Access, so the DR string is the only readable signal (a proxy for the signing class, not DR wording).
"""
from hermes_cli.doctor import _desktop_app_bundle, _macos_desktop_dr
if sys.platform != "darwin":
return
app = _desktop_app_bundle()
app = _desktop_app_bundle() if sys.platform == "darwin" else None
if app is None:
return
dr = _macos_desktop_dr(app)
@@ -335,13 +320,11 @@ def _desktop_app_bundle() -> Path | None:
def _macos_desktop_dr(app: Path) -> str | None:
"""Return the bundle's designated requirement string, or None on failure (a hanging codesign must never abort doctor)."""
codesign = shutil.which("codesign")
if not codesign:
return None
try:
proc = subprocess.run([codesign, "-d", "--requirements", "-", str(app)], capture_output=True, text=True, timeout=15)
proc = subprocess.run([codesign, "-d", "--requirements", "-", str(app)], capture_output=True, text=True, timeout=15) if codesign else None
except (FileNotFoundError, subprocess.TimeoutExpired):
return None
return None if proc.returncode != 0 else (proc.stdout or "") + (proc.stderr or "")
return None if proc is None or proc.returncode != 0 else (proc.stdout or "") + (proc.stderr or "")
def check_macos_tcc_anchor(should_fix: bool = False) -> None:
@@ -354,13 +337,10 @@ def check_macos_tcc_anchor(should_fix: bool = False) -> None:
if status == "skip":
return
if status == "active":
check_ok("macOS TCC anchor active", f"({detail})")
return
if should_fix:
anchored = tcc.ensure_tcc_anchor()
if anchored is not None:
check_ok("macOS TCC anchor installed", f"({anchored})")
return
return check_ok("macOS TCC anchor active", f"({detail})")
anchored = tcc.ensure_tcc_anchor() if should_fix else None
if anchored is not None:
return check_ok("macOS TCC anchor installed", f"({anchored})")
check_warn("macOS TCC anchor missing" if status == "missing" else "macOS TCC anchor stale", f"({detail})")
except Exception as e:
check_warn("macOS TCC anchor check failed", f"({e})")
@@ -377,7 +357,9 @@ def check_macos_full_disk_access() -> None:
tcc_dir = Path.home() / "Library" / "Application Support" / "com.apple.TCC"
try:
os.listdir(tcc_dir)
except PermissionError:
except OSError as e:
if not isinstance(e, PermissionError):
return
check_info(
"One switch silences all macOS folder prompts: grant your terminal "
"app Full Disk Access and Hermes will never trip per-folder dialogs "
@@ -389,8 +371,6 @@ def check_macos_full_disk_access() -> None:
"and restart them once. With Hermes' stable signing identities the "
"grant survives every update."
)
except OSError:
return
else:
check_ok("macOS Full Disk Access granted", "(no per-folder permission prompts will occur)")

View File

@@ -15,7 +15,6 @@ def _mark(glyph: str, col: str):
check_ok, check_warn, check_fail = _mark("✓", Colors.GREEN), _mark("⚠", Colors.YELLOW), _mark("✗", Colors.RED)
CHECK_ROW = {"ok": check_ok, "warn": check_warn, "fail": check_fail}
def check_info(text: str):
@@ -24,13 +23,9 @@ def check_info(text: str):
def check_bool(cond, ok, bad, *, fail: bool = False):
"""``check_ok(*ok)`` when *cond* else ``check_warn(*bad)`` (``check_fail`` with fail=True); returns bool(cond).
*ok* / *bad* are a text string or a ``(text, detail)`` tuple.
"""
*ok* / *bad* are a text string or a ``(text, detail)`` tuple."""
args = ok if cond else bad
if isinstance(args, str):
args = (args,)
(check_ok if cond else (check_fail if fail else check_warn))(*args)
(check_ok if cond else (check_fail if fail else check_warn))(*((args,) if isinstance(args, str) else args))
return bool(cond)
@@ -62,11 +57,8 @@ class Finding:
def doctor_check(on_error: str | None = None, detail: str = ""):
"""Turn ``fn(should_fix, f: Finding)`` into a ``(should_fix) -> Finding`` doctor check.
Creates the Finding, and if *fn* raises prints ``check_warn(on_error.format(e=e), detail.format(e=e))``
(nothing when *on_error* is None) — the partial Finding is still returned, so issues recorded
before the crash survive.
"""
If *fn* raises, prints ``check_warn(on_error.format(e=e), detail.format(e=e))`` (nothing when *on_error*
is None); the partial Finding is still returned, so issues recorded before the crash survive."""
def deco(fn):
@functools.wraps(fn)
def check(should_fix: bool) -> Finding:

View File

@@ -18,7 +18,6 @@ def _honcho_is_configured_for_doctor() -> bool:
"""Return True when Honcho is configured, even if this process has no active session."""
try:
from plugins.memory.honcho.client import HonchoClientConfig
cfg = HonchoClientConfig.from_global_config()
return bool(cfg.enabled and (cfg.api_key or cfg.base_url))
except Exception:
@@ -30,14 +29,12 @@ def _doctor_memory_config(hermes_home: Path | None = None) -> dict:
from hermes_cli.doctor import HERMES_HOME
try:
from hermes_cli.config import _expand_env_vars, read_user_config_raw
config_path = (hermes_home if hermes_home is not None else HERMES_HOME) / "config.yaml"
if not config_path.exists():
return {}
config = _expand_env_vars(read_user_config_raw(config_path))
try:
from hermes_cli import managed_scope
config = managed_scope.apply_managed_overlay(config)
except Exception:
pass
@@ -52,6 +49,11 @@ def _doctor_memory_config(hermes_home: Path | None = None) -> dict:
STATE_DB_SIZE_WARN_BYTES = 1 * 1024 * 1024 * 1024 # 1 GiB logical size
def _bits(*pairs) -> list:
"""``[fmt() for value, fmt in pairs if value is not None]`` — present-only stat fragments."""
return [fmt() for value, fmt in pairs if value is not None]
def _render_state_db_stats(stats: dict, holders=None) -> list:
"""Turn a collect_state_db_stats() dict into ``(kind, text, detail)`` rows, kind 'info' / 'warn'.
@@ -64,27 +66,20 @@ def _render_state_db_stats(stats: dict, holders=None) -> list:
wal = stats.get("wal_size_bytes")
freelist = stats.get("freelist_count")
size_bits = []
if logical is not None:
size_bits.append(f"logical size {_human_bytes(logical)}")
if stats.get("page_count") is not None:
size_bits.append(f"{stats['page_count']:,} pages")
if freelist is not None:
size_bits.append(f"{freelist:,} free")
if wal is not None:
size_bits.append(f"WAL {_human_bytes(wal)}")
size_bits = _bits(
(logical, lambda: f"logical size {_human_bytes(logical)}"),
(stats.get("page_count"), lambda: f"{stats['page_count']:,} pages"),
(freelist, lambda: f"{freelist:,} free"),
(wal, lambda: f"WAL {_human_bytes(wal)}"),
)
if size_bits:
lines.append(("info", "state.db " + ", ".join(size_bits), ""))
row_bits = []
if stats.get("messages") is not None:
row_bits.append(f"{stats['messages']:,} messages")
if stats.get("sessions") is not None:
row_bits.append(f"{stats['sessions']:,} sessions")
if stats.get("journal_mode"):
row_bits.append(f"journal_mode={stats['journal_mode']}")
if holders is not None:
row_bits.append(f"{holders} process(es) holding the DB open")
row_bits = _bits(
(stats.get("messages"), lambda: f"{stats['messages']:,} messages"),
(stats.get("sessions"), lambda: f"{stats['sessions']:,} sessions"),
(stats.get("journal_mode") or None, lambda: f"journal_mode={stats['journal_mode']}"),
(holders, lambda: f"{holders} process(es) holding the DB open"),
)
if row_bits:
lines.append(("info", ", ".join(row_bits), ""))
@@ -120,7 +115,6 @@ def _render_state_db_stats(stats: dict, holders=None) -> list:
def _memory_store_flags(hermes_home: Path) -> tuple:
from tools.memory_tool import get_builtin_memory_store_flags
return get_builtin_memory_store_flags({"memory": _doctor_memory_config(hermes_home)})
@@ -226,7 +220,6 @@ def _state_db_health(f: Finding, should_fix: bool, state_db_path: Path, _DHH: st
# `SELECT COUNT(*)` succeeds even when the FTS index is corrupt and every message write
# fails through the triggers; _db_opens_cleanly drives a rolled-back write to surface that.
from hermes_state import _db_opens_cleanly
_write_reason = _db_opens_cleanly(state_db_path)
if _write_reason is not None:
check_warn(f"{_DHH}/state.db fails a write-health probe (FTS index may be corrupt)", f"({_write_reason})")
@@ -241,7 +234,6 @@ def _state_db_health(f: Finding, should_fix: bool, state_db_path: Path, _DHH: st
)
except Exception as e:
from hermes_state import is_malformed_db_error
if not is_malformed_db_error(e):
check_warn(f"{_DHH}/state.db exists but has issues: {e}")
return
@@ -264,7 +256,6 @@ def _state_db_stats(issues: list, state_db_path: Path) -> None:
the gateway; any failure degrades to one info line rather than failing doctor."""
try:
from hermes_state import collect_state_db_stats, count_db_holders
rows = _render_state_db_stats(collect_state_db_stats(state_db_path), holders=count_db_holders(state_db_path))
for _kind, _text, _detail in rows:
if _kind != "warn":
@@ -351,7 +342,6 @@ def _check_skills_hub(should_fix: bool) -> Finding:
check_warn(f"{q_count} skill(s) in quarantine", "(pending review)")
from hermes_cli.config import get_env_value
if get_env_value("GITHUB_TOKEN") or get_env_value("GH_TOKEN"):
check_ok("GitHub token configured (authenticated API access)")
else:
@@ -440,7 +430,6 @@ def _check_memory_provider(should_fix: bool) -> Finding:
def _check_profiles(should_fix: bool, f: Finding) -> None:
from hermes_cli.profiles import list_profiles, _get_wrapper_dir, profile_exists
import re as _re
named_profiles = [p for p in list_profiles() if not p.is_default]
if not named_profiles:
return

View File

@@ -11,7 +11,7 @@ import shutil
import subprocess
import sys
from hermes_cli.doctor_platform import _system_package_install_cmd
from hermes_cli.doctor_report import CHECK_ROW, Finding, _fail_and_issue, check_bool, check_info, check_ok, check_warn, doctor_check
from hermes_cli.doctor_report import Finding, _fail_and_issue, check_bool, check_info, check_ok, check_warn, doctor_check
from hermes_cli.vercel_auth import describe_vercel_auth
from hermes_constants import agent_browser_runnable, is_termux as _is_termux
@@ -71,12 +71,10 @@ def _doctor_web_capability_rows() -> list[tuple[str, str, str]]:
try:
from agent.web_search_registry import get_active_extract_provider, get_active_search_provider
from tools.web_tools import _ensure_web_plugins_loaded, _provider_is_ready
# Fresh process: bundled web providers only register during plugin discovery (idempotent, cheap).
_ensure_web_plugins_loaded()
except Exception:
return rows
for capability, getter in (("web search", get_active_search_provider), ("web extract", get_active_extract_provider)):
try:
provider = getter()
@@ -94,8 +92,7 @@ def _doctor_web_capability_rows() -> list[tuple[str, str, str]]:
def _apply_doctor_tool_availability_overrides(available: list[str], unavailable: list[dict]) -> tuple[list[str], list[dict]]:
"""Adjust runtime-gated tool availability for doctor diagnostics."""
from hermes_cli.doctor import _honcho_is_configured_for_doctor
updated_available = list(available)
updated_unavailable = []
updated_available, updated_unavailable = list(available), []
for item in unavailable:
if _is_kanban_worker_env_gate(item):
gated = "kanban"
@@ -220,10 +217,8 @@ def _check_vercel_backend(issues: list[str]) -> None:
def _check_plugin_backend(terminal_env: str, issues: list[str]) -> None:
try:
from hermes_cli.plugins import discover_plugins
discover_plugins()
from agent.terminal_env_registry import get_provider
provider = get_provider(terminal_env)
except Exception:
provider = None
@@ -247,7 +242,6 @@ def _check_terminal_backend(should_fix: bool) -> Finding:
running_in_container = _is_container()
except Exception:
running_in_container = False
# Inside our container docker-in-docker isn't set up, so the local backend is the intended one: skip the
# noisy "docker not found" warning. An explicit TERMINAL_ENV=docker (mounted docker.sock) still gets checked.
if running_in_container and terminal_env != "docker":
@@ -460,7 +454,7 @@ def _check_tool_availability(should_fix: bool, f: Finding) -> None:
for tid in available:
check_ok(TOOLSET_REQUIREMENTS.get(tid, {}).get("name", tid), _doctor_tool_availability_detail(tid))
for status, label, detail in web_rows:
CHECK_ROW[status](label, detail)
(check_ok if status == "ok" else check_warn)(label, detail)
for item in unavailable:
env_vars = item.get("missing_vars") or item.get("env_vars") or []
check_warn(item["name"], f"(missing {', '.join(env_vars)})" if env_vars else "(system dependency not met)")

View File

@@ -76,7 +76,6 @@ 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 = "manual" if snapshot.has_process_service_mismatch else snapshot.manager
@@ -142,7 +141,6 @@ _INTERESTING_PATHS = (
def _config_overrides(config: dict) -> dict[str, str]:
"""Find non-default config values worth reporting."""
from hermes_cli.config import DEFAULT_CONFIG
overrides = {}
for section, key in _INTERESTING_PATHS:
default_section = DEFAULT_CONFIG.get(section, {})
@@ -214,7 +212,6 @@ def _api_key_lines(show_keys: bool) -> list[str]:
if not val and label == "openrouter":
try:
from agent.credential_pool import load_pool as _load_pool
if _load_pool("openrouter").has_credentials():
display = "set (auth pool)"
except Exception: