diff --git a/hermes_cli/tools_config_cua.py b/hermes_cli/tools_config_cua.py index 95e9e694de..1db5ac6dae 100644 --- a/hermes_cli/tools_config_cua.py +++ b/hermes_cli/tools_config_cua.py @@ -49,11 +49,9 @@ def _run_text(cmd: list, *, timeout, capture_output: bool = True, **kwargs) -> s def _post_setup_no_window_flags(*, streams_to_console: bool = False) -> int: """Win32 creationflags that stop post-setup children flashing a console. - CREATE_NO_WINDOW hides console grandchildren (npm, pip, powershell) while keeping stdio inheritable (unlike DETACHED_PROCESS). Returns 0 on POSIX. ``streams_to_console`` children are only hidden when - our own stdout is not a console, so live installer output is never swallowed. - """ + our own stdout is not a console, so live installer output is never swallowed.""" from hermes_cli._subprocess_compat import windows_hide_flags flags = windows_hide_flags() @@ -136,11 +134,10 @@ def _cua_driver_install_ready() -> bool: def _pip_install(args: List[str], *, timeout: int = 300, capture_output: bool = True): """Install Python packages from a post-setup hook. - Order: ``uv pip install`` (fast, needs no pip in the venv), then ``python -m pip install``, then ``python -m ensurepip --upgrade`` and retry pip. The last tier exists because the Windows installer - creates the venv via ``uv venv``, which does NOT seed pip, so bare ``-m pip`` failed on fresh installs. - """ + creates the venv via ``uv venv``, which does NOT seed pip, so bare ``-m pip`` failed on fresh + installs.""" venv_root = Path(sys.executable).parent.parent uv_env = {**os.environ, "VIRTUAL_ENV": str(venv_root)} install_flags = _post_setup_no_window_flags(streams_to_console=not capture_output) @@ -207,12 +204,10 @@ def _cua_driver_version(binary: str) -> Optional[str]: def _confirmed_update_check(driver_cmd: str, require_confirmed_update: bool) -> tuple: """Ask the installed driver whether a newer release exists; returns ``(proceed, pin_version)``. - ``proceed=False`` means stop with success (already latest, or indeterminate under ``require_confirmed_update``). An old driver (no check-update verb) or offline check yields None: `hermes update` then keeps the installed version — an indeterminate check must never cost a - multi-minute silent reinstall on every update — while explicit `install --upgrade` falls through. - """ + multi-minute silent reinstall on every update — while explicit `install --upgrade` falls through.""" try: from tools.computer_use.cua_backend import cua_driver_update_check _state = cua_driver_update_check() @@ -248,11 +243,9 @@ def install_cua_driver( upgrade: bool = False, require_confirmed_update: bool = False, show_installer_progress: bool = True, ) -> bool: """Install or refresh the cua-driver binary used by Computer Use. - The upstream installer always pulls the latest release tag, so re-running it is the canonical way to upgrade. ``upgrade=False`` (toolset enable flow) keeps a compatible installation, repairs an - old/incomplete one and installs when missing; ``upgrade=True`` always refreshes. - """ + old/incomplete one and installs when missing; ``upgrade=True`` always refreshes.""" import platform as _plat system = _plat.system() @@ -385,11 +378,9 @@ def _cua_windows_install_lock_file() -> "Path": def _clear_stale_windows_cua_install_lock() -> None: """Delete install.ps1's lock file only when no process still holds it. - install.ps1 locks with ``FileShare::None``; mirror it with a zero-share ``CreateFileW`` probe and ``FILE_FLAG_DELETE_ON_CLOSE`` so an unlocked leftover is removed atomically, with no window in which - a new installer could acquire the file between probe and delete. - """ + a new installer could acquire the file between probe and delete.""" lock_file = _cua_windows_install_lock_file() try: if not lock_file.is_file(): @@ -437,11 +428,9 @@ def _clear_stale_windows_cua_install_lock() -> None: def _clear_stale_cua_install_lock() -> None: """Best-effort: remove a stale installer lock left by a dead holder. - POSIX stamps the holder pid into ``~/.cua-driver/packages/.install.lock.d/info``; Windows holds ``~/.cua-driver/install.lock`` open with ``FileShare::None``. Clear either artifact up front only - when its platform-specific liveness check proves that no install still holds it. - """ + when its platform-specific liveness check proves that no install still holds it.""" if sys.platform == "win32": _clear_stale_windows_cua_install_lock() return @@ -485,10 +474,8 @@ def _clear_stale_cua_install_lock() -> None: def _cua_install_lock_held() -> bool: """True when the upstream installer's lock is held by a LIVE process. - Called after ``_clear_stale_cua_install_lock()``: anything provably stale is already gone, so a - surviving lock artifact means a concurrent (or orphaned-but-alive) install owns it. - """ + surviving lock artifact means a concurrent (or orphaned-but-alive) install owns it.""" try: if sys.platform != "win32": return _cua_install_lock_dir().is_dir() @@ -509,11 +496,9 @@ def _cua_install_lock_held() -> bool: def _cua_release_endpoint_reachable(timeout: float = 5.0) -> bool: """Fast probe: can we reach GitHub's release download host at all? - - When github.com is down the installer dies slowly inside its own retries and eats the whole unattended - ceiling; a 5s HEAD decides in seconds. Only a connection-level failure counts as unreachable — any - HTTP response (even 4xx/5xx) proves the path works. - """ + When github.com is down the installer dies slowly inside its own retries and eats the whole + unattended ceiling; a 5s HEAD decides in seconds. Only a connection-level failure counts as + unreachable — any HTTP response (even 4xx/5xx) proves the path works.""" import urllib.error import urllib.request @@ -548,11 +533,9 @@ def _cua_driver_autostart_registered_windows() -> bool: def _repair_cua_driver_autostart_windows(driver_cmd: str, *, verbose: bool) -> bool: """Best-effort repair for Windows installer autostart quoting failures. - - Older install.ps1 builds interpolated the binary path into a PowerShell command string, which split at - the first space. If the scheduled task is missing, retry via Start-Process's structured ``-FilePath`` / - ``-ArgumentList`` parameters instead. - """ + Older install.ps1 builds interpolated the binary path into a PowerShell command string, which split + at the first space. If the scheduled task is missing, retry via Start-Process's structured + ``-FilePath`` / ``-ArgumentList`` parameters instead.""" if sys.platform != "win32" or _cua_driver_autostart_registered_windows(): return True @@ -654,10 +637,8 @@ def _kill_installer_tree(proc, *, is_windows: bool) -> None: def _reap_after_timeout(proc, *, is_windows: bool) -> None: """Kill the installer tree, then drain its pipes under a deadline. - An unbounded drain blocks on an EOF that only arrives when someone kills a surviving descendant by - hand, so ``_CUA_INSTALLER_TIMEOUT`` would stop bounding anything. - """ + hand, so ``_CUA_INSTALLER_TIMEOUT`` would stop bounding anything.""" _kill_installer_tree(proc, is_windows=is_windows) try: drained_out, _ = proc.communicate(timeout=_CUA_INSTALLER_DRAIN_GRACE) @@ -705,13 +686,11 @@ def _cua_installer_command(is_windows: bool): def _unattended_installer_preflight(install_cmd: list, is_windows: bool): """Fail FAST on the two conditions that otherwise consume the whole unattended ceiling. - - 1. Install lock held by a live process — upstream would poll it for up to LOCK_STALE_AFTER_SECONDS=600 - before probing the holder (the 11-minute silent hang class). - 2. Release host unreachable — the installer would die slowly inside its own retries; a 5s HEAD answers. - Returns the (possibly rewritten) install command, or None to skip this refresh. Explicit - `computer-use install --upgrade` runs never come here and keep upstream's full lock-recovery. - """ + 1. Install lock held by a live process — upstream would poll it for up to + LOCK_STALE_AFTER_SECONDS=600 before probing the holder (the 11-minute silent hang class). + 2. Release host unreachable — the installer would die slowly inside its own retries; a 5s HEAD + answers. Returns the (possibly rewritten) install command, or None to skip this refresh. Explicit + `computer-use install --upgrade` runs never come here and keep upstream's full lock-recovery.""" if _cua_install_lock_held(): _print_info(" Another cua-driver install is in progress (upstream install lock is held) — skipping this refresh.") _print_info(f" If no install is really running, retry with: {_UPGRADE_CMD}") @@ -734,11 +713,9 @@ def _run_cua_driver_installer( label: str = "Installing", verbose: bool = True, pin_version: Optional[str] = None, show_progress: bool = True, installer_timeout: Optional[float] = None) -> bool: """Run the upstream cua-driver installer for this platform. - The scripts are idempotent: they always download the latest release, so re-running on an already-installed system performs an upgrade. ``installer_timeout`` lets quiet callers use a shorter - ceiling without weakening the explicit install path's stale-lock recovery window. - """ + ceiling without weakening the explicit install path's stale-lock recovery window.""" import platform as _plat system = _plat.system() diff --git a/hermes_cli/tools_config_mcp.py b/hermes_cli/tools_config_mcp.py index c37b31c997..dc11920dc5 100644 --- a/hermes_cli/tools_config_mcp.py +++ b/hermes_cli/tools_config_mcp.py @@ -14,11 +14,9 @@ from hermes_cli.toolset_scope import ( def _mcp_match_filter(): """Runtime name-filter matcher (exact names or fnmatch globs), with a literal fallback. - - Must use the SAME semantics as tools/mcp_tool.py registration — a literal ``in`` check renders - glob excludes (e.g. ``*team_member*`` from catalog default_excluded manifests) as if nothing - were excluded. - """ + Must use the SAME semantics as tools/mcp_tool.py registration — a literal ``in`` check renders glob + excludes (e.g. ``*team_member*`` from catalog default_excluded manifests) as if nothing were + excluded.""" try: from tools.mcp_tool import matches_name_filter return matches_name_filter diff --git a/hermes_cli/tools_config_post_setup.py b/hermes_cli/tools_config_post_setup.py index 2559b24235..2175db42e3 100644 --- a/hermes_cli/tools_config_post_setup.py +++ b/hermes_cli/tools_config_post_setup.py @@ -25,11 +25,9 @@ PROJECT_ROOT = Path(__file__).parent.parent.resolve() def _ensure_browser_use_cli(*, verbose_hints: bool = False) -> None: """Install the Browser Use CLI if it isn't already runnable. - Primary driver engine for EVERY browser backend except Camofox (Firefox-based, no CDP surface). MANAGED-FIRST: a browser-use on the user's PATH does NOT satisfy this check — only the - Hermes-managed ``$HERMES_HOME/bin`` copy does. - """ + Hermes-managed ``$HERMES_HOME/bin`` copy does.""" _print_info(" Ensuring browser-use CLI (managed install)...") try: from tools.browser_use_cli import install_cli @@ -86,10 +84,8 @@ def _install_chromium(install_cmd: list[str]) -> None: def _post_setup_agent_browser(post_setup_key: str) -> None: """``agent_browser`` (local Chromium) and ``browserbase`` (cloud rows) hooks. - agent-browser is not a root package.json dependency — it resolves lazily via npx (or a - global/Hermes-managed install), so there is no ``npm install`` step here. - """ + global/Hermes-managed install), so there is no ``npm install`` step here.""" # Every non-Camofox backend drives through the Browser Use CLI — install it here too. _ensure_browser_use_cli() try: @@ -286,10 +282,9 @@ def _post_setup_langfuse() -> None: def _post_setup_xai_grok() -> None: - """Shared xAI credential bootstrap for any picker row that talks to xAI (TTS, STT, Video Gen, - x_search …). Accepts a SuperGrok-tier OAuth token (preferred — billed to the existing - subscription) or a raw XAI_API_KEY; the rows declare empty env_vars so the auth UX lives here. - """ + """Shared xAI credential bootstrap for any picker row that talks to xAI (TTS, STT, Video Gen, x_search + …). Accepts a SuperGrok-tier OAuth token (preferred — billed to the existing subscription) or a raw + XAI_API_KEY; the rows declare empty env_vars so the auth UX lives here.""" try: from hermes_cli.auth import get_xai_oauth_auth_status oauth_logged_in = bool(get_xai_oauth_auth_status().get("logged_in")) @@ -361,8 +356,7 @@ def _run_post_setup(post_setup_key: str): def valid_post_setup_keys() -> Set[str]: """Return the set of post-setup keys declared by any visible provider (``TOOL_CATEGORIES`` plus plugin-registered providers). This is the allowlist ``post-setup`` and the dashboard endpoint - validate against, so a caller cannot drive ``_run_post_setup`` with an arbitrary key. - """ + validate against, so a caller cannot drive ``_run_post_setup`` with an arbitrary key.""" from hermes_cli.tools_config import ( TOOL_CATEGORIES, _plugin_browser_providers, @@ -384,9 +378,8 @@ def valid_post_setup_keys() -> Set[str]: def run_post_setup_command(args) -> int: - """``hermes tools post-setup `` — non-interactive runner the dashboard spawns so the GUI can - drive backend setup without re-implementing install logic. Exit code: 0 ok, 2 unknown key. - """ + """``hermes tools post-setup `` — non-interactive runner the dashboard spawns so the GUI can drive + backend setup without re-implementing install logic. Exit code: 0 ok, 2 unknown key.""" key = getattr(args, "post_setup_key", None) if not key: _print_error("Usage: hermes tools post-setup ") @@ -458,10 +451,9 @@ def restorable_python_tool_dependency(name: str) -> tuple[str, tuple[str, ...]] def _agent_browser_installed() -> bool: - """True when everything ``_run_post_setup("agent_browser")`` installs is present: the agent-browser - CLI *and* the Chromium build it drives (or the Lightpanda engine, which needs no Chromium), so - "Run setup" flips to installed only when re-running it would be a no-op. - """ + """True when everything ``_run_post_setup("agent_browser")`` installs is present: the agent-browser CLI + *and* the Chromium build it drives (or the Lightpanda engine, which needs no Chromium), so "Run + setup" flips to installed only when re-running it would be a no-op.""" from hermes_cli.nous_subscription import _local_browser_runnable # The hook runs in a spawned process; this probe runs in the long-lived web-server/CLI process whose diff --git a/hermes_cli/tools_config_providers.py b/hermes_cli/tools_config_providers.py index 319cdaf18b..30306d4378 100644 --- a/hermes_cli/tools_config_providers.py +++ b/hermes_cli/tools_config_providers.py @@ -39,11 +39,10 @@ def _plugin_provider_rows( registry_module: str, marker_keys: tuple[str, ...], *, require_name: bool = True, skip_builtin: bool = False, flatten_variants: bool = False) -> list[dict]: """Picker-row dicts (TOOL_CATEGORIES-shaped) for a plugin registry's providers. - - ``marker_keys`` are all set to the registry name so downstream config/model-picker code routes through - the registry. ``skip_builtin`` drops names shadowing ``_BUILTIN_NAMES``; ``flatten_variants`` expands a - schema's tier ``variants`` into separate rows sharing one backend name, distinguished by ``web_tier``. - """ + ``marker_keys`` are all set to the registry name so downstream config/model-picker code routes + through the registry. ``skip_builtin`` drops names shadowing ``_BUILTIN_NAMES``; + ``flatten_variants`` expands a schema's tier ``variants`` into separate rows sharing one backend + name, distinguished by ``web_tier``.""" registry = _plugin_registry(registry_module) if registry is None: return [] @@ -109,10 +108,8 @@ _plugin_tts_providers = partial(_plugin_rows_for, "tts") def web_provider_capabilities(backend: str) -> list: """Capabilities (``search`` / ``extract``) a web backend supports, per the registry provider instance. - - Lets the Capabilities GUI offer ``web.search_backend`` / ``web.extract_backend`` only where it makes sense - (ddgs and brave-free are search-only). Unknown backend or registry failure -> both. - """ + Lets the Capabilities GUI offer ``web.search_backend`` / ``web.extract_backend`` only where it makes + sense (ddgs and brave-free are search-only). Unknown backend or registry failure -> both.""" try: from agent.web_search_registry import get_provider @@ -138,10 +135,8 @@ def _visible_providers( cat: dict, config: dict, *, force_fresh: bool = False, features: Optional[NousSubscriptionFeatures] = None, ) -> list[dict]: """Provider entries visible for the current auth/config state. - - Nous-managed rows (``managed_nous_feature``) are always shown, even logged-out/unentitled, to advertise - the capability. - """ + Nous-managed rows (``managed_nous_feature``) are always shown, even logged-out/unentitled, to + advertise the capability.""" from hermes_cli.tools_config import get_nous_subscription_features if features is None: @@ -170,10 +165,8 @@ def _visible_providers( def provider_readiness_status(provider: dict, config: dict, *, features=None, is_active: Optional[bool] = None) -> str: """Honest readiness state for a provider picker row. - - ``features`` avoids re-fetching portal state per row. ``is_active`` is the completed-setup fallback for - post_setup hooks with no registered installed-check (selecting a row runs its hook). - """ + ``features`` avoids re-fetching portal state per row. ``is_active`` is the completed-setup fallback + for post_setup hooks with no registered installed-check (selecting a row runs its hook).""" from hermes_cli.tools_config import ( _POST_SETUP_READY, _provider_env_ready, _xai_credentials_present, get_nous_subscription_features, ) @@ -262,10 +255,8 @@ def _any_plugin_provider_available(registry_module: str) -> bool: def _configure_tool_category(ts_key: str, cat: dict, config: dict, *, force_fresh: bool = True, reconfigure: bool = False): """Provider selection for a tool category, then API-key setup for the chosen row. - - ``reconfigure`` ("Reconfigure an existing tool"): no setup note / skip row / Nous marker, and the chosen - provider goes through the key-update prompts instead of the new-enable prompts. - """ + ``reconfigure`` ("Reconfigure an existing tool"): no setup note / skip row / Nous marker, and the + chosen provider goes through the key-update prompts instead of the new-enable prompts.""" from hermes_cli.tools_config import _prompt_choice, _provider_env_ready, get_nous_subscription_features icon = cat.get("icon", "") @@ -329,11 +320,10 @@ def _configure_tool_category(ts_key: str, cat: dict, config: dict, *, force_fres def _web_tier_matches(provider: dict, config: dict) -> bool: """True when a web picker row's tier matches the configured tier (``web.provider_tier.``). - - Tiered rows (Exa/Parallel Free vs Paid) share one ``web_backend`` and differ only in ``web_tier``. No - ``web_tier`` on the row → matches; configured tier set → must equal the row's tier; unset → "auto": - paid when the row's key is present, free otherwise (highlight the row the runtime would actually use). - """ + Tiered rows (Exa/Parallel Free vs Paid) share one ``web_backend`` and differ only in ``web_tier``. + No ``web_tier`` on the row → matches; configured tier set → must equal the row's tier; unset → + "auto": paid when the row's key is present, free otherwise (highlight the row the runtime would + actually use).""" row_tier = provider.get("web_tier") if not row_tier: return True @@ -547,11 +537,10 @@ def _pick_model_from_catalog( catalog: dict, default_model, cfg_key: str, display: str, config: dict, *, row_indent: str = "", ) -> None: """Column-aligned model picker shared by the FAL, plugin image gen and video gen flows. - - Writes the choice to ``config[cfg_key]["model"]``. The current model is listed first so the cursor lands - on it; a saved model belonging to another provider (shared config key) or a drifted catalog default never - indexes the catalog. Safe when stdin is not a TTY — curses_radiolist keeps the current selection. - """ + Writes the choice to ``config[cfg_key]["model"]``. The current model is listed first so the cursor + lands on it; a saved model belonging to another provider (shared config key) or a drifted catalog + default never indexes the catalog. Safe when stdin is not a TTY — curses_radiolist keeps the current + selection.""" from hermes_cli.tools_config import _cfg_section, _prompt_choice if not catalog: @@ -633,10 +622,9 @@ def _configure_xai_imagine_storage(section_name: str, config: dict) -> None: def _select_plugin_gen_provider(section: str, plugin_name: str, config: dict, *, use_gateway: bool = False) -> None: """Persist a plugin-backed image/video gen provider selection and run its model picker. - - ``use_gateway=True`` (Nous-managed pick) stores ``
.provider: nous``; BYOK picks store the plugin - name. Any legacy ``use_gateway`` key is removed so old read-time shims cannot override the selection. - """ + ``use_gateway=True`` (Nous-managed pick) stores ``
.provider: nous``; BYOK picks store the + plugin name. Any legacy ``use_gateway`` key is removed so old read-time shims cannot override the + selection.""" from hermes_cli.tools_config import _cfg_section cfg = _cfg_section(config, section) @@ -698,12 +686,10 @@ def _drop_use_gateway(section) -> None: def _write_provider_config(provider: dict, config: dict, *, managed_feature) -> None: """Persist the provider/backend config keys for a selected provider. - - Pure, non-interactive core of :func:`_configure_provider` (no env prompts, post-setup hooks, Nous auth - gating or model pickers) shared by the CLI and the GUI ``PUT .../provider`` endpoint. Each pick writes - exactly ONE provider string per category (``nous`` for managed rows) and removes any legacy - ``use_gateway`` key so the read-time shim cannot override the new choice. - """ + Pure, non-interactive core of :func:`_configure_provider` (no env prompts, post-setup hooks, Nous + auth gating or model pickers) shared by the CLI and the GUI ``PUT .../provider`` endpoint. Each pick + writes exactly ONE provider string per category (``nous`` for managed rows) and removes any legacy + ``use_gateway`` key so the read-time shim cannot override the new choice.""" from hermes_cli.tools_config import TOOL_CATEGORIES, _cfg_section def _set_selection(section_key: str, name_key: str, vendor_value) -> None: @@ -797,11 +783,10 @@ def apply_provider_selection(ts_key: str, provider_name: str, config: dict) -> N def _nous_provider_gate(provider: dict, config: dict, managed_feature, *, force_fresh: bool) -> bool: """Return False (after printing why) when a Nous-gated row cannot be selected. - - Managed Tool Gateway rows are always listed but only *activate* with paid Nous Portal access — selecting - one runs an inline Portal login (auth + entitlement only, no inference-provider switch). Pure pre-auth UX - rows (``requires_nous_auth`` without a managed feature) keep the older logged-in + entitled gate. - """ + Managed Tool Gateway rows are always listed but only *activate* with paid Nous Portal access — + selecting one runs an inline Portal login (auth + entitlement only, no inference-provider switch). + Pure pre-auth UX rows (``requires_nous_auth`` without a managed feature) keep the older logged-in + + entitled gate.""" from hermes_cli.tools_config import get_nous_subscription_features if managed_feature: @@ -896,11 +881,9 @@ def _prompt_secret( key: str, label: str, url: str, default_val: str, *, reconfigure: bool, url_label: str, strip: bool = False, ) -> bool: """One env-var prompt; True unless the new-enable flow skipped the key. - - Reconfigure mode shows the current value and re-prompts ("Enter to keep current"); the new-enable flow - prompts with ``default_val`` visible when one exists, else as a password, and ``strip`` decides whether - whitespace is trimmed (and whitespace-only counts as skipped). - """ + Reconfigure mode shows the current value and re-prompts ("Enter to keep current"); the new-enable + flow prompts with ``default_val`` visible when one exists, else as a password, and ``strip`` decides + whether whitespace is trimmed (and whitespace-only counts as skipped).""" if reconfigure: existing = get_env_value(key) if existing: @@ -928,10 +911,8 @@ def _prompt_secret( def _prompt_env_vars(env_vars: list, *, reconfigure: bool) -> bool: """Prompt for a provider's env vars; True when every key ended up configured. - Reconfigure mode re-prompts every key and always returns True; the new-enable flow keeps already-set - keys without asking and reports False on any skipped key. - """ + keys without asking and reports False on any skipped key.""" all_configured = True for var in env_vars: if not reconfigure and get_env_value(var["key"]): @@ -945,10 +926,9 @@ def _prompt_env_vars(env_vars: list, *, reconfigure: bool) -> bool: def _configure_provider(provider: dict, config: dict, *, force_fresh: bool = True, reconfigure: bool = False): """Configure a single provider - prompt for API keys and set config. - - ``reconfigure=False`` (new-enable): already-set keys are kept without asking and the post-setup hook only - runs when every key was provided. ``reconfigure=True`` re-prompts every key and always runs the hook. - """ + ``reconfigure=False`` (new-enable): already-set keys are kept without asking and the post-setup hook + only runs when every key was provided. ``reconfigure=True`` re-prompts every key and always runs the + hook.""" from hermes_cli.tools_config import _run_post_setup env_vars = provider.get("env_vars", []) @@ -989,10 +969,9 @@ def _reconfigure_provider(provider: dict, config: dict, *, force_fresh: bool = T def _configure_vision_backend() -> None: """Interactive vision-backend configuration (``auxiliary.vision.{provider,model,base_url}``). - - Offers any authenticated provider + model (same surface as ``hermes model``) or a custom endpoint rather - than forcing OpenRouter. "Auto" leaves the keys empty so the resolver uses the main-model fallback chain. - """ + Offers any authenticated provider + model (same surface as ``hermes model``) or a custom endpoint + rather than forcing OpenRouter. "Auto" leaves the keys empty so the resolver uses the main-model + fallback chain.""" from hermes_cli.tools_config import _cfg_section, _prompt_choice print() @@ -1045,11 +1024,9 @@ def _configure_vision_backend() -> None: def _configure_vision_provider_model(config: dict, vision_cfg: dict) -> None: """Provider + model picker for vision, mirroring the ``/model`` surface. - - Rows come from ``build_aux_picker_rows()`` so this lists exactly what the ``hermes model`` aux-task picker - lists, including user-defined ``providers:`` / ``custom_providers:`` endpoints. Persists - ``auxiliary.vision.provider`` + ``.model``. - """ + Rows come from ``build_aux_picker_rows()`` so this lists exactly what the ``hermes model`` aux-task + picker lists, including user-defined ``providers:`` / ``custom_providers:`` endpoints. Persists + ``auxiliary.vision.provider`` + ``.model``.""" from hermes_cli.tools_config import _prompt_choice try: @@ -1108,10 +1085,8 @@ def _configure_vision_provider_model(config: dict, vision_cfg: dict) -> None: def _configure_simple_requirements(ts_key: str, *, reconfigure: bool = False): """Fallback for toolsets that just need env vars (no provider selection). - Vision has its own provider/model picker — run it directly so neither flow falls back to the generic - single-key prompt (which would re-ask for OPENROUTER_API_KEY). - """ + single-key prompt (which would re-ask for OPENROUTER_API_KEY).""" from hermes_cli.tools_config import TOOLSET_ENV_REQUIREMENTS, _toolset_has_keys, _toolset_label if ts_key == "vision": diff --git a/hermes_cli/toolset_validation.py b/hermes_cli/toolset_validation.py index 6b8a25905f..72cbc3a6b9 100644 --- a/hermes_cli/toolset_validation.py +++ b/hermes_cli/toolset_validation.py @@ -33,14 +33,11 @@ def validate_platform_toolsets( is_allowed_for_platform: Callable[[str, str], bool] = toolset_allowed_for_platform, ) -> List[str]: """Return human-readable warnings for a ``platform_toolsets`` mapping. - - Reports: a toolset name ``is_valid_toolset`` rejects (suggesting ``hermes-`` when - that would have been valid); a non-empty mapping resolving to zero valid toolsets (agent - would start with no tools); a platform with no valid toolsets, checked per-platform because - the global net is suppressed once any platform is valid; and non-list platform values, which - fall back to the platform default. ``is_valid_toolset`` is injected so this does no registry - imports or I/O. - """ + Reports: a toolset name ``is_valid_toolset`` rejects (suggesting ``hermes-`` when that + would have been valid); a non-empty mapping resolving to zero valid toolsets (agent would start with + no tools); a platform with no valid toolsets, checked per-platform because the global net is + suppressed once any platform is valid; and non-list platform values, which fall back to the platform + default. ``is_valid_toolset`` is injected so this does no registry imports or I/O.""" warnings: List[str] = [] if not isinstance(platform_toolsets, dict) or not platform_toolsets: return warnings