From 324701cd84450d6c52ee18967e26159b40e143ef Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:04:47 -0700 Subject: [PATCH] refactor(cli/setup_*): table-drive bluebubbles/vercel prompts, flatten telegram allowlist branches, compact prose --- hermes_cli/setup_migration.py | 41 ++++++----------- hermes_cli/setup_platforms.py | 83 ++++++++++++++--------------------- hermes_cli/setup_quick.py | 5 ++- hermes_cli/setup_summary.py | 17 +++---- hermes_cli/setup_terminal.py | 60 ++++++++++++------------- hermes_cli/setup_tts.py | 28 +++++------- 6 files changed, 94 insertions(+), 140 deletions(-) diff --git a/hermes_cli/setup_migration.py b/hermes_cli/setup_migration.py index 44943e4565..c941d5975f 100644 --- a/hermes_cli/setup_migration.py +++ b/hermes_cli/setup_migration.py @@ -23,13 +23,9 @@ _OPENROUTER_ENV_VARS = ("OPENROUTER_API_KEY", "OPENAI_API_KEY") def _model_section_has_credentials(config: dict) -> bool: - """Return True when any known inference provider has usable credentials. - - Sources of truth: ``PROVIDER_REGISTRY`` in ``hermes_cli.auth`` (every provider with its - ``api_key_env_vars``); ``active_provider`` in the auth store (OAuth device-code / external-OAuth - providers: Nous, Codex, Qwen, Gemini CLI, ...); and the legacy OpenRouter aggregator env vars, - which route generic ``OPENAI_API_KEY`` / ``OPENROUTER_API_KEY`` values through OpenRouter. - """ + """True when any known inference provider has usable credentials: ``active_provider`` in the + auth store (OAuth providers), ``PROVIDER_REGISTRY`` ``api_key_env_vars``, or the legacy + OpenRouter aggregator env vars (``OPENAI_API_KEY`` / ``OPENROUTER_API_KEY``).""" from hermes_cli.setup import get_env_value try: from hermes_cli.auth import get_active_provider @@ -93,10 +89,8 @@ def _cfg_summary(config: dict, section: str, key: str, default, prefix: str) -> def _gateway_summary(config: dict) -> Optional[str]: from hermes_cli.gateway import _all_platforms, _platform_status - # Count any non-empty status other than the "not configured" sentinel — platforms like - # WhatsApp ("enabled, not paired"), Matrix ("configured + E2EE"), and Signal ("partially - # configured") all indicate the user has already started setup and we shouldn't force - # the section to rerun. No platforms configured -> None -> section must run. + # Any non-empty status other than "not configured" counts — WhatsApp ("enabled, not paired"), + # Matrix ("configured + E2EE"), Signal ("partially configured") mean setup already started. configured = [ _gateway_platform_short_label(plat["label"]) for plat in _all_platforms() @@ -179,10 +173,8 @@ def _load_openclaw_migration_module(): return mod -# Item kinds that represent high-impact changes warranting explicit warnings. -# Gateway tokens/channels can hijack messaging platforms from the old agent. -# Config values may have different semantics between OpenClaw and Hermes. -# Instruction/context files (.md) can contain incompatible setup procedures. +# Item kinds that warrant explicit warnings: gateway tokens/channels hijack the old agent's +# platforms; config values and instruction/context .md files may not map 1:1 to Hermes. _HIGH_IMPACT_KIND_KEYWORDS = { "gateway": "⚠ Gateway/messaging — this will configure Hermes to use your OpenClaw messaging channels", "telegram": "⚠ Telegram — this will point Hermes at your OpenClaw Telegram bot", @@ -214,12 +206,9 @@ def _reason_row(default_reason: str, item: dict, kind: str) -> str: def _print_migration_preview(report: dict): - """Print a detailed dry-run preview of what migration would do. - - Groups items by status and adds explicit warnings for high-impact changes like - gateway token takeover and config value differences. - """ - from hermes_cli.setup import Colors, color, print_info + """Dry-run preview grouped by status, with warnings for high-impact items (gateway takeover, + config semantics).""" + from hermes_cli.setup import color, Colors, print_info items = report.get("items", []) if not items: print_info("Nothing to migrate.") @@ -267,14 +256,10 @@ def _run_migrator(mod, openclaw_dir: Path, hermes_home: Path, selected, *, execu def _offer_openclaw_migration(hermes_home: Path) -> bool: - """Detect ~/.openclaw and offer to migrate during first-time setup. - - Runs a dry-run first to show the user exactly what would be imported, overwritten, or - taken over. Only executes after explicit confirmation. Returns True if migration ran - successfully, False otherwise. - """ + """Detect ~/.openclaw and offer to migrate during first-time setup: dry-run preview first, + execute only after explicit confirmation. Returns True iff migration ran successfully.""" from hermes_cli.setup import ( - _OPENCLAW_SCRIPT, get_config_path, _info, load_config, print_header, print_info, print_success, + get_config_path, _info, load_config, _OPENCLAW_SCRIPT, print_header, print_info, print_success, print_warning, prompt_yes_no, save_config, ) openclaw_dir = Path.home() / ".openclaw" diff --git a/hermes_cli/setup_platforms.py b/hermes_cli/setup_platforms.py index e22922f03c..3354583955 100644 --- a/hermes_cli/setup_platforms.py +++ b/hermes_cli/setup_platforms.py @@ -150,10 +150,9 @@ def _setup_telegram(): " 1. Message @userinfobot on Telegram", " 2. It will reply with your numeric ID (e.g., 123456789)", None) - open_access_q = "Allowed user IDs (comma-separated, leave empty for open access)" - detected_user_id = getattr(setup_result, "owner_user_id", None) - if detected_user_id: - detected_id = str(detected_user_id) + allowed_users = None + detected_id = str(getattr(setup_result, "owner_user_id", None) or "") + if detected_id: print_success(f"Detected your Telegram user ID: {detected_id}") if prompt_yes_no("Allow this Telegram account to use the bot?", True): extra = prompt("Additional allowed user IDs (comma-separated, optional)") @@ -162,15 +161,12 @@ def _setup_telegram(): if uid and uid not in ids: ids.append(uid) allowed_users = ",".join(ids) - else: - allowed_users = prompt(open_access_q) - else: - allowed_users = prompt(open_access_q) + if allowed_users is None: + allowed_users = prompt("Allowed user IDs (comma-separated, leave empty for open access)") if allowed_users: allowed_users = allowed_users.replace(" ", "") - save_env_value("TELEGRAM_ALLOWED_USERS", allowed_users) - print_success("Telegram allowlist configured - only listed users can use the bot") + _save_allowlist("TELEGRAM_ALLOWED_USERS", allowed_users, "Telegram allowlist configured - only listed users can use the bot") else: print_info("⚠️ No allowlist set - anyone who finds your bot can use it!") @@ -179,20 +175,15 @@ def _setup_telegram(): " For Telegram DMs, this is your user ID (same as above).") first_user_id = allowed_users.split(",")[0].strip() if allowed_users else "" - if first_user_id: - if prompt_yes_no(f"Use your user ID ({first_user_id}) as the home channel?", True): - save_env_value("TELEGRAM_HOME_CHANNEL", first_user_id) - print_success(f"Telegram home channel set to {first_user_id}") - else: - q = "Home channel ID (or leave empty to set later with /set-home in Telegram)" - _save_if_set("TELEGRAM_HOME_CHANNEL", prompt(q)) - else: + if not first_user_id: print_info(" You can also set this later by typing /set-home in your Telegram chat.") _save_if_set("TELEGRAM_HOME_CHANNEL", prompt("Home channel ID (leave empty to set later)")) - - -# _setup_slack/_write_slack_manifest_and_instruct and _setup_matrix live in their plugins -# (plugins/platforms/{slack,matrix}/adapter.py::interactive_setup, via setup_fn). #41112 #3823. + elif prompt_yes_no(f"Use your user ID ({first_user_id}) as the home channel?", True): + save_env_value("TELEGRAM_HOME_CHANNEL", first_user_id) + print_success(f"Telegram home channel set to {first_user_id}") + else: + q = "Home channel ID (or leave empty to set later with /set-home in Telegram)" + _save_if_set("TELEGRAM_HOME_CHANNEL", prompt(q)) def _setup_bluebubbles(): @@ -209,17 +200,15 @@ def _setup_bluebubbles(): " Download: https://bluebubbles.app/", None, "In BlueBubbles Server → Settings → API, note your Server URL and Password.", None) - server_url = prompt("BlueBubbles server URL (e.g. http://192.168.1.10:1234)") - if not server_url: - print_warning("Server URL is required — skipping BlueBubbles setup") - return - save_env_value("BLUEBUBBLES_SERVER_URL", server_url.rstrip("/")) - - password = prompt("BlueBubbles server password", password=True) - if not password: - print_warning("Password is required — skipping BlueBubbles setup") - return - save_env_value("BLUEBUBBLES_PASSWORD", password) + for label, env_var, secret, what in ( + ("BlueBubbles server URL (e.g. http://192.168.1.10:1234)", "BLUEBUBBLES_SERVER_URL", False, "Server URL"), + ("BlueBubbles server password", "BLUEBUBBLES_PASSWORD", True, "Password"), + ): + value = prompt(label, password=secret) + if not value: + print_warning(f"{what} is required — skipping BlueBubbles setup") + return + save_env_value(env_var, value.rstrip("/") if env_var == "BLUEBUBBLES_SERVER_URL" else value) print_success("BlueBubbles credentials saved") _info(None, "🔒 Security: Restrict who can message your bot", @@ -281,8 +270,6 @@ def _setup_webhooks(): " Open config in your editor: hermes config edit") -# ── Gateway ── - # (platform label, credential env var, home-channel env vars — any one satisfies) _HOME_CHANNEL_CHECKS = ( ("Telegram", "TELEGRAM_BOT_TOKEN", ("TELEGRAM_HOME_CHANNEL",)), @@ -317,9 +304,8 @@ def _warn_missing_home_channels() -> None: def _restart_running_gateway(any_messaging: bool, supports_systemd: bool) -> None: - """Already running: only offer a restart when this setup pass may have changed - platform config — a restart interrupts any active session, so it stays behind a - prompt.""" + """Already running: offer a restart only when this pass may have changed platform config — + a restart interrupts any active session, so it stays behind a prompt.""" from hermes_cli.setup import print_error, prompt_yes_no from hermes_cli.gateway import ( systemd_restart, launchd_restart, UserSystemdUnavailableError, SystemScopeRequiresRootError, @@ -344,9 +330,8 @@ def _restart_running_gateway(any_messaging: bool, supports_systemd: bool) -> Non for line in str(e).splitlines(): print(f" {line}") except SystemScopeRequiresRootError as e: - # Defense in depth: the pre-check above should have caught this, but a race - # (unit file appearing mid-run) could still land here. Previously this exited - # the whole wizard via sys.exit(1). + # Defense in depth: a race (unit file appearing mid-run) can slip past the pre-check; + # this used to sys.exit(1) the whole wizard. print_error(f" Restart failed: {e}") _print_system_scope_remediation("restart") except Exception as e: @@ -374,9 +359,8 @@ def setup_gateway(config: dict): for idx in selected or (): _configure_platform(platforms[idx]) - # Count any platform (built-in or plugin) the user configured during this setup - # pass — reuses ``_platform_status`` so plugin platforms like IRC are picked up - # without another hard-coded env-var list. + # Any platform (built-in or plugin) configured in this pass — via ``_platform_status`` so + # plugin platforms like IRC are counted without another hard-coded env-var list. any_messaging = any(_is_progress(_platform_status(p)) for p in _all_platforms()) if any_messaging: print() @@ -384,13 +368,10 @@ def setup_gateway(config: dict): print_success("Messaging platforms configured!") _warn_missing_home_channels() - # ── Gateway Service Setup ── - # Runs UNCONDITIONALLY — even with zero platforms configured. A gateway without - # platforms is a supported mode (cron scheduler keeps running, and adapters come up - # automatically once tokens are added later, e.g. via `hermes import` or - # `hermes setup gateway`). Gating this on messaging config was the bug that left - # install-then-import machines with registered cron jobs and restored bot tokens - # but no process to serve them. + # Gateway service setup runs UNCONDITIONALLY — a gateway with zero platforms is a supported + # mode (cron keeps running; adapters come up once tokens are added via `hermes import` / + # `hermes setup gateway`). Gating it on messaging config left install-then-import machines + # with cron jobs and bot tokens but no process to serve them. from hermes_cli.gateway import _is_service_running, supports_systemd_services, ensure_gateway_service supports_systemd = supports_systemd_services() diff --git a/hermes_cli/setup_quick.py b/hermes_cli/setup_quick.py index d79a186db6..30e4b5f51f 100644 --- a/hermes_cli/setup_quick.py +++ b/hermes_cli/setup_quick.py @@ -41,7 +41,7 @@ def _run_portal_one_shot(config: dict) -> None: ``_model_flow_nous`` — the same flow quick setup and ``hermes model`` use for Nous — so there is one source of truth and ``hermes portal`` always offers a picker. """ - from hermes_cli.setup import Colors, color, _info, load_config, print_error, print_info, print_success + from hermes_cli.setup import color, Colors, _info, load_config, print_error, print_info, print_success print() print(color("┌─────────────────────────────────────────────────────────┐", Colors.MAGENTA)) print(color("│ ⚕ Hermes Setup — Nous Portal (one-shot) │", Colors.MAGENTA)) @@ -374,7 +374,8 @@ def _blank_slate_walkthrough(config: dict, hermes_home): def _run_quick_setup(config: dict, hermes_home): """Quick setup — only configure items that are missing.""" from hermes_cli.setup import ( - Colors, color, _info, print_header, print_info, _print_setup_summary, print_success, _prompt_api_key, prompt_checklist, save_config, + color, Colors, _info, print_header, print_info, _print_setup_summary, print_success, + _prompt_api_key, prompt_checklist, save_config, ) from hermes_cli.config import (get_missing_env_vars, get_missing_config_fields, check_config_version) print() diff --git a/hermes_cli/setup_summary.py b/hermes_cli/setup_summary.py index 7ae2b6f8e9..552696b10c 100644 --- a/hermes_cli/setup_summary.py +++ b/hermes_cli/setup_summary.py @@ -239,13 +239,13 @@ _TOOL_ROW_BUILDERS = ( def _print_cmd_rows(rows): """Print (command, description) rows as ' '.""" - from hermes_cli.setup import Colors, color + from hermes_cli.setup import color, Colors for cmd, desc in rows: print(f" {color(cmd, Colors.GREEN)}{desc}") def _print_section_header(title): - from hermes_cli.setup import Colors, color + from hermes_cli.setup import color, Colors print(color("─" * 60, Colors.DIM)) print() print(color(title, Colors.CYAN, Colors.BOLD)) @@ -255,13 +255,11 @@ def _print_section_header(title): def _print_setup_summary(config: dict, hermes_home): """Print the setup completion summary.""" from hermes_cli.setup import ( - Colors, color, get_config_path, get_env_path, get_nous_subscription_features, _info, print_header, + color, Colors, get_config_path, get_env_path, get_nous_subscription_features, _info, print_header, print_warning, ) - # Provider readiness — the one thing setup absolutely must produce. Previously a user - # could cancel the API-key prompt mid-wizard (Enter → "Cancelled."), watch the wizard - # continue through Terminal/Gateway/Tools, and exit "successfully" with NO working model — - # believing they were set up. Say so loudly instead (consumer-onboarding audit finding #7). + # Provider readiness — the one thing setup must produce. A user who cancelled the API-key + # prompt mid-wizard used to exit "successfully" with NO working model; say so loudly. try: from hermes_cli.auth import resolve_provider @@ -283,10 +281,7 @@ def _print_setup_summary(config: dict, hermes_home): subscription_features = get_nous_subscription_features(config) for build in _TOOL_ROW_BUILDERS: row = build(config, subscription_features) - if isinstance(row, list): - tool_status.extend(row) - elif row is not None: - tool_status.append(row) + tool_status.extend(row if isinstance(row, list) else [row] if row is not None else []) available_count = sum(1 for _, avail, _ in tool_status if avail) _info(f"{available_count}/{len(tool_status)} tool categories available:", None) diff --git a/hermes_cli/setup_terminal.py b/hermes_cli/setup_terminal.py index 6d98cf0d23..86e0f9e0c2 100644 --- a/hermes_cli/setup_terminal.py +++ b/hermes_cli/setup_terminal.py @@ -36,8 +36,9 @@ def _prompt_vercel_sandbox_settings(config: dict): save_env_value("TERMINAL_VERCEL_RUNTIME", runtime) persist_label = "yes" if terminal.get("container_persistent", True) else "no" - answer = prompt(" Persist filesystem with snapshots? (yes/no)", persist_label) - terminal["container_persistent"] = answer.lower() in {"yes", "true", "y", "1"} + terminal["container_persistent"] = ( + prompt(" Persist filesystem with snapshots? (yes/no)", persist_label).lower() in {"yes", "true", "y", "1"} + ) # (key, prompt label, default, parser) — unparseable input leaves the value untouched. for key, label, default, parse in ( @@ -50,9 +51,7 @@ def _prompt_vercel_sandbox_settings(config: dict): pass if terminal.get("container_disk", 51200) not in {0, 51200}: - print_warning( - "Vercel Sandbox does not support custom disk sizing; resetting container_disk to 51200." - ) + print_warning("Vercel Sandbox does not support custom disk sizing; resetting container_disk to 51200.") terminal["container_disk"] = 51200 _info(None, "Vercel authentication:", " Use a long-lived Vercel access token plus project/team IDs.") @@ -61,12 +60,14 @@ def _prompt_vercel_sandbox_settings(config: dict): print_info(" Found defaults in nearest .vercel/project.json.") remove_env_value("VERCEL_OIDC_TOKEN") - token = prompt(" Vercel access token", get_env_value("VERCEL_TOKEN") or "", password=True) - project = prompt( - " Vercel project ID", get_env_value("VERCEL_PROJECT_ID") or linked.get("projectId", "") - ) - team = prompt(" Vercel team ID", get_env_value("VERCEL_TEAM_ID") or linked.get("orgId", "")) - for env_var, value in (("VERCEL_TOKEN", token), ("VERCEL_PROJECT_ID", project), ("VERCEL_TEAM_ID", team)): + # (label, env var, linked-project fallback key, secret) — prompted in order, saved when non-empty. + for label, env_var, linked_key, secret in ( + (" Vercel access token", "VERCEL_TOKEN", None, True), + (" Vercel project ID", "VERCEL_PROJECT_ID", "projectId", False), + (" Vercel team ID", "VERCEL_TEAM_ID", "orgId", False), + ): + default = get_env_value(env_var) or (linked.get(linked_key, "") if linked_key else "") + value = prompt(label, default, password=secret) if value: save_env_value(env_var, value) @@ -135,11 +136,11 @@ def _setup_backend_docker(config: dict) -> None: from hermes_cli.setup import _info, print_info, print_success, print_warning, prompt_yes_no print_success("Terminal backend: Docker") docker_bin = shutil.which("docker") - if not docker_bin: + if docker_bin: + print_info(f"Docker found: {docker_bin}") + else: print_warning("Docker not found in PATH!") print_info("Install Docker: https://docs.docker.com/get-docker/") - else: - print_info(f"Docker found: {docker_bin}") # Image and resource limits use defaults; tune via `hermes setup terminal`. config["terminal"].setdefault("docker_image", _SANDBOX_IMAGE) @@ -161,11 +162,11 @@ def _setup_backend_singularity(config: dict) -> None: from hermes_cli.setup import print_info, print_success, print_warning print_success("Terminal backend: Singularity/Apptainer") sing_bin = shutil.which("apptainer") or shutil.which("singularity") - if not sing_bin: + if sing_bin: + print_info(f"Found: {sing_bin}") + else: print_warning("Singularity/Apptainer not found in PATH!") print_info("Install: https://apptainer.org/docs/admin/main/installation.html") - else: - print_info(f"Found: {sing_bin}") # Image and resource limits use defaults; tune via `hermes setup terminal`. config["terminal"].setdefault("singularity_image", "docker://nikolaik/python-nodejs:python3.11-nodejs20") @@ -188,17 +189,14 @@ def _setup_backend_modal(config: dict) -> None: modal_mode = normalize_modal_mode(cfg_get(config, "terminal", "modal_mode")) use_managed_modal = False if managed_modal_available: - if modal_mode == "managed": - default_modal_idx = 0 - elif modal_mode == "direct": - default_modal_idx = 1 + if modal_mode in ("managed", "direct"): + default_modal_idx = 0 if modal_mode == "managed" else 1 else: default_modal_idx = 1 if get_env_value("MODAL_TOKEN_ID") else 0 - modal_mode_idx = prompt_choice( + use_managed_modal = prompt_choice( "Select how Modal execution should be billed:", ["Use my Nous subscription", "Use my own Modal account"], default_modal_idx, - ) - use_managed_modal = modal_mode_idx == 0 + ) == 0 if use_managed_modal: config["terminal"]["modal_mode"] = "managed" @@ -248,9 +246,8 @@ def _setup_backend_vercel(config: dict) -> None: print_info("Installing vercel SDK...") import subprocess - # Managed uv first: $HERMES_HOME/bin is never on PATH, so a bare which() - # misses the uv Hermes installed; bootstrapping one mid-wizard is fine, - # and a `uv venv` venv may not even have pip. + # Managed uv first: $HERMES_HOME/bin is never on PATH so which() misses Hermes' uv; + # bootstrapping one mid-wizard is fine, and a `uv venv` venv may not even have pip. from hermes_cli.managed_uv import ensure_uv uv_bin = ensure_uv() @@ -338,7 +335,7 @@ _TERMINAL_BACKEND_SETUP = { def setup_terminal_backend(config: dict): """Configure the terminal execution backend.""" from hermes_cli.setup import ( - _DOCS_BASE, cfg_get, _info, print_header, print_info, print_success, prompt_choice, save_config, + cfg_get, _DOCS_BASE, _info, print_header, print_info, print_success, prompt_choice, save_config, save_env_value, ) import platform as _platform @@ -352,8 +349,8 @@ def setup_terminal_backend(config: dict): if _platform.system() == "Linux": backends.append(("singularity", "Singularity/Apptainer - HPC-friendly container")) - # Plugin-registered terminal backends (standalone plugin repos under - # ~/.hermes/plugins/). Fail-soft: a broken plugin must not take the wizard down. + # Plugin-registered backends (~/.hermes/plugins/). Fail-soft: a broken plugin must not take + # the wizard down. plugin_backend_names = [] try: from hermes_cli.plugins import discover_plugins @@ -384,8 +381,7 @@ def setup_terminal_backend(config: dict): elif selected_backend in plugin_backend_names: _setup_backend_plugin(config, selected_backend) - # config.yaml is the source of truth, but terminal_tool reads TERMINAL_ENV - # from .env — keep them in sync. + # config.yaml is the source of truth, but terminal_tool reads TERMINAL_ENV from .env — sync. save_env_value("TERMINAL_ENV", selected_backend) if selected_backend == "modal": save_env_value("TERMINAL_MODAL_MODE", config["terminal"].get("modal_mode", "auto")) diff --git a/hermes_cli/setup_tts.py b/hermes_cli/setup_tts.py index 26d82f35eb..010559978f 100644 --- a/hermes_cli/setup_tts.py +++ b/hermes_cli/setup_tts.py @@ -83,11 +83,8 @@ def _install_kittentts_deps() -> bool: def _xai_oauth_logged_in_for_setup() -> bool: - """True iff xAI Grok OAuth credentials are already stored locally. - - Lets TTS / STT setup skip the API-key prompt for users who logged in - through ``hermes model`` -> xAI Grok OAuth (SuperGrok / Premium+). - """ + """True iff xAI Grok OAuth credentials are stored locally, so TTS/STT setup can skip the + API-key prompt for users who logged in via ``hermes model`` -> xAI Grok OAuth.""" try: from hermes_cli.auth import get_xai_oauth_auth_status @@ -99,12 +96,9 @@ 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 inference - provider or rewrite ``model.provider`` — callers (TTS setup, tools - config) only need credentials for side tools. - - Returns True on success, False on any failure (the caller falls back - to whatever the user picked next, e.g. Edge TTS). + 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). """ from hermes_cli.setup import _info, print_warning try: @@ -124,8 +118,8 @@ def _run_xai_oauth_login_from_setup() -> bool: creds["tokens"], discovery=creds.get("discovery"), redirect_uri=creds.get("redirect_uri", ""), last_refresh=creds.get("last_refresh"), auth_mode="oauth_device_code", set_active=False, ) - # Mirror model/dashboard re-login: clear device_code suppression so - # the pool can seed from the singleton after a prior `auth remove`. + # Mirror model/dashboard re-login: clear device_code suppression so the pool can seed + # from the singleton after a prior `auth remove`. unsuppress_credential_source("xai-oauth", "device_code") return True except Exception as exc: @@ -182,7 +176,9 @@ _TTS_LOCAL_PROVIDERS = { def _tts_api_key_step(selected: str) -> str: """Ensure the key for an API-key TTS provider exists; fall back to edge otherwise.""" - from hermes_cli.setup import get_env_value, print_info, print_success, print_warning, prompt, save_env_value + from hermes_cli.setup import ( + get_env_value, print_info, print_success, print_warning, prompt, save_env_value, + ) env_vars, save_var, prompt_label, saved_msg, hint = _TTS_API_KEY_PROVIDERS[selected] if any(get_env_value(v) for v in env_vars): return selected @@ -222,8 +218,8 @@ def _tts_xai_step(config: dict) -> str: """xAI TTS auth. Order: existing OAuth tokens (free for SuperGrok) > existing XAI_API_KEY > offer both paths — xAI TTS works with OAuth bearer tokens too.""" from hermes_cli.setup import ( - _run_xai_oauth_login_from_setup, _xai_oauth_logged_in_for_setup, get_env_value, print_success, - print_warning, prompt, prompt_choice, save_env_value, + get_env_value, print_success, print_warning, prompt, prompt_choice, _run_xai_oauth_login_from_setup, + save_env_value, _xai_oauth_logged_in_for_setup, ) selected = "xai" if _xai_oauth_logged_in_for_setup():