refactor(hermes_cli): replace per-function lazy 'from hermes_cli.setup/status import' with one bottom module alias
This commit is contained in:
+32
-39
@@ -66,14 +66,13 @@ _READY_ROWS = (
|
||||
def _voice_provider_status(kind: str, provider: str, rows: dict, default: tuple) -> tuple:
|
||||
"""Summary row for a TTS/STT provider. A keyed provider whose key is missing
|
||||
falls through to the default row, matching the runtime fallback."""
|
||||
from hermes_cli.setup import get_env_value, _module_installed
|
||||
row = rows.get(provider, default)
|
||||
if isinstance(row[1], tuple) and row[1] and not any(get_env_value(v) for v in row[1]):
|
||||
if isinstance(row[1], tuple) and row[1] and not any(_setup.get_env_value(v) for v in row[1]):
|
||||
row = default
|
||||
if isinstance(row[1], tuple):
|
||||
return (f"{kind} ({row[0]})", True, None)
|
||||
label, module, hint = row
|
||||
if _module_installed(module):
|
||||
if _setup._module_installed(module):
|
||||
return (f"{kind} ({label}{' local' if kind == 'Text-to-Speech' else ''})", True, None)
|
||||
return (f"{kind} ({label} — not installed)", False, hint)
|
||||
|
||||
@@ -110,7 +109,7 @@ def _vision_row(config, feats):
|
||||
ok = bool(get_available_vision_backends())
|
||||
except Exception:
|
||||
ok = False
|
||||
return ("Vision (image analysis)", True, None) if ok else ("Vision (image analysis)", False, "run 'hermes setup' to configure")
|
||||
return ("Vision (image analysis)", ok, None if ok else "run 'hermes setup' to configure")
|
||||
|
||||
|
||||
def _managed_or_provider_row(feature, name: str, managed_label: str, missing_hint: str):
|
||||
@@ -157,16 +156,14 @@ def _video_gen_row(config, feats):
|
||||
|
||||
def _tts_row(config, feats):
|
||||
# Configured provider, gated on its key (or local install)
|
||||
from hermes_cli.setup import cfg_get
|
||||
tts_provider = cfg_get(config, "tts", "provider", default="edge")
|
||||
tts_provider = _setup.cfg_get(config, "tts", "provider", default="edge")
|
||||
if feats.tts.managed_by_nous:
|
||||
return ("Text-to-Speech (OpenAI via Nous subscription)", True, None)
|
||||
return _voice_provider_status("Text-to-Speech", tts_provider, _TTS_SUMMARY_ROWS, _TTS_SUMMARY_DEFAULT)
|
||||
|
||||
|
||||
def _stt_row(config, feats):
|
||||
from hermes_cli.setup import cfg_get
|
||||
stt_provider = cfg_get(config, "stt", "provider", default="local") or "local"
|
||||
stt_provider = _setup.cfg_get(config, "stt", "provider", default="local") or "local"
|
||||
_stt_feature = feats.features.get("stt")
|
||||
if _stt_feature is not None and _stt_feature.managed_by_nous:
|
||||
return ("Speech-to-Text (OpenAI via Nous subscription)", True, None)
|
||||
@@ -174,21 +171,19 @@ def _stt_row(config, feats):
|
||||
|
||||
|
||||
def _modal_row(config, feats):
|
||||
from hermes_cli.setup import cfg_get, managed_nous_tools_enabled
|
||||
if feats.modal.managed_by_nous:
|
||||
return ("Modal Execution (Nous subscription)", True, None)
|
||||
if cfg_get(config, "terminal", "backend") == "modal":
|
||||
if _setup.cfg_get(config, "terminal", "backend") == "modal":
|
||||
if feats.modal.direct_override:
|
||||
return ("Modal Execution (direct Modal)", True, None)
|
||||
return ("Modal Execution", False, "run 'hermes setup terminal'")
|
||||
if managed_nous_tools_enabled() and feats.nous_auth_present:
|
||||
if _setup.managed_nous_tools_enabled() and feats.nous_auth_present:
|
||||
return ("Modal Execution (optional via Nous subscription)", True, None)
|
||||
return None
|
||||
|
||||
|
||||
def _home_assistant_row(config, feats):
|
||||
from hermes_cli.setup import get_env_value
|
||||
return ("Smart Home (Home Assistant)", True, None) if get_env_value("HASS_TOKEN") else None
|
||||
return ("Smart Home (Home Assistant)", True, None) if _setup.get_env_value("HASS_TOKEN") else None
|
||||
|
||||
|
||||
def _spotify_row(config, feats):
|
||||
@@ -204,8 +199,7 @@ def _spotify_row(config, feats):
|
||||
|
||||
|
||||
def _skills_hub_row(config, feats):
|
||||
from hermes_cli.setup import get_env_value
|
||||
ok = bool(get_env_value("GITHUB_TOKEN"))
|
||||
ok = bool(_setup.get_env_value("GITHUB_TOKEN"))
|
||||
return ("Skills Hub (GitHub)", ok, None if ok else "GITHUB_TOKEN")
|
||||
|
||||
|
||||
@@ -222,24 +216,19 @@ _TOOL_ROW_BUILDERS = (
|
||||
|
||||
def _print_cmd_rows(rows):
|
||||
"""Print (command, description) rows as ' <green cmd><desc>'."""
|
||||
from hermes_cli.setup import color, Colors
|
||||
for cmd, desc in rows:
|
||||
print(f" {color(cmd, Colors.GREEN)}{desc}")
|
||||
print(f" {_setup.color(cmd, _setup.Colors.GREEN)}{desc}")
|
||||
|
||||
|
||||
def _print_section_header(title):
|
||||
from hermes_cli.setup import color, Colors
|
||||
print(color("─" * 60, Colors.DIM))
|
||||
print(_setup.color("─" * 60, _setup.Colors.DIM))
|
||||
print()
|
||||
print(color(title, Colors.CYAN, Colors.BOLD))
|
||||
print(_setup.color(title, _setup.Colors.CYAN, _setup.Colors.BOLD))
|
||||
print()
|
||||
|
||||
|
||||
def _print_setup_summary(config: dict, hermes_home):
|
||||
"""Print the setup completion summary."""
|
||||
from hermes_cli.setup import (
|
||||
color, Colors, get_config_path, get_env_path, get_nous_subscription_features, _info, print_header,
|
||||
print_warning)
|
||||
from hermes_constants import display_hermes_home as _dhh
|
||||
# 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.
|
||||
@@ -249,43 +238,44 @@ def _print_setup_summary(config: dict, hermes_home):
|
||||
resolve_provider()
|
||||
except Exception:
|
||||
print()
|
||||
print_warning("No inference provider is configured — Hermes cannot chat yet.")
|
||||
_info(" Finish this one step with either of:",
|
||||
_setup.print_warning("No inference provider is configured — Hermes cannot chat yet.")
|
||||
_setup._info(" Finish this one step with either of:",
|
||||
" hermes model (pick any provider/model)",
|
||||
" hermes setup --portal (Nous Portal OAuth, no API key)")
|
||||
|
||||
print()
|
||||
print_header("Tool Availability Summary")
|
||||
_setup.print_header("Tool Availability Summary")
|
||||
|
||||
tool_status = []
|
||||
subscription_features = get_nous_subscription_features(config)
|
||||
subscription_features = _setup.get_nous_subscription_features(config)
|
||||
for build in _TOOL_ROW_BUILDERS:
|
||||
row = build(config, subscription_features)
|
||||
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)
|
||||
_setup._info(f"{available_count}/{len(tool_status)} tool categories available:", None)
|
||||
for name, available, missing_var in tool_status:
|
||||
if available:
|
||||
print(f" {color('✓', Colors.GREEN)} {name}")
|
||||
print(f" {_setup.color('✓', _setup.Colors.GREEN)} {name}")
|
||||
else:
|
||||
print(f" {color('✗', Colors.RED)} {name} {color(f'(missing {missing_var})', Colors.DIM)}")
|
||||
print(f" {_setup.color('✗', _setup.Colors.RED)} {name} "
|
||||
f"{_setup.color(f'(missing {missing_var})', _setup.Colors.DIM)}")
|
||||
print()
|
||||
|
||||
if available_count < len(tool_status):
|
||||
print_warning("Some tools are disabled. Run 'hermes setup tools' to configure them,")
|
||||
print_warning(f"or edit {_dhh()}/.env directly to add the missing API keys.")
|
||||
_setup.print_warning("Some tools are disabled. Run 'hermes setup tools' to configure them,")
|
||||
_setup.print_warning(f"or edit {_dhh()}/.env directly to add the missing API keys.")
|
||||
print()
|
||||
|
||||
print()
|
||||
for line in _DONE_BANNER:
|
||||
print(color(line, Colors.GREEN))
|
||||
print(_setup.color(line, _setup.Colors.GREEN))
|
||||
print()
|
||||
print(color(f"📁 All your files are in {_dhh()}/:", Colors.CYAN, Colors.BOLD))
|
||||
print(_setup.color(f"📁 All your files are in {_dhh()}/:", _setup.Colors.CYAN, _setup.Colors.BOLD))
|
||||
print()
|
||||
print(f" {color('Settings:', Colors.YELLOW)} {get_config_path()}")
|
||||
print(f" {color('API Keys:', Colors.YELLOW)} {get_env_path()}")
|
||||
print(f" {color('Data:', Colors.YELLOW)} {hermes_home}/cron/, sessions/, logs/")
|
||||
print(f" {_setup.color('Settings:', _setup.Colors.YELLOW)} {_setup.get_config_path()}")
|
||||
print(f" {_setup.color('API Keys:', _setup.Colors.YELLOW)} {_setup.get_env_path()}")
|
||||
print(f" {_setup.color('Data:', _setup.Colors.YELLOW)} {hermes_home}/cron/, sessions/, logs/")
|
||||
print()
|
||||
|
||||
_print_section_header("📝 To edit your configuration:")
|
||||
@@ -295,10 +285,13 @@ def _print_setup_summary(config: dict, hermes_home):
|
||||
print(" Set a specific value")
|
||||
print()
|
||||
print(" Or edit the files directly:")
|
||||
print(f" {color(f'nano {get_config_path()}', Colors.DIM)}")
|
||||
print(f" {color(f'nano {get_env_path()}', Colors.DIM)}")
|
||||
print(f" {_setup.color(f'nano {_setup.get_config_path()}', _setup.Colors.DIM)}")
|
||||
print(f" {_setup.color(f'nano {_setup.get_env_path()}', _setup.Colors.DIM)}")
|
||||
print()
|
||||
|
||||
_print_section_header("🚀 Ready to go!")
|
||||
_print_cmd_rows(_READY_ROWS)
|
||||
print()
|
||||
|
||||
|
||||
import hermes_cli.setup as _setup # noqa: E402 (bottom: hermes_cli.setup imports this module)
|
||||
|
||||
@@ -19,53 +19,51 @@ _RUN_KW = dict(capture_output=True, text=True, encoding="utf-8", errors="replace
|
||||
|
||||
def _prompt_vercel_sandbox_settings(config: dict):
|
||||
"""Prompt for Vercel Sandbox settings without exposing unsupported disk sizing."""
|
||||
from hermes_cli.setup import (
|
||||
get_env_value, _info, print_info, print_warning, prompt, remove_env_value, save_env_value)
|
||||
terminal = config.setdefault("terminal", {})
|
||||
_info(None, "Vercel Sandbox settings:", " Filesystem persistence uses Vercel snapshots.",
|
||||
_setup._info(None, "Vercel Sandbox settings:", " Filesystem persistence uses Vercel snapshots.",
|
||||
" Snapshots restore files only; live processes do not continue after sandbox recreation.")
|
||||
|
||||
from tools.terminal_tool import _SUPPORTED_VERCEL_RUNTIMES
|
||||
|
||||
current_runtime = terminal.get("vercel_runtime") or "node24"
|
||||
supported_label = ", ".join(_SUPPORTED_VERCEL_RUNTIMES)
|
||||
runtime = prompt(f" Runtime ({supported_label})", current_runtime).strip() or current_runtime
|
||||
runtime = _setup.prompt(f" Runtime ({supported_label})", current_runtime).strip() or current_runtime
|
||||
if runtime not in _SUPPORTED_VERCEL_RUNTIMES:
|
||||
print_warning(f"Unsupported Vercel runtime '{runtime}', keeping {current_runtime}.")
|
||||
_setup.print_warning(f"Unsupported Vercel runtime '{runtime}', keeping {current_runtime}.")
|
||||
runtime = current_runtime if current_runtime in _SUPPORTED_VERCEL_RUNTIMES else "node24"
|
||||
terminal["vercel_runtime"] = runtime
|
||||
save_env_value("TERMINAL_VERCEL_RUNTIME", runtime)
|
||||
_setup.save_env_value("TERMINAL_VERCEL_RUNTIME", runtime)
|
||||
|
||||
persist_label = "yes" if terminal.get("container_persistent", True) else "no"
|
||||
terminal["container_persistent"] = (
|
||||
prompt(" Persist filesystem with snapshots? (yes/no)", persist_label).lower() in {"yes", "true", "y", "1"}
|
||||
)
|
||||
persist = _setup.prompt(" Persist filesystem with snapshots? (yes/no)", persist_label).lower()
|
||||
terminal["container_persistent"] = persist in {"yes", "true", "y", "1"}
|
||||
# (key, prompt label, default, parser) — unparseable input leaves the value untouched.
|
||||
for key, label, default, parse in (
|
||||
("container_cpu", " CPU cores", 1, float),
|
||||
("container_memory", " Memory in MB (5120 = 5GB)", 5120, int)):
|
||||
try:
|
||||
terminal[key] = parse(prompt(label, str(terminal.get(key, default))))
|
||||
terminal[key] = parse(_setup.prompt(label, str(terminal.get(key, default))))
|
||||
except ValueError:
|
||||
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.")
|
||||
_setup.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.")
|
||||
_setup._info(None, "Vercel authentication:", " Use a long-lived Vercel access token plus project/team IDs.")
|
||||
linked = _read_nearest_vercel_project()
|
||||
if linked:
|
||||
print_info(" Found defaults in nearest .vercel/project.json.")
|
||||
remove_env_value("VERCEL_OIDC_TOKEN")
|
||||
_setup.print_info(" Found defaults in nearest .vercel/project.json.")
|
||||
_setup.remove_env_value("VERCEL_OIDC_TOKEN")
|
||||
# (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)
|
||||
default = _setup.get_env_value(env_var) or (linked.get(linked_key, "") if linked_key else "")
|
||||
value = _setup.prompt(label, default, password=secret)
|
||||
if value:
|
||||
save_env_value(env_var, value)
|
||||
_setup.save_env_value(env_var, value)
|
||||
|
||||
|
||||
def _read_nearest_vercel_project(start: Path | None = None) -> dict[str, str]:
|
||||
@@ -91,12 +89,11 @@ def _read_nearest_vercel_project(start: Path | None = None) -> dict[str, str]:
|
||||
|
||||
def _prompt_secret_env(label: str, env_var: str, *, confirm_msg: str = "") -> None:
|
||||
"""Prompt for a secret and persist it to .env when non-empty."""
|
||||
from hermes_cli.setup import print_success, prompt, save_env_value
|
||||
value = prompt(label, password=True)
|
||||
value = _setup.prompt(label, password=True)
|
||||
if value:
|
||||
save_env_value(env_var, value)
|
||||
_setup.save_env_value(env_var, value)
|
||||
if confirm_msg:
|
||||
print_success(confirm_msg)
|
||||
_setup.print_success(confirm_msg)
|
||||
|
||||
|
||||
def _pip_install_vercel(package):
|
||||
@@ -113,64 +110,60 @@ def _pip_install_vercel(package):
|
||||
|
||||
def _ensure_sdk(package: str, manual_hint: str, *, show_stderr: bool = False, install=None) -> None:
|
||||
"""Import *package*; if missing, install it (default: the venv pip ladder)."""
|
||||
from hermes_cli.setup import print_info, print_success, print_warning
|
||||
try:
|
||||
__import__(package)
|
||||
except ImportError:
|
||||
print_info(f"Installing {package} SDK...")
|
||||
_setup.print_info(f"Installing {package} SDK...")
|
||||
if install is None:
|
||||
from hermes_cli.tools_config import _pip_install
|
||||
install = lambda pkg: _pip_install([pkg]) # noqa: E731
|
||||
result = install(package)
|
||||
if result.returncode == 0:
|
||||
print_success(f"{package} SDK installed")
|
||||
_setup.print_success(f"{package} SDK installed")
|
||||
else:
|
||||
print_warning(f"Install failed — run manually: {manual_hint}")
|
||||
_setup.print_warning(f"Install failed — run manually: {manual_hint}")
|
||||
if show_stderr and result.stderr:
|
||||
print_info(f" Error: {result.stderr.strip().splitlines()[-1]}")
|
||||
_setup.print_info(f" Error: {result.stderr.strip().splitlines()[-1]}")
|
||||
|
||||
|
||||
def _report_binary(found: str | None, missing: str, install_hint: str, found_prefix: str = "Found: ") -> None:
|
||||
from hermes_cli.setup import print_info, print_warning
|
||||
if found:
|
||||
print_info(f"{found_prefix}{found}")
|
||||
_setup.print_info(f"{found_prefix}{found}")
|
||||
else:
|
||||
print_warning(missing)
|
||||
print_info(install_hint)
|
||||
_setup.print_warning(missing)
|
||||
_setup.print_info(install_hint)
|
||||
|
||||
|
||||
def _setup_backend_local(config: dict) -> None:
|
||||
from hermes_cli.setup import print_info, print_success
|
||||
print_success("Terminal backend: Local")
|
||||
print_info("Commands run directly on this machine.")
|
||||
_setup.print_success("Terminal backend: Local")
|
||||
_setup.print_info("Commands run directly on this machine.")
|
||||
# Gateway cwd defaults to home; sudo stays off. Both configurable via `hermes setup terminal`.
|
||||
config["terminal"].setdefault("cwd", str(Path.home()))
|
||||
|
||||
|
||||
def _setup_backend_docker(config: dict) -> None:
|
||||
from hermes_cli.setup import _info, print_info, print_success, prompt_yes_no
|
||||
print_success("Terminal backend: Docker")
|
||||
_setup.print_success("Terminal backend: Docker")
|
||||
_report_binary(shutil.which("docker"), "Docker not found in PATH!",
|
||||
"Install Docker: https://docs.docker.com/get-docker/", "Docker found: ")
|
||||
# Image and resource limits use defaults; tune via `hermes setup terminal`.
|
||||
config["terminal"].setdefault("docker_image", _SANDBOX_IMAGE)
|
||||
_info(None, "Docker sandboxes can be protected with the egress credential firewall.",
|
||||
_setup._info(None, "Docker sandboxes can be protected with the egress credential firewall.",
|
||||
"It routes sandbox traffic through iron-proxy so containers receive "
|
||||
"proxy tokens instead of real API keys.",
|
||||
" Docker only for now; Modal, SSH, Daytona, and Singularity are not wired yet.")
|
||||
if prompt_yes_no(" Enable egress firewall for Docker sandboxes?", False):
|
||||
if _setup.prompt_yes_no(" Enable egress firewall for Docker sandboxes?", False):
|
||||
proxy_cfg = config.setdefault("proxy", {})
|
||||
proxy_cfg["enabled"] = True
|
||||
proxy_cfg.setdefault("enforce_on_docker", True)
|
||||
print_success("Egress firewall enabled in config")
|
||||
print_info("Run `hermes egress setup` then `hermes egress start` to mint tokens and launch the proxy.")
|
||||
_setup.print_success("Egress firewall enabled in config")
|
||||
_setup.print_info(
|
||||
"Run `hermes egress setup` then `hermes egress start` to mint tokens and launch the proxy.")
|
||||
else:
|
||||
print_info("Skipping egress firewall. You can enable it later with `hermes egress setup`.")
|
||||
_setup.print_info("Skipping egress firewall. You can enable it later with `hermes egress setup`.")
|
||||
|
||||
|
||||
def _setup_backend_singularity(config: dict) -> None:
|
||||
from hermes_cli.setup import print_success
|
||||
print_success("Terminal backend: Singularity/Apptainer")
|
||||
_setup.print_success("Terminal backend: Singularity/Apptainer")
|
||||
_report_binary(shutil.which("apptainer") or shutil.which("singularity"),
|
||||
"Singularity/Apptainer not found in PATH!",
|
||||
"Install: https://apptainer.org/docs/admin/main/installation.html")
|
||||
@@ -178,60 +171,57 @@ def _setup_backend_singularity(config: dict) -> None:
|
||||
|
||||
|
||||
def _setup_backend_modal(config: dict) -> None:
|
||||
from hermes_cli.setup import (
|
||||
cfg_get, get_env_value, get_nous_subscription_features, _info, managed_nous_tools_enabled,
|
||||
print_info, print_success, prompt_choice, prompt_yes_no)
|
||||
print_success("Terminal backend: Modal")
|
||||
print_info("Serverless cloud sandboxes. Each session gets its own container.")
|
||||
_setup.print_success("Terminal backend: Modal")
|
||||
_setup.print_info("Serverless cloud sandboxes. Each session gets its own container.")
|
||||
from tools.managed_tool_gateway import is_managed_tool_gateway_ready
|
||||
from tools.tool_backend_helpers import normalize_modal_mode
|
||||
|
||||
managed_modal_available = bool(
|
||||
managed_nous_tools_enabled()
|
||||
and get_nous_subscription_features(config).nous_auth_present
|
||||
_setup.managed_nous_tools_enabled()
|
||||
and _setup.get_nous_subscription_features(config).nous_auth_present
|
||||
and is_managed_tool_gateway_ready("modal"))
|
||||
modal_mode = normalize_modal_mode(cfg_get(config, "terminal", "modal_mode"))
|
||||
modal_mode = normalize_modal_mode(_setup.cfg_get(config, "terminal", "modal_mode"))
|
||||
use_managed_modal = False
|
||||
if managed_modal_available:
|
||||
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
|
||||
use_managed_modal = prompt_choice(
|
||||
default_modal_idx = 1 if _setup.get_env_value("MODAL_TOKEN_ID") else 0
|
||||
use_managed_modal = _setup.prompt_choice(
|
||||
"Select how Modal execution should be billed:",
|
||||
["Use my Nous subscription", "Use my own Modal account"], default_modal_idx,
|
||||
) == 0
|
||||
|
||||
if use_managed_modal:
|
||||
config["terminal"]["modal_mode"] = "managed"
|
||||
print_info("Modal execution will use the managed Nous gateway and bill to your subscription.")
|
||||
if get_env_value("MODAL_TOKEN_ID") or get_env_value("MODAL_TOKEN_SECRET"):
|
||||
print_info("Direct Modal credentials are still configured, but this backend is pinned to managed mode.")
|
||||
_setup.print_info("Modal execution will use the managed Nous gateway and bill to your subscription.")
|
||||
if _setup.get_env_value("MODAL_TOKEN_ID") or _setup.get_env_value("MODAL_TOKEN_SECRET"):
|
||||
_setup.print_info(
|
||||
"Direct Modal credentials are still configured, but this backend is pinned to managed mode.")
|
||||
return
|
||||
|
||||
config["terminal"]["modal_mode"] = "direct"
|
||||
print_info("Requires a Modal account: https://modal.com")
|
||||
_setup.print_info("Requires a Modal account: https://modal.com")
|
||||
_ensure_sdk("modal", "uv pip install modal")
|
||||
_info(None, "Modal authentication:", " Get your token at: https://modal.com/settings")
|
||||
if get_env_value("MODAL_TOKEN_ID"):
|
||||
print_info(" Modal token: already configured")
|
||||
if not prompt_yes_no(" Update Modal credentials?", False):
|
||||
_setup._info(None, "Modal authentication:", " Get your token at: https://modal.com/settings")
|
||||
if _setup.get_env_value("MODAL_TOKEN_ID"):
|
||||
_setup.print_info(" Modal token: already configured")
|
||||
if not _setup.prompt_yes_no(" Update Modal credentials?", False):
|
||||
return
|
||||
_prompt_secret_env(" Modal Token ID", "MODAL_TOKEN_ID")
|
||||
_prompt_secret_env(" Modal Token Secret", "MODAL_TOKEN_SECRET")
|
||||
|
||||
|
||||
def _setup_backend_daytona(config: dict) -> None:
|
||||
from hermes_cli.setup import get_env_value, _info, print_info, print_success, prompt_yes_no
|
||||
print_success("Terminal backend: Daytona")
|
||||
_info("Persistent cloud development environments.",
|
||||
_setup.print_success("Terminal backend: Daytona")
|
||||
_setup._info("Persistent cloud development environments.",
|
||||
"Each session gets a dedicated sandbox with filesystem persistence.",
|
||||
"Sign up at: https://daytona.io")
|
||||
_ensure_sdk("daytona", "uv pip install daytona", show_stderr=True)
|
||||
print()
|
||||
if get_env_value("DAYTONA_API_KEY"):
|
||||
print_info(" Daytona API key: already configured")
|
||||
if prompt_yes_no(" Update API key?", False):
|
||||
if _setup.get_env_value("DAYTONA_API_KEY"):
|
||||
_setup.print_info(" Daytona API key: already configured")
|
||||
if _setup.prompt_yes_no(" Update API key?", False):
|
||||
_prompt_secret_env(" Daytona API key", "DAYTONA_API_KEY", confirm_msg=" Updated")
|
||||
else:
|
||||
_prompt_secret_env(" Daytona API key", "DAYTONA_API_KEY", confirm_msg=" Configured")
|
||||
@@ -239,20 +229,16 @@ def _setup_backend_daytona(config: dict) -> None:
|
||||
|
||||
|
||||
def _setup_backend_vercel(config: dict) -> None:
|
||||
from hermes_cli.setup import _info, print_success
|
||||
print_success("Terminal backend: Vercel Sandbox")
|
||||
_info("Cloud microVM sandboxes with snapshot-backed filesystem persistence.",
|
||||
_setup.print_success("Terminal backend: Vercel Sandbox")
|
||||
_setup._info("Cloud microVM sandboxes with snapshot-backed filesystem persistence.",
|
||||
"Requires the optional SDK: pip install 'hermes-agent[vercel]'")
|
||||
_ensure_sdk("vercel", "pip install 'hermes-agent[vercel]'", show_stderr=True, install=_pip_install_vercel)
|
||||
_prompt_vercel_sandbox_settings(config)
|
||||
|
||||
|
||||
def _setup_backend_ssh(config: dict) -> None:
|
||||
from hermes_cli.setup import (
|
||||
get_env_value, print_info, print_success, print_warning, prompt, prompt_yes_no, save_env_value,
|
||||
)
|
||||
print_success("Terminal backend: SSH")
|
||||
print_info("Run commands on a remote machine via SSH.")
|
||||
_setup.print_success("Terminal backend: SSH")
|
||||
_setup.print_info("Run commands on a remote machine via SSH.")
|
||||
|
||||
# (label, env var, fallback default when .env is empty); the port is only saved when not 22.
|
||||
fields = (
|
||||
@@ -262,14 +248,14 @@ def _setup_backend_ssh(config: dict) -> None:
|
||||
(" SSH private key path", "TERMINAL_SSH_KEY", str(Path.home() / ".ssh" / "id_rsa")))
|
||||
values = []
|
||||
for label, env_var, default in fields:
|
||||
value = prompt(label, (get_env_value(env_var) or "") or default)
|
||||
value = _setup.prompt(label, (_setup.get_env_value(env_var) or "") or default)
|
||||
values.append(value)
|
||||
if value and (env_var != "TERMINAL_SSH_PORT" or value != "22"):
|
||||
save_env_value(env_var, value)
|
||||
_setup.save_env_value(env_var, value)
|
||||
host, user, port, ssh_key = values
|
||||
|
||||
if host and prompt_yes_no(" Test SSH connection?", True):
|
||||
print_info(" Testing connection...")
|
||||
if host and _setup.prompt_yes_no(" Test SSH connection?", True):
|
||||
_setup.print_info(" Testing connection...")
|
||||
import subprocess
|
||||
|
||||
ssh_cmd = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5"]
|
||||
@@ -280,24 +266,23 @@ def _setup_backend_ssh(config: dict) -> None:
|
||||
ssh_cmd += [f"{user}@{host}" if user else host, "echo ok"]
|
||||
result = subprocess.run(ssh_cmd, timeout=10, **_RUN_KW)
|
||||
if result.returncode == 0:
|
||||
print_success(" SSH connection successful!")
|
||||
_setup.print_success(" SSH connection successful!")
|
||||
else:
|
||||
print_warning(f" SSH connection failed: {result.stderr.strip()}")
|
||||
print_info(" Check your SSH key and host settings.")
|
||||
_setup.print_warning(f" SSH connection failed: {result.stderr.strip()}")
|
||||
_setup.print_info(" Check your SSH key and host settings.")
|
||||
|
||||
|
||||
def _setup_backend_plugin(config: dict, backend: str) -> None:
|
||||
from hermes_cli.setup import print_info, print_success, print_warning
|
||||
try:
|
||||
from agent.terminal_env_registry import get_provider
|
||||
|
||||
provider = get_provider(backend)
|
||||
print_success(f"Terminal backend: {provider.display_name}")
|
||||
_setup.print_success(f"Terminal backend: {provider.display_name}")
|
||||
for line in provider.setup_instructions():
|
||||
print_info(line)
|
||||
_setup.print_info(line)
|
||||
provider.post_setup()
|
||||
except Exception as exc:
|
||||
print_warning(f"Backend plugin setup hook failed: {exc}")
|
||||
_setup.print_warning(f"Backend plugin setup hook failed: {exc}")
|
||||
|
||||
|
||||
_BUILTIN_TERMINAL_BACKENDS = [
|
||||
@@ -319,16 +304,13 @@ _BACKEND_ENV_MIRROR = {"modal": ("TERMINAL_MODAL_MODE", "modal_mode", "auto"),
|
||||
|
||||
def setup_terminal_backend(config: dict):
|
||||
"""Configure the terminal execution backend."""
|
||||
from hermes_cli.setup import (
|
||||
cfg_get, _DOCS_BASE, _info, print_header, print_info, print_success, prompt_choice, save_config,
|
||||
save_env_value)
|
||||
import platform as _platform
|
||||
print_header("Terminal Backend")
|
||||
_info("Choose where Hermes runs shell commands and code.",
|
||||
_setup.print_header("Terminal Backend")
|
||||
_setup._info("Choose where Hermes runs shell commands and code.",
|
||||
"This affects tool execution, file access, and isolation.",
|
||||
f" Guide: {_DOCS_BASE}/user-guide/configuration#terminal-backend-configuration", None)
|
||||
f" Guide: {_setup._DOCS_BASE}/user-guide/configuration#terminal-backend-configuration", None)
|
||||
|
||||
current_backend = cfg_get(config, "terminal", "backend", default="local")
|
||||
current_backend = _setup.cfg_get(config, "terminal", "backend", default="local")
|
||||
backends = list(_BUILTIN_TERMINAL_BACKENDS)
|
||||
if _platform.system() == "Linux":
|
||||
backends.append(("singularity", "Singularity/Apptainer - HPC-friendly container"))
|
||||
@@ -351,9 +333,9 @@ def setup_terminal_backend(config: dict):
|
||||
|
||||
keep_current_idx = len(backends)
|
||||
terminal_choices = [label for _, label in backends] + [f"Keep current ({current_backend})"]
|
||||
terminal_idx = prompt_choice("Select terminal backend:", terminal_choices, keep_current_idx)
|
||||
terminal_idx = _setup.prompt_choice("Select terminal backend:", terminal_choices, keep_current_idx)
|
||||
if terminal_idx == keep_current_idx:
|
||||
print_info(f"Keeping current backend: {current_backend}")
|
||||
_setup.print_info(f"Keeping current backend: {current_backend}")
|
||||
return
|
||||
selected_backend = backends[terminal_idx][0] if 0 <= terminal_idx < len(backends) else None
|
||||
|
||||
@@ -365,10 +347,13 @@ def setup_terminal_backend(config: dict):
|
||||
elif selected_backend in plugin_backend_names:
|
||||
_setup_backend_plugin(config, selected_backend)
|
||||
|
||||
save_env_value("TERMINAL_ENV", selected_backend)
|
||||
_setup.save_env_value("TERMINAL_ENV", selected_backend)
|
||||
if selected_backend in _BACKEND_ENV_MIRROR:
|
||||
env_var, key, default = _BACKEND_ENV_MIRROR[selected_backend]
|
||||
save_env_value(env_var, config["terminal"].get(key, default))
|
||||
save_config(config)
|
||||
_setup.save_env_value(env_var, config["terminal"].get(key, default))
|
||||
_setup.save_config(config)
|
||||
print()
|
||||
print_success(f"Terminal backend set to: {selected_backend}")
|
||||
_setup.print_success(f"Terminal backend set to: {selected_backend}")
|
||||
|
||||
|
||||
import hermes_cli.setup as _setup # noqa: E402 (bottom: hermes_cli.setup imports this module)
|
||||
|
||||
+62
-67
@@ -15,19 +15,18 @@ logger = logging.getLogger("hermes_cli.setup")
|
||||
def _pip_install_tts_package(name: str, pip_args: list, manual_cmd: str) -> bool:
|
||||
"""Install a local TTS engine through the canonical uv → pip → ensurepip
|
||||
ladder so pip-less venvs (Ubuntu 25.10 ``python -m venv``, ``uv venv``) work."""
|
||||
from hermes_cli.setup import print_error, print_info, print_success
|
||||
from hermes_cli.tools_config import _pip_install
|
||||
try:
|
||||
result = _pip_install(pip_args, timeout=300)
|
||||
if result.returncode == 0:
|
||||
print_success(f"{name} installed successfully")
|
||||
_setup.print_success(f"{name} installed successfully")
|
||||
return True
|
||||
err = (result.stderr or "").strip()
|
||||
reason = err[:300] if err else "install failed"
|
||||
except Exception as e:
|
||||
reason = e
|
||||
print_error(f"Failed to install {name}: {reason}")
|
||||
print_info(f"Try manually: {manual_cmd}")
|
||||
_setup.print_error(f"Failed to install {name}: {reason}")
|
||||
_setup.print_info(f"Try manually: {manual_cmd}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -41,34 +40,32 @@ _ESPEAK_INSTALL = {
|
||||
|
||||
def _install_neutts_deps() -> bool:
|
||||
"""Install NeuTTS dependencies with user approval. Returns True on success."""
|
||||
from hermes_cli.setup import _info, print_info, print_success, print_warning, prompt_yes_no
|
||||
if not (shutil.which("espeak-ng") or shutil.which("espeak")):
|
||||
hint, install_cmd = _ESPEAK_INSTALL.get(sys.platform, _ESPEAK_INSTALL["linux"])
|
||||
print()
|
||||
print_warning("NeuTTS requires espeak-ng for phonemization.")
|
||||
print_info(hint)
|
||||
_setup.print_warning("NeuTTS requires espeak-ng for phonemization.")
|
||||
_setup.print_info(hint)
|
||||
print()
|
||||
if prompt_yes_no("Install espeak-ng now?", True):
|
||||
if _setup.prompt_yes_no("Install espeak-ng now?", True):
|
||||
try:
|
||||
subprocess.run(install_cmd, check=True)
|
||||
print_success("espeak-ng installed")
|
||||
_setup.print_success("espeak-ng installed")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||
print_warning(f"Could not install espeak-ng automatically: {e}")
|
||||
print_info("Please install it manually and re-run setup.")
|
||||
_setup.print_warning(f"Could not install espeak-ng automatically: {e}")
|
||||
_setup.print_info("Please install it manually and re-run setup.")
|
||||
return False
|
||||
else:
|
||||
print_warning("espeak-ng is required for NeuTTS. Install it manually before using NeuTTS.")
|
||||
_setup.print_warning("espeak-ng is required for NeuTTS. Install it manually before using NeuTTS.")
|
||||
|
||||
_info(None, "Installing neutts Python package...",
|
||||
_setup._info(None, "Installing neutts Python package...",
|
||||
"This will also download the TTS model (~300MB) on first use.", None)
|
||||
return _pip_install_tts_package("neutts", ["-U", "neutts[all]", "--quiet"], "uv pip install -U 'neutts[all]'")
|
||||
|
||||
|
||||
def _install_kittentts_deps() -> bool:
|
||||
"""Install KittenTTS dependencies with user approval. Returns True on success."""
|
||||
from hermes_cli.setup import _info
|
||||
wheel_url = "https://github.com/KittenML/KittenTTS/releases/download/0.8.1/kittentts-0.8.1-py3-none-any.whl"
|
||||
_info(None, "Installing kittentts Python package (~25-80MB model downloaded on first use)...", None)
|
||||
_setup._info(None, "Installing kittentts Python package (~25-80MB model downloaded on first use)...", None)
|
||||
return _pip_install_tts_package(
|
||||
"kittentts", ["-U", wheel_url, "soundfile", "--quiet"], f"uv pip install -U '{wheel_url}' soundfile",
|
||||
)
|
||||
@@ -91,17 +88,16 @@ def _run_xai_oauth_login_from_setup() -> bool:
|
||||
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:
|
||||
from hermes_cli.auth import (
|
||||
_is_remote_session, _save_xai_oauth_tokens, _xai_oauth_device_code_login,
|
||||
unsuppress_credential_source)
|
||||
except Exception as exc:
|
||||
print_warning(f"xAI Grok OAuth helpers unavailable: {exc}")
|
||||
_setup.print_warning(f"xAI Grok OAuth helpers unavailable: {exc}")
|
||||
return False
|
||||
|
||||
open_browser = not _is_remote_session()
|
||||
_info(None, "Signing in to xAI Grok OAuth (SuperGrok / Premium+)...")
|
||||
_setup._info(None, "Signing in to xAI Grok OAuth (SuperGrok / Premium+)...")
|
||||
try:
|
||||
creds = _xai_oauth_device_code_login(open_browser=open_browser)
|
||||
_save_xai_oauth_tokens(
|
||||
@@ -112,7 +108,7 @@ def _run_xai_oauth_login_from_setup() -> bool:
|
||||
unsuppress_credential_source("xai-oauth", "device_code")
|
||||
return True
|
||||
except Exception as exc:
|
||||
print_warning(f"xAI Grok OAuth login failed: {exc}")
|
||||
_setup.print_warning(f"xAI Grok OAuth login failed: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -131,11 +127,14 @@ _TTS_PROVIDER_CHOICES = [
|
||||
_TTS_PROVIDER_LABELS = {key: label.split(" (")[0] for key, label in _TTS_PROVIDER_CHOICES}
|
||||
# provider -> (env vars that satisfy it, env var to save, prompt, success line, pre-prompt hint)
|
||||
_TTS_API_KEY_PROVIDERS = {
|
||||
"elevenlabs": (("ELEVENLABS_API_KEY",), "ELEVENLABS_API_KEY", "ElevenLabs API key", "ElevenLabs API key saved", ""),
|
||||
"elevenlabs": (("ELEVENLABS_API_KEY",), "ELEVENLABS_API_KEY", "ElevenLabs API key",
|
||||
"ElevenLabs API key saved", ""),
|
||||
"openai": (("VOICE_TOOLS_OPENAI_KEY", "OPENAI_API_KEY"), "VOICE_TOOLS_OPENAI_KEY",
|
||||
"OpenAI API key for TTS", "OpenAI TTS API key saved", ""),
|
||||
"minimax": (("MINIMAX_API_KEY",), "MINIMAX_API_KEY", "MiniMax API key for TTS", "MiniMax TTS API key saved", ""),
|
||||
"mistral": (("MISTRAL_API_KEY",), "MISTRAL_API_KEY", "Mistral API key for TTS", "Mistral TTS API key saved", ""),
|
||||
"minimax": (("MINIMAX_API_KEY",), "MINIMAX_API_KEY", "MiniMax API key for TTS",
|
||||
"MiniMax TTS API key saved", ""),
|
||||
"mistral": (("MISTRAL_API_KEY",), "MISTRAL_API_KEY", "Mistral API key for TTS",
|
||||
"Mistral TTS API key saved", ""),
|
||||
"gemini": (("GEMINI_API_KEY", "GOOGLE_API_KEY"), "GEMINI_API_KEY", "Gemini API key for TTS",
|
||||
"Gemini TTS API key saved", "Get a free API key at https://aistudio.google.com/app/apikey"),
|
||||
}
|
||||
@@ -154,38 +153,36 @@ _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
|
||||
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):
|
||||
if any(_setup.get_env_value(v) for v in env_vars):
|
||||
return selected
|
||||
print()
|
||||
if hint:
|
||||
print_info(hint)
|
||||
api_key = prompt(prompt_label, password=True)
|
||||
_setup.print_info(hint)
|
||||
api_key = _setup.prompt(prompt_label, password=True)
|
||||
if api_key:
|
||||
save_env_value(save_var, api_key)
|
||||
print_success(saved_msg)
|
||||
_setup.save_env_value(save_var, api_key)
|
||||
_setup.print_success(saved_msg)
|
||||
return selected
|
||||
print_warning("No API key provided. Falling back to Edge TTS.")
|
||||
_setup.print_warning("No API key provided. Falling back to Edge TTS.")
|
||||
return "edge"
|
||||
|
||||
|
||||
def _tts_local_install_step(selected: str) -> str:
|
||||
"""Offer to install a local TTS engine; fall back to edge if declined/failed."""
|
||||
from hermes_cli.setup import _module_installed, print_info, print_success, print_warning, prompt_yes_no
|
||||
module, name, lines, question, installer = _TTS_LOCAL_PROVIDERS[selected]
|
||||
if _module_installed(module):
|
||||
print_success(f"{name} is already installed")
|
||||
if _setup._module_installed(module):
|
||||
_setup.print_success(f"{name} is already installed")
|
||||
return selected
|
||||
print()
|
||||
for line in lines:
|
||||
print_info(line)
|
||||
_setup.print_info(line)
|
||||
print()
|
||||
if not prompt_yes_no(question, True):
|
||||
print_info(f"Skipping install. Set tts.provider to '{selected}' after installing manually.")
|
||||
if not _setup.prompt_yes_no(question, True):
|
||||
_setup.print_info(f"Skipping install. Set tts.provider to '{selected}' after installing manually.")
|
||||
return "edge"
|
||||
if not installer():
|
||||
print_warning(f"{name} installation incomplete. Falling back to Edge TTS.")
|
||||
_setup.print_warning(f"{name} installation incomplete. Falling back to Edge TTS.")
|
||||
return "edge"
|
||||
return selected
|
||||
|
||||
@@ -193,67 +190,62 @@ def _tts_local_install_step(selected: str) -> str:
|
||||
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 (
|
||||
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)
|
||||
if _xai_oauth_logged_in_for_setup():
|
||||
print_success("xAI TTS will use your xAI Grok OAuth (SuperGrok / Premium+) credentials")
|
||||
elif get_env_value("XAI_API_KEY"):
|
||||
print_success("xAI TTS will use your existing XAI_API_KEY")
|
||||
if _setup._xai_oauth_logged_in_for_setup():
|
||||
_setup.print_success("xAI TTS will use your xAI Grok OAuth (SuperGrok / Premium+) credentials")
|
||||
elif _setup.get_env_value("XAI_API_KEY"):
|
||||
_setup.print_success("xAI TTS will use your existing XAI_API_KEY")
|
||||
else:
|
||||
print()
|
||||
choice_idx = prompt_choice(
|
||||
choice_idx = _setup.prompt_choice(
|
||||
"How do you want xAI TTS to authenticate?",
|
||||
choices=["Sign in with xAI Grok OAuth (SuperGrok / Premium+) — browser login",
|
||||
"Paste an xAI API key (console.x.ai)", "Skip → fallback to Edge TTS"],
|
||||
default=0)
|
||||
fallback = None # warning printed when xAI auth did not happen; result is then "edge"
|
||||
if choice_idx == 0:
|
||||
if _run_xai_oauth_login_from_setup():
|
||||
print_success("Logged in — xAI TTS will use these OAuth credentials")
|
||||
if _setup._run_xai_oauth_login_from_setup():
|
||||
_setup.print_success("Logged in — xAI TTS will use these OAuth credentials")
|
||||
else:
|
||||
fallback = "xAI Grok OAuth login did not complete. Falling back to Edge TTS."
|
||||
elif choice_idx == 1:
|
||||
api_key = prompt("xAI API key for TTS", password=True)
|
||||
api_key = _setup.prompt("xAI API key for TTS", password=True)
|
||||
if api_key:
|
||||
save_env_value("XAI_API_KEY", api_key)
|
||||
print_success("xAI TTS API key saved")
|
||||
_setup.save_env_value("XAI_API_KEY", api_key)
|
||||
_setup.print_success("xAI TTS API key saved")
|
||||
else:
|
||||
from hermes_constants import display_hermes_home as _dhh
|
||||
fallback = ("No xAI API key provided for TTS. Configure XAI_API_KEY "
|
||||
f"via hermes setup model or {_dhh()}/.env to use xAI TTS. Falling back to Edge TTS.")
|
||||
fallback = ("No xAI API key provided for TTS. Configure XAI_API_KEY via hermes setup model "
|
||||
f"or {_dhh()}/.env to use xAI TTS. Falling back to Edge TTS.")
|
||||
else:
|
||||
fallback = "xAI TTS skipped. Falling back to Edge TTS."
|
||||
if fallback:
|
||||
print_warning(fallback)
|
||||
_setup.print_warning(fallback)
|
||||
return "edge"
|
||||
|
||||
print()
|
||||
voice_id = (prompt("xAI voice_id (Enter for 'eve', or paste a custom voice ID)") or "").strip()
|
||||
voice_id = (_setup.prompt("xAI voice_id (Enter for 'eve', or paste a custom voice ID)") or "").strip()
|
||||
if voice_id:
|
||||
config.setdefault("tts", {}).setdefault("xai", {})["voice_id"] = voice_id
|
||||
print_success(f"xAI voice_id set to: {voice_id}")
|
||||
_setup.print_success(f"xAI voice_id set to: {voice_id}")
|
||||
return "xai"
|
||||
|
||||
|
||||
def _setup_tts_provider(config: dict):
|
||||
"""Interactive TTS provider selection with install flow for local engines."""
|
||||
from hermes_cli.setup import (
|
||||
get_env_value, get_nous_subscription_features, _info, managed_nous_tools_enabled, print_header,
|
||||
print_info, print_success, print_warning, prompt_choice, save_config)
|
||||
current_provider = config.get("tts", {}).get("provider", "edge")
|
||||
current_label = _TTS_PROVIDER_LABELS.get(current_provider, current_provider)
|
||||
|
||||
print()
|
||||
print_header("Text-to-Speech Provider (optional)")
|
||||
_info(f"Current: {current_label}", None)
|
||||
_setup.print_header("Text-to-Speech Provider (optional)")
|
||||
_setup._info(f"Current: {current_label}", None)
|
||||
|
||||
options = list(_TTS_PROVIDER_CHOICES)
|
||||
if managed_nous_tools_enabled() and get_nous_subscription_features(config).nous_auth_present:
|
||||
options.insert(0, ("nous-openai", "Nous Subscription (managed OpenAI TTS, billed to your subscription)"))
|
||||
if _setup.managed_nous_tools_enabled() and _setup.get_nous_subscription_features(config).nous_auth_present:
|
||||
options.insert(0, ("nous-openai",
|
||||
"Nous Subscription (managed OpenAI TTS, billed to your subscription)"))
|
||||
choices = [label for _, label in options] + [f"Keep current ({current_label})"]
|
||||
keep_current_idx = len(choices) - 1
|
||||
idx = prompt_choice("Select TTS provider:", choices, keep_current_idx)
|
||||
idx = _setup.prompt_choice("Select TTS provider:", choices, keep_current_idx)
|
||||
if idx == keep_current_idx:
|
||||
return
|
||||
|
||||
@@ -261,9 +253,9 @@ def _setup_tts_provider(config: dict):
|
||||
selected_via_nous = selected == "nous-openai"
|
||||
if selected_via_nous:
|
||||
selected = "openai"
|
||||
print_info("OpenAI TTS will use the managed Nous gateway and bill to your subscription.")
|
||||
if get_env_value("VOICE_TOOLS_OPENAI_KEY") or get_env_value("OPENAI_API_KEY"):
|
||||
print_warning("Direct OpenAI credentials are still configured and may take precedence "
|
||||
_setup.print_info("OpenAI TTS will use the managed Nous gateway and bill to your subscription.")
|
||||
if _setup.get_env_value("VOICE_TOOLS_OPENAI_KEY") or _setup.get_env_value("OPENAI_API_KEY"):
|
||||
_setup.print_warning("Direct OpenAI credentials are still configured and may take precedence "
|
||||
"until removed from ~/.hermes/.env.")
|
||||
|
||||
if selected in _TTS_LOCAL_PROVIDERS:
|
||||
@@ -274,10 +266,13 @@ def _setup_tts_provider(config: dict):
|
||||
selected = _tts_xai_step(config)
|
||||
|
||||
config.setdefault("tts", {})["provider"] = selected
|
||||
save_config(config)
|
||||
print_success(f"TTS provider set to: {_TTS_PROVIDER_LABELS.get(selected, selected)}")
|
||||
_setup.save_config(config)
|
||||
_setup.print_success(f"TTS provider set to: {_TTS_PROVIDER_LABELS.get(selected, selected)}")
|
||||
|
||||
|
||||
def setup_tts(config: dict):
|
||||
"""Standalone TTS setup (for 'hermes setup tts')."""
|
||||
_setup_tts_provider(config)
|
||||
|
||||
|
||||
import hermes_cli.setup as _setup # noqa: E402 (bottom: hermes_cli.setup imports this module)
|
||||
|
||||
+28
-25
@@ -36,13 +36,12 @@ def _oauth_block(name: str, status: dict, hint: str, rows) -> None:
|
||||
``rows`` are ``(label, status_key, formatter, gate)``: a detail prints when the raw value is
|
||||
truthy and ``gate`` is None or equals the logged-in state (False = only while logged out).
|
||||
"""
|
||||
from hermes_cli.status import _detail, _row
|
||||
logged_in = bool(status.get("logged_in"))
|
||||
_row(name, logged_in, "logged in" if logged_in else f"not logged in (run: {hint})")
|
||||
_status._row(name, logged_in, "logged in" if logged_in else f"not logged in (run: {hint})")
|
||||
for label, key, fmt, gate in rows:
|
||||
raw = status.get(key)
|
||||
if raw and (gate is None or gate == logged_in):
|
||||
_detail(label, fmt(raw) if fmt else raw)
|
||||
_status._detail(label, fmt(raw) if fmt else raw)
|
||||
|
||||
|
||||
# Values may be a single env var name (str) or a tuple of alternates (first found wins).
|
||||
@@ -100,20 +99,18 @@ _APIKEY_PROVIDERS = {
|
||||
|
||||
|
||||
def _render_api_keys(ctx):
|
||||
from hermes_cli.status import _first_env_value, _row, _section, redact_key
|
||||
_section("API Keys")
|
||||
_status._section("API Keys")
|
||||
for name, env_ref in _API_KEYS.items():
|
||||
value = _first_env_value(env_ref)
|
||||
_row(name, bool(value), redact_key(value))
|
||||
value = _status._first_env_value(env_ref)
|
||||
_status._row(name, bool(value), _status.redact_key(value))
|
||||
# Anthropic uses the dedicated lookup (it also resolves OAuth tokens).
|
||||
from hermes_cli.auth import get_anthropic_key
|
||||
anthropic_value = get_anthropic_key()
|
||||
_row("Anthropic", bool(anthropic_value), redact_key(anthropic_value))
|
||||
_status._row("Anthropic", bool(anthropic_value), _status.redact_key(anthropic_value))
|
||||
|
||||
|
||||
def _render_auth_providers(ctx):
|
||||
from hermes_cli.status import _detail, _row, _section
|
||||
_section("Auth Providers")
|
||||
_status._section("Auth Providers")
|
||||
import hermes_cli.auth as auth
|
||||
try:
|
||||
# Read-only display: the refresh-free snapshot, so `hermes status` never performs an OAuth
|
||||
@@ -141,7 +138,7 @@ def _render_auth_providers(ctx):
|
||||
nous_status.get("inference_credential_present") or (info and info.inference_credential_present)
|
||||
)
|
||||
nous_error = nous_status.get("error")
|
||||
_row("Nous Portal", logged_in,
|
||||
_status._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)"
|
||||
@@ -157,17 +154,17 @@ def _render_auth_providers(ctx):
|
||||
logged_in or nous_status.get("has_refresh_token")),
|
||||
("Error:", nous_error, nous_error)):
|
||||
if show:
|
||||
_detail(label, value)
|
||||
_status._detail(label, value)
|
||||
for name, getter, hint, rows in _OAUTH_BLOCKS:
|
||||
_oauth_block(name, statuses.get(getter, {}), hint, rows)
|
||||
|
||||
|
||||
def _render_nous_gateway(ctx):
|
||||
from hermes_cli.status import _section, check_mark
|
||||
if managed_nous_tools_enabled():
|
||||
features = get_nous_subscription_features(ctx.config)
|
||||
_section("Nous Tool Gateway")
|
||||
print(" Nous Portal ✓ managed tools available" if features.nous_auth_present else " Nous Portal ✗ not logged in")
|
||||
_status._section("Nous Tool Gateway")
|
||||
print(" Nous Portal ✓ managed tools available" if features.nous_auth_present
|
||||
else " Nous Portal ✗ not logged in")
|
||||
for f in features.items():
|
||||
if f.managed_by_nous:
|
||||
state = "active via Nous subscription"
|
||||
@@ -179,11 +176,12 @@ def _render_nous_gateway(ctx):
|
||||
state = "available via subscription (optional)"
|
||||
else:
|
||||
state = "not configured"
|
||||
print(f" {f.label:<15} {check_mark(f.available or f.active or f.managed_by_nous)} {state}")
|
||||
mark = _status.check_mark(f.available or f.active or f.managed_by_nous)
|
||||
print(f" {f.label:<15} {mark} {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.
|
||||
_section("Nous Tool Gateway")
|
||||
_status._section("Nous Tool Gateway")
|
||||
message = format_nous_portal_entitlement_message(
|
||||
ctx.nous_account_info, capability="managed web, image, TTS, STT, browser, and Modal tools"
|
||||
)
|
||||
@@ -192,22 +190,27 @@ def _render_nous_gateway(ctx):
|
||||
|
||||
|
||||
def _render_apikey_providers(ctx):
|
||||
from hermes_cli.status import _effective_provider_label, _first_env_value, _section, check_mark, get_env_value
|
||||
_section("API-Key Providers")
|
||||
_status._section("API-Key Providers")
|
||||
for pname, env_vars in _APIKEY_PROVIDERS.items():
|
||||
configured = bool(_first_env_value(env_vars))
|
||||
print(f" {pname:<16} {check_mark(configured)} {'configured' if configured else 'not configured (run: hermes model)'}")
|
||||
configured = bool(_status._first_env_value(env_vars))
|
||||
state = "configured" if configured else "not configured (run: hermes model)"
|
||||
print(f" {pname:<16} {_status.check_mark(configured)} {state}")
|
||||
|
||||
# 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":
|
||||
if _status._effective_provider_label() == "LM Studio":
|
||||
from hermes_cli.models import probe_lmstudio_models
|
||||
model_cfg = ctx.config.get("model")
|
||||
base = (model_cfg.get("base_url") if isinstance(model_cfg, dict) else None) or get_env_value("LM_BASE_URL") or "http://127.0.0.1:1234/v1"
|
||||
base = ((model_cfg.get("base_url") if isinstance(model_cfg, dict) else None)
|
||||
or _status.get_env_value("LM_BASE_URL") or "http://127.0.0.1:1234/v1")
|
||||
try:
|
||||
models = probe_lmstudio_models(api_key=get_env_value("LM_API_KEY") or "", base_url=base, timeout=1.5)
|
||||
models = probe_lmstudio_models(api_key=_status.get_env_value("LM_API_KEY") or "",
|
||||
base_url=base, timeout=1.5)
|
||||
ok = models is not None
|
||||
msg = f"reachable ({len(models)} model(s)) at {base}" if ok else f"unreachable at {base}"
|
||||
except AuthError:
|
||||
ok, msg = False, "auth rejected — set LM_API_KEY"
|
||||
print(f" {'LM Studio':<16} {check_mark(ok)} {msg}")
|
||||
print(f" {'LM Studio':<16} {_status.check_mark(ok)} {msg}")
|
||||
|
||||
|
||||
import hermes_cli.status as _status # noqa: E402 (bottom: hermes_cli.status imports this module)
|
||||
|
||||
Reference in New Issue
Block a user