diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 28c5bd9a6..71bdaa39e 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -2821,6 +2821,7 @@ def _prompt_model_selection( pricing: Optional[Dict[str, Dict[str, str]]] = None, unavailable_models: Optional[List[str]] = None, portal_url: str = "", + allow_custom = True ) -> Optional[str]: """Interactive model selection. Puts current_model first with a marker. Returns chosen model ID or None. @@ -2909,8 +2910,16 @@ def _prompt_model_selection( from simple_term_menu import TerminalMenu choices = [f" {_label(mid)}" for mid in ordered] - choices.append(" Enter custom model name") - choices.append(" Skip (keep current)") + + custom_idx = None + if allow_custom: + custom_idx = len(choices) + choices.append(" Enter custom model name") + + skip_idx = None + if current_model: + skip_idx = len(choices) + choices.append(" Skip (keep current)") # Print the unavailable block BEFORE the menu via regular print(). # simple_term_menu pads title lines to terminal width (causes wrapping), @@ -2947,21 +2956,29 @@ def _prompt_model_selection( print() if idx < len(ordered): return ordered[idx] - elif idx == len(ordered): + if idx == custom_idx: custom = input("Enter model name: ").strip() return custom if custom else None + if idx == skip_idx: + return None return None except (ImportError, NotImplementedError, OSError, subprocess.SubprocessError): pass # Fallback: numbered list print(menu_title) - num_width = len(str(len(ordered) + 2)) + n = len(ordered) + extra = [] + if allow_custom: + extra.append("Enter custom model name") + if current_model: + extra.append("Skip (keep current)") + total = n + len(extra) + num_width = len(str(total)) for i, mid in enumerate(ordered, 1): print(f" {i:>{num_width}}. {_label(mid)}") - n = len(ordered) - print(f" {n + 1:>{num_width}}. Enter custom model name") - print(f" {n + 2:>{num_width}}. Skip (keep current)") + for j, label in enumerate(extra, n + 1): + print(f" {j:>{num_width}}. {label}") if _unavailable: _upgrade_url = (portal_url or DEFAULT_NOUS_PORTAL_URL).rstrip("/") @@ -2973,18 +2990,19 @@ def _prompt_model_selection( while True: try: - choice = input(f"Choice [1-{n + 2}] (default: skip): ").strip() + choice = input(f"Choice [1-{total}]: ").strip() if not choice: return None - idx = int(choice) - if 1 <= idx <= n: - return ordered[idx - 1] - elif idx == n + 1: - custom = input("Enter model name: ").strip() - return custom if custom else None - elif idx == n + 2: - return None - print(f"Please enter 1-{n + 2}") + val = int(choice) + if 1 <= val <= n: + return ordered[val - 1] + extra_idx = val - n - 1 + if 0 <= extra_idx < len(extra): + if extra[extra_idx] == "Enter custom model name": + custom = input("Enter model name: ").strip() + return custom if custom else None + return None # skip + print(f"Please enter 1-{total}") except ValueError: print("Please enter a number") except (KeyboardInterrupt, EOFError): @@ -3260,7 +3278,6 @@ def _nous_device_code_login( open_browser = False print(f"Starting Hermes login via {pconfig.name}...") - print(f"Portal: {portal_base_url}") if insecure: print("TLS verification: disabled (--insecure)") elif ca_bundle: @@ -3280,19 +3297,18 @@ def _nous_device_code_login( interval = int(device_data["interval"]) print() - print("To continue:") - print(f" 1. Open: {verification_url}") - print(f" 2. If prompted, enter code: {user_code}") - if open_browser: opened = webbrowser.open(verification_url) if opened: - print(" (Opened browser for verification)") + print("If you don't see a browser window open, navigate to this URL:") else: - print(" Could not open browser automatically — use the URL above.") + print("Navigate to this URL to continue:") + print(verification_url) + print(f"If you're prompted for a code, use {user_code}") + print() effective_interval = max(1, min(interval, DEVICE_AUTH_POLL_INTERVAL_CAP_SECONDS)) - print(f"Waiting for approval (polling every {effective_interval}s)...") + print(f"Waiting for approval (checking every {effective_interval}s)...") token_data = _poll_for_token( client=client, @@ -3357,7 +3373,7 @@ def _nous_device_code_login( raise -def _login_nous(args, pconfig: ProviderConfig) -> None: +def login_nous(args, pconfig: ProviderConfig) -> None: """Nous Portal device authorization flow.""" timeout_seconds = getattr(args, "timeout", None) or 15.0 insecure = bool(getattr(args, "insecure", False)) @@ -3419,7 +3435,10 @@ def _login_nous(args, pconfig: ProviderConfig) -> None: ) model_ids = _PROVIDER_MODELS.get("nous", []) + _portal = auth_state.get("portal_base_url", "") + print() + unavailable_models: list = [] if model_ids: pricing = get_pricing_for_provider("nous") @@ -3428,14 +3447,17 @@ def _login_nous(args, pconfig: ProviderConfig) -> None: model_ids, unavailable_models = partition_nous_models_by_tier( model_ids, pricing, free_tier=True, ) - _portal = auth_state.get("portal_base_url", "") - if model_ids: - print(f"Showing {len(model_ids)} curated models — use \"Enter custom model name\" for others.") - selected_model = _prompt_model_selection( - model_ids, pricing=pricing, - unavailable_models=unavailable_models, - portal_url=_portal, - ) + if not free_tier: + print(f"Showing {len(model_ids)} curated models — use \"Enter custom model name\" for others.") + if len(model_ids) > 1: + selected_model = _prompt_model_selection( + model_ids, pricing=pricing, + unavailable_models=unavailable_models, + portal_url=_portal, + allow_custom=not free_tier + ) + else: + selected_model = model_ids[0] elif unavailable_models: _url = (_portal or DEFAULT_NOUS_PORTAL_URL).rstrip("/") print("No free models currently available.") diff --git a/hermes_cli/main.py b/hermes_cli/main.py index d1b75d0a2..49991ef74 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -2124,7 +2124,7 @@ def _model_flow_nous(config, current_model="", args=None): resolve_nous_runtime_credentials, AuthError, format_auth_error, - _login_nous, + login_nous, PROVIDER_REGISTRY, ) from hermes_cli.config import ( @@ -2137,8 +2137,6 @@ def _model_flow_nous(config, current_model="", args=None): state = get_provider_auth_state("nous") if not state or not state.get("access_token"): - print("Not logged into Nous Portal. Starting login...") - print() try: mock_args = argparse.Namespace( portal_url=getattr(args, "portal_url", None), @@ -2150,7 +2148,7 @@ def _model_flow_nous(config, current_model="", args=None): ca_bundle=getattr(args, "ca_bundle", None), insecure=bool(getattr(args, "insecure", False)), ) - _login_nous(mock_args, PROVIDER_REGISTRY["nous"]) + login_nous(mock_args, PROVIDER_REGISTRY["nous"]) # Offer Tool Gateway enablement for paid subscribers try: _refreshed = load_config() or {} @@ -2201,7 +2199,7 @@ def _model_flow_nous(config, current_model="", args=None): ca_bundle=None, insecure=False, ) - _login_nous(mock_args, PROVIDER_REGISTRY["nous"]) + login_nous(mock_args, PROVIDER_REGISTRY["nous"]) except Exception as login_exc: print(f"Re-login failed: {login_exc}") return diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index ebc7de940..8cb0271f9 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -18,9 +18,10 @@ import shutil import sys import copy from pathlib import Path -from typing import Optional, Dict, Any +from typing import Literal, Optional, Dict, Any from hermes_cli.nous_subscription import get_nous_subscription_features +from hermes_cli.main import _model_flow_nous from tools.tool_backend_helpers import managed_nous_tools_enabled from utils import base_url_hostname from hermes_constants import get_optional_skills_dir @@ -655,7 +656,7 @@ def _prompt_container_resources(config: dict): -def setup_model_provider(config: dict, *, quick: bool = False): +def setup_model_provider(config: dict, *, quick: bool | Literal["nous_portal"] = False): """Configure the inference provider and default model. Delegates to ``cmd_model()`` (the same flow used by ``hermes model``) @@ -677,7 +678,11 @@ def setup_model_provider(config: dict, *, quick: bool = False): # credential prompting, model selection, and config persistence. from hermes_cli.main import select_provider_and_model try: - select_provider_and_model() + if quick == "nous_portal": + config = load_config() + _model_flow_nous(config) + else: + select_provider_and_model() except (SystemExit, KeyboardInterrupt): print() print_info("Provider setup skipped.") @@ -3030,11 +3035,15 @@ def run_setup_wizard(args): config = load_config() setup_mode = prompt_choice("How would you like to set up Hermes?", [ - "Quick setup — provider, model & messaging (recommended)", + "Nous Account setup — model & messaging (recommended)", + "Quick setup — provider, model & messaging", "Full setup — configure everything", ], 0) if setup_mode == 0: + _run_first_time_quick_setup(config, hermes_home, is_existing, nous_quick=True) + return + if setup_mode == 1: _run_first_time_quick_setup(config, hermes_home, is_existing) return @@ -3095,7 +3104,7 @@ def _resolve_hermes_chat_argv() -> Optional[list[str]]: return None -def _offer_launch_chat(): +def _offer_launch_chat(auto_launch = False): """Prompt the user to jump straight into chat after setup.""" print() if not prompt_yes_no("Launch hermes chat now?", True): @@ -3109,7 +3118,7 @@ def _offer_launch_chat(): os.execvp(chat_argv[0], chat_argv) -def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool): +def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool, nous_quick=False): """Streamlined first-time setup: provider + model only. Applies sensible defaults for TTS (Edge), terminal (local), agent @@ -3117,7 +3126,7 @@ def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool): ``hermes setup
``. """ # Step 1: Model & Provider (essential — skips rotation/vision/TTS) - setup_model_provider(config, quick=True) + setup_model_provider(config, quick="nous_portal" if nous_quick else True ) # Step 2: Apply defaults for everything else _apply_default_agent_settings(config) @@ -3150,7 +3159,9 @@ def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool): _print_setup_summary(config, hermes_home) - _offer_launch_chat() + # if the user hasn't set up the gateway, assume they want to launch chat. + force_launch_chat = gateway_choice == 0 + _offer_launch_chat(force_launch_chat) def _run_quick_setup(config: dict, hermes_home): diff --git a/tests/cli/test_cli_provider_resolution.py b/tests/cli/test_cli_provider_resolution.py index 0c9aab82a..babfedf45 100644 --- a/tests/cli/test_cli_provider_resolution.py +++ b/tests/cli/test_cli_provider_resolution.py @@ -571,7 +571,7 @@ def test_cmd_model_forwards_nous_login_tls_options(monkeypatch): captured["ca_bundle"] = login_args.ca_bundle captured["insecure"] = login_args.insecure - monkeypatch.setattr("hermes_cli.auth._login_nous", _fake_login) + monkeypatch.setattr("hermes_cli.auth.login_nous", _fake_login) hermes_main.cmd_model( SimpleNamespace(