refactor(hermes_cli): replace per-function lazy 'from hermes_cli.setup/status import' with one bottom module alias

This commit is contained in:
Teknium
2026-09-02 21:16:32 -07:00
parent 5b44c23655
commit beeccf716c
4 changed files with 205 additions and 229 deletions
+32 -39
View File
@@ -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)
+83 -98
View File
@@ -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
View File
@@ -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
View File
@@ -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)