refactor(cli/setup_*): table-drive bluebubbles/vercel prompts, flatten telegram allowlist branches, compact prose

This commit is contained in:
Teknium
2026-09-02 16:04:47 -07:00
parent 1ac48ebe5a
commit 324701cd84
6 changed files with 94 additions and 140 deletions
+13 -28
View File
@@ -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"
+32 -51
View File
@@ -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()
+3 -2
View File
@@ -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()
+6 -11
View File
@@ -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)
+28 -32
View File
@@ -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
View File
@@ -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():