diff --git a/hermes_cli/setup_tts.py b/hermes_cli/setup_tts.py index 363bd7c57f..6c2d013209 100644 --- a/hermes_cli/setup_tts.py +++ b/hermes_cli/setup_tts.py @@ -88,9 +88,8 @@ def _xai_oauth_logged_in_for_setup() -> bool: def _run_xai_oauth_login_from_setup() -> bool: """Run the xAI Grok OAuth device-code login from inside the setup wizard. - Saves OAuth tokens only — does **not** switch the active provider or rewrite - ``model.provider`` (callers only need credentials for side tools). Returns True on success, - False on any failure (the caller falls back, e.g. to Edge TTS). + Saves OAuth tokens only — does **not** switch the active provider or rewrite ``model.provider`` + (callers only need credentials for side tools). False on any failure (caller falls back). """ from hermes_cli.setup import _info, print_warning try: diff --git a/hermes_cli/setup_whatsapp_cloud.py b/hermes_cli/setup_whatsapp_cloud.py index 56e7ec0c37..e566303752 100644 --- a/hermes_cli/setup_whatsapp_cloud.py +++ b/hermes_cli/setup_whatsapp_cloud.py @@ -312,24 +312,21 @@ def run_whatsapp_cloud_setup() -> int: print() _header("STEP 5 — Verify Token (auto-generated)") - current_verify = get_env_value("WHATSAPP_CLOUD_VERIFY_TOKEN") or None - if current_verify: - print(f" An existing verify token is already set ({current_verify[:8]}...).") + verify_token = get_env_value("WHATSAPP_CLOUD_VERIFY_TOKEN") or None + regen = "y" + if verify_token: + print(f" An existing verify token is already set ({verify_token[:8]}...).") try: regen = input(" Generate a new one? [y/N]: ").strip().lower() except (EOFError, KeyboardInterrupt): regen = "n" - if regen in {"y", "yes"}: - verify_token = secrets.token_urlsafe(32) - save_env_value("WHATSAPP_CLOUD_VERIFY_TOKEN", verify_token) - print(f" ✓ New verify token: {verify_token}") - else: - verify_token = current_verify - print(" ✓ Keeping existing verify token") - else: + if regen in {"y", "yes"}: + label = "New verify token" if verify_token else "Generated" verify_token = secrets.token_urlsafe(32) save_env_value("WHATSAPP_CLOUD_VERIFY_TOKEN", verify_token) - print(f" ✓ Generated: {verify_token}") + print(f" ✓ {label}: {verify_token}") + else: + print(" ✓ Keeping existing verify token") _lines("", " → COPY THIS TOKEN NOW. You'll paste it into Meta's webhook", " configuration dialog (next step).", "") diff --git a/hermes_cli/sqlite_runtime.py b/hermes_cli/sqlite_runtime.py index edaf986c20..d46ef77fb3 100644 --- a/hermes_cli/sqlite_runtime.py +++ b/hermes_cli/sqlite_runtime.py @@ -47,21 +47,15 @@ class SQLiteRuntimeInfo: _PROBE_SCRIPT = """ -import json -import sqlite3 -import sys - +import json, sqlite3, sys conn = sqlite3.connect(":memory:") try: row = conn.execute("SELECT sqlite_source_id()").fetchone() finally: conn.close() - print(json.dumps({ - "base_prefix": sys.base_prefix, - "executable": sys.executable, - "python_version": list(sys.version_info[:3]), - "sqlite_version": list(sqlite3.sqlite_version_info), + "base_prefix": sys.base_prefix, "executable": sys.executable, + "python_version": list(sys.version_info[:3]), "sqlite_version": list(sqlite3.sqlite_version_info), "sqlite_version_string": sqlite3.sqlite_version, "sqlite_source_id": str(row[0]) if row and row[0] is not None else "", })) @@ -91,8 +85,7 @@ def probe_sqlite_runtime(python: str | Path, *, timeout: float = 30.0) -> SQLite try: payload = json.loads(result.stdout) return SQLiteRuntimeInfo( - executable=Path(str(payload["executable"])), - base_prefix=Path(str(payload["base_prefix"])), + executable=Path(str(payload["executable"])), base_prefix=Path(str(payload["base_prefix"])), python_version=_version_tuple(payload["python_version"]), sqlite_version=_version_tuple(payload["sqlite_version"]), sqlite_version_string=str(payload["sqlite_version_string"]), diff --git a/hermes_cli/sqlite_safe_read.py b/hermes_cli/sqlite_safe_read.py index 8629ce2b58..4e3acf166b 100644 --- a/hermes_cli/sqlite_safe_read.py +++ b/hermes_cli/sqlite_safe_read.py @@ -68,20 +68,19 @@ def track_connection(path: Path | str) -> None: _track_key(_key(path)) -def _track_key(key: str) -> None: - """Bump the live count for an already-canonical key (caller holds ``_live_lock``).""" - _live_connections[key] = _live_connections.get(key, 0) + 1 +def _track_key(key: str, delta: int = 1) -> None: + """Adjust the live count for an already-canonical key (caller holds ``_live_lock``).""" + remaining = _live_connections.get(key, 0) + delta + if remaining > 0: + _live_connections[key] = remaining + else: + _live_connections.pop(key, None) def untrack_connection(path: Path | str) -> None: """Record that one connection to *path* has been closed.""" - key = _key(path) with _live_lock: - remaining = _live_connections.get(key, 0) - 1 - if remaining > 0: - _live_connections[key] = remaining - else: - _live_connections.pop(key, None) + _track_key(_key(path), -1) def has_live_connection(path: Path | str) -> bool: diff --git a/hermes_cli/status_auth.py b/hermes_cli/status_auth.py index b7ce339895..15ae06de79 100644 --- a/hermes_cli/status_auth.py +++ b/hermes_cli/status_auth.py @@ -1,10 +1,7 @@ -"""Credential sections of `hermes status`: API keys, OAuth providers, Nous Tool Gateway, -API-key providers. Split out of ``hermes_cli/status.py``; the renderers are re-imported there -and run through ``status._SECTIONS`` with the shared ``_StatusContext``. - -Origin helpers (``_row``, ``_first_env_value``, ...) are imported lazily from -``hermes_cli.status`` so tests that monkeypatch that module keep working. -""" +"""Credential sections of `hermes status` (API keys, OAuth providers, Nous Tool Gateway, API-key +providers), run through ``status._SECTIONS`` with the shared ``_StatusContext``. Origin helpers +(``_row``, ``_first_env_value``, ...) are imported lazily from ``hermes_cli.status`` so tests that +monkeypatch that module keep working.""" from hermes_cli.auth import AuthError from hermes_cli.nous_account import ( @@ -23,7 +20,9 @@ def _format_iso_timestamp(value) -> str: parsed = datetime.fromisoformat(text[:-1] + "+00:00" if text.endswith("Z") else text) except Exception: return value - return (parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=timezone.utc)).astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=timezone.utc) + return parsed.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") def _qwen_expiry(expires_at_ms) -> str: @@ -117,14 +116,13 @@ def _render_auth_providers(ctx): _section("Auth Providers") import hermes_cli.auth as auth try: - # Read-only display: use the refresh-free snapshot so `hermes status` - # never performs an OAuth refresh or burns a single-use refresh token. + # Read-only display: the refresh-free snapshot, so `hermes status` never performs an OAuth + # refresh or burns a single-use refresh token. nous_status = auth.get_nous_auth_status_local() statuses = {getter: getattr(auth, getter)() for _, getter, _, _ in _OAUTH_BLOCKS[:3]} except Exception: nous_status, statuses = {}, {} - # xAI OAuth is guarded separately so an import failure there cannot disrupt - # the Nous/Codex/Qwen/MiniMax rows. + # xAI OAuth is guarded separately so an import failure there cannot disrupt the other rows. try: statuses["get_xai_oauth_auth_status"] = auth.get_xai_oauth_auth_status() or {} except Exception: @@ -136,18 +134,16 @@ def _render_auth_providers(ctx): try: info = get_nous_portal_account_info() except Exception: - info = None + pass ctx.nous_account_info = info ctx.nous_logged_in = logged_in = bool(nous_status.get("logged_in") or (info and info.logged_in)) ctx.nous_inference_present = inference = bool( nous_status.get("inference_credential_present") or (info and info.inference_credential_present) ) nous_error = nous_status.get("error") - _row( - "Nous Portal", logged_in, - "logged in" if logged_in - else "not logged in (Nous inference key configured)" if inference - else "not logged in (run: hermes portal)") + _row("Nous Portal", logged_in, + "logged in" if logged_in else "not logged in (Nous inference key configured)" if inference + else "not logged in (run: hermes portal)") portal_url = nous_status.get("portal_base_url") or "(unknown)" inference_url = nous_status.get("inference_base_url") or (info.inference_base_url if info else None) for label, value, show in ( @@ -185,8 +181,8 @@ def _render_nous_gateway(ctx): state = "not configured" print(f" {f.label:<15} {check_mark(f.available or f.active or f.managed_by_nous)} {state}") elif ctx.nous_logged_in or ctx.nous_inference_present: - # Nous OAuth without entitlement, or an opaque inference key without - # Portal account information, cannot enable the Tool Gateway. + # Nous OAuth without entitlement, or an opaque inference key without Portal account + # information, cannot enable the Tool Gateway. _section("Nous Tool Gateway") message = format_nous_portal_entitlement_message( ctx.nous_account_info, capability="managed web, image, TTS, STT, browser, and Modal tools" @@ -202,9 +198,8 @@ def _render_apikey_providers(ctx): configured = bool(_first_env_value(env_vars)) print(f" {pname:<16} {check_mark(configured)} {'configured' if configured else 'not configured (run: hermes model)'}") - # LM Studio reachability: probe only when it is the active provider so users - # with foreign configs see no noise. Auth rejection vs. a silent empty list - # is the most common LM Studio support case. + # LM Studio reachability: probe only when it is the active provider so users with foreign + # configs see no noise. Auth rejection vs. a silent empty list is the common support case. if _effective_provider_label() == "LM Studio": from hermes_cli.models import probe_lmstudio_models model_cfg = ctx.config.get("model")