diff --git a/hermes_cli/setup_summary.py b/hermes_cli/setup_summary.py index 1674b04ac9..810a5064b0 100644 --- a/hermes_cli/setup_summary.py +++ b/hermes_cli/setup_summary.py @@ -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 ' '.""" - 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) diff --git a/hermes_cli/setup_terminal.py b/hermes_cli/setup_terminal.py index d811e81536..1fd88608f4 100644 --- a/hermes_cli/setup_terminal.py +++ b/hermes_cli/setup_terminal.py @@ -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) diff --git a/hermes_cli/setup_tts.py b/hermes_cli/setup_tts.py index 0f7416aba0..b4bce8e997 100644 --- a/hermes_cli/setup_tts.py +++ b/hermes_cli/setup_tts.py @@ -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) diff --git a/hermes_cli/status_auth.py b/hermes_cli/status_auth.py index 15ae06de79..25fdd222b6 100644 --- a/hermes_cli/status_auth.py +++ b/hermes_cli/status_auth.py @@ -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)