refactor(cli/setup_*): table-drive bluebubbles/vercel prompts, flatten telegram allowlist branches, compact prose
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -239,13 +239,13 @@ _TOOL_ROW_BUILDERS = (
|
||||
|
||||
def _print_cmd_rows(rows):
|
||||
"""Print (command, description) rows as ' <green cmd><desc>'."""
|
||||
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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
+12
-16
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user