diff --git a/hermes_cli/main.py b/hermes_cli/main.py index ae83d7546..57dbaaf7c 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -12681,9 +12681,9 @@ def main(): setup_parser.add_argument( "--portal", action="store_true", - help="One-shot Nous Portal setup: log in via OAuth, set Nous as the " - "inference provider, and opt into the Tool Gateway. Skips the " - "rest of the wizard.", + help="One-shot Nous Portal setup: log in via OAuth, pick a Nous " + "model, set Nous as the inference provider, and opt into the Tool " + "Gateway. Skips the rest of the wizard.", ) setup_parser.set_defaults(func=cmd_setup) diff --git a/hermes_cli/portal_cli.py b/hermes_cli/portal_cli.py index 5f792787a..622eb4736 100644 --- a/hermes_cli/portal_cli.py +++ b/hermes_cli/portal_cli.py @@ -1,10 +1,11 @@ """``hermes portal`` — the human-readable entry point for Nous Portal. Running ``hermes portal`` with no subcommand performs the one-shot Portal -onboarding: OAuth login, switch the inference provider to Nous, and offer to -enable the Tool Gateway. It is the friendly alias for -``hermes auth add nous --type oauth`` (which still works) and is identical to -``hermes setup --portal``. +onboarding: OAuth login, pick a Nous model, switch the inference provider to +Nous, and offer to enable the Tool Gateway. It is the friendly alias for +``hermes auth add nous --type oauth`` (which still works), is identical to +``hermes setup --portal``, and runs the same Nous flow as the first-time quick +setup. Subcommands: (none) Log in to Nous Portal + set it up (one-shot onboarding). @@ -167,12 +168,13 @@ def _cmd_tools(args) -> int: def _cmd_login(args) -> int: - """Run the one-shot Nous Portal onboarding (OAuth + provider + Tool Gateway). + """Run the one-shot Nous Portal onboarding (login + model + provider + tools). This is the human-readable front door for `hermes auth add nous --type - oauth`. It reuses the exact wiring behind `hermes setup --portal` so the - two commands stay in lockstep: device-code login, switch the inference - provider to Nous, then offer the Tool Gateway opt-in. + oauth`. It reuses the exact wiring behind `hermes setup --portal` (which in + turn runs the same Nous flow as the first-time quick setup), so the + commands stay in lockstep: device-code login, pick a Nous model, switch the + inference provider to Nous, then offer the Tool Gateway opt-in. """ from hermes_cli.setup import _run_portal_one_shot @@ -210,12 +212,13 @@ def add_parser(subparsers) -> None: """Register `hermes portal` on the given argparse subparsers object.""" portal_parser = subparsers.add_parser( "portal", - help="Set up Nous Portal (OAuth login + Tool Gateway); see also `portal info`", + help="Set up Nous Portal (login, model pick, Tool Gateway); see also `portal info`", description=( "Run `hermes portal` with no subcommand to log in to Nous Portal " - "and set it up (the human-readable alias for `hermes auth add nous " - "--type oauth`, identical to `hermes setup --portal`). Subcommands: " - "login (default), info, open, tools." + "and set it up — pick a model, set Nous as your provider, and offer " + "the Tool Gateway (the human-readable alias for `hermes auth add " + "nous --type oauth`, identical to `hermes setup --portal`). " + "Subcommands: login (default), info, open, tools." ), ) portal_sub = portal_parser.add_subparsers(dest="portal_command") diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 573566408..8930d543a 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -2721,20 +2721,23 @@ SETUP_SECTIONS = [ def _run_portal_one_shot(config: dict) -> None: - """One-shot Nous Portal setup — OAuth + provider switch + Tool Gateway. + """One-shot Nous Portal setup — OAuth + model pick + provider + Tool Gateway. - Wired into ``hermes setup --portal``. Does NOT prompt for anything - besides what the underlying OAuth + Tool Gateway prompts already need. - Designed to be shareable as a single command (``hermes setup --portal``) - that gets a brand-new user from zero to a fully working Hermes session - with web/image/tts/browser tools all routed via their Portal sub. + Wired into ``hermes setup --portal`` and ``hermes portal``. This is the + Nous-Portal slice of the first-time quick setup, collapsed into a single + shareable command so a brand-new user goes from zero to a fully working + Hermes session — model selected, provider set, and web/image/tts/browser + tools routed via their Portal sub — without being told to run + ``hermes setup`` and hunt for the quick-setup option. + + The login + model selection + provider switch + Tool Gateway opt-in are all + delegated to ``_model_flow_nous`` — the exact same flow quick setup uses + (``_run_first_time_quick_setup``) and the same one ``hermes model`` runs + when you pick Nous. Routing through it (instead of hand-rolling the auth + + provider write here) means ``hermes portal`` always offers a model picker, + and there is a single source of truth for the Nous onboarding steps. """ - from types import SimpleNamespace - - from hermes_cli.auth_commands import auth_add_command - from hermes_cli.config import save_config - from hermes_cli.auth import get_nous_auth_status - from hermes_cli.nous_subscription import prompt_enable_tool_gateway + from hermes_cli.config import load_config print() print( @@ -2758,73 +2761,42 @@ def _run_portal_one_shot(config: dict) -> None: print_info(" Sign up: https://portal.nousresearch.com/manage-subscription") print() - # Skip OAuth if already logged in (don't re-prompt every time the user - # runs `hermes setup --portal` after a successful first run). - already_logged_in = False + # _model_flow_nous handles BOTH the logged-out path (device-code OAuth, + # which selects a model internally) and the already-logged-in path (curated + # Nous model picker), then offers the Tool Gateway opt-in and sets + # provider=nous via the login/model save. This is the same routine quick + # setup calls, so `hermes portal` == quick setup's Nous step. try: - already_logged_in = bool((get_nous_auth_status() or {}).get("logged_in")) - except Exception: - already_logged_in = False + from hermes_cli.main import _model_flow_nous - if already_logged_in: - print_success(" Already logged into Nous Portal.") - else: - # Hand off to the shared auth wiring so the device-code flow is - # identical to `hermes auth add nous --type oauth`. SimpleNamespace - # mirrors the argparse Namespace contract that auth_add_command expects. - ns = SimpleNamespace( - provider="nous", - auth_type="oauth", - label=None, - api_key=None, - portal_url=None, - inference_url=None, - client_id=None, - scope=None, - no_browser=False, - timeout=None, - insecure=False, - ca_bundle=None, - ) - try: - auth_add_command(ns) - except SystemExit as e: - print() - print_error(f" Nous Portal login failed (exit {e.code}).") - print_info(" You can retry later with `hermes portal`.") - return - except (KeyboardInterrupt, EOFError): - print() - print_info(" Setup cancelled.") - return - except Exception as exc: - print() - print_error(f" Nous Portal login failed: {exc}") - print_info(" You can retry later with `hermes portal`.") - return - - # Set provider → nous so the model picker, status surfaces, and - # managed-tool gating all light up. Leave model.model empty so the - # runtime picks Nous's default model; the user can change it later - # with `hermes model`. - model_cfg = config.get("model") - if not isinstance(model_cfg, dict): - model_cfg = {} - config["model"] = model_cfg - model_cfg["provider"] = "nous" - save_config(config) - print() - print_success(" Nous set as your inference provider.") - - # Offer the Tool Gateway opt-in (single Y/n) — same flow that fires - # from `hermes model` after picking Nous. - print() - try: - prompt_enable_tool_gateway(config) - except (KeyboardInterrupt, EOFError): - pass + _model_flow_nous(config) + except (KeyboardInterrupt, EOFError, SystemExit): + # _login_nous raises SystemExit(130)/(1) on cancel/failure; the + # logged-out path inside _model_flow_nous catches it, but the + # expired-session re-login path only catches Exception, so a + # SystemExit there would otherwise escape and kill the whole CLI. + # Treat all of these as a graceful cancel/abort for the portal flow. + print() + print_info(" Setup cancelled.") + print_info(" You can retry later with `hermes portal`.") + return except Exception as exc: - print_warning(f" Tool Gateway prompt skipped: {exc}") + logger.debug("_model_flow_nous error during `hermes portal`: %s", exc) + print() + print_error(f" Nous Portal setup encountered an error: {exc}") + print_info(" You can retry later with `hermes portal`.") + return + + # Re-sync the in-memory config from disk — _model_flow_nous (and the + # underlying login/model save) write via their own load/save cycle, so any + # later save_config(config) by a caller must not clobber those values. + try: + _refreshed = load_config() + if isinstance(_refreshed, dict): + config.clear() + config.update(_refreshed) + except Exception: + pass print() print_success("Portal setup complete.") diff --git a/tests/hermes_cli/test_portal_cli.py b/tests/hermes_cli/test_portal_cli.py index d3643e170..927661ef5 100644 --- a/tests/hermes_cli/test_portal_cli.py +++ b/tests/hermes_cli/test_portal_cli.py @@ -109,3 +109,49 @@ def test_parser_registers_subcommands(): for sub in ("login", "info", "status", "open", "tools"): ns = parser.parse_args(["portal", sub]) assert ns.portal_command == sub + + +def test_one_shot_delegates_to_model_flow_nous(monkeypatch): + """`hermes portal` must run the quick-setup Nous flow (login + MODEL PICK + + provider + Tool Gateway), i.e. delegate to `_model_flow_nous` — not the + lighter auth-only path that skipped model selection. + """ + import hermes_cli.setup as setup_mod + + calls = {"model_flow": 0} + + def fake_model_flow(config): + calls["model_flow"] += 1 + + # _model_flow_nous lives in hermes_cli.main and is imported lazily inside + # _run_portal_one_shot, so patch it at the source module. + monkeypatch.setattr("hermes_cli.main._model_flow_nous", fake_model_flow) + # Keep the disk re-sync a no-op so the test never touches real config. + monkeypatch.setattr("hermes_cli.config.load_config", lambda: {}) + + setup_mod._run_portal_one_shot({}) + + assert calls["model_flow"] == 1, ( + "`hermes portal` must route through _model_flow_nous so the model " + "picker runs every time (matching quick setup)." + ) + + +@pytest.mark.parametrize("exc", [KeyboardInterrupt, EOFError, SystemExit]) +def test_one_shot_swallows_cancel_and_systemexit(monkeypatch, exc): + """A cancel/abort from the delegated Nous flow must NOT escape and kill the + CLI. `_login_nous` raises SystemExit(130)/(1) on cancel/failure, and the + expired-session re-login path inside `_model_flow_nous` only catches + Exception — so SystemExit could otherwise propagate out. The portal handler + must treat KeyboardInterrupt/EOFError/SystemExit as a graceful cancel. + """ + import hermes_cli.setup as setup_mod + + def boom(config): + raise exc + + monkeypatch.setattr("hermes_cli.main._model_flow_nous", boom) + monkeypatch.setattr("hermes_cli.config.load_config", lambda: {}) + + # Must return normally (None), not propagate the exception. + assert setup_mod._run_portal_one_shot({}) is None diff --git a/website/docs/guides/run-hermes-with-nous-portal.md b/website/docs/guides/run-hermes-with-nous-portal.md index 77518ff8b..6850193a1 100644 --- a/website/docs/guides/run-hermes-with-nous-portal.md +++ b/website/docs/guides/run-hermes-with-nous-portal.md @@ -95,7 +95,7 @@ You should see Hermes call `web_search` (Firecrawl-backed, through the gateway) ## 5. Pick the model you actually want -The default after `hermes setup --portal` is a sensible general-purpose model, but the whole point of the subscription is access to the full catalog. Switch with `/model` mid-session: +`hermes setup --portal` lets you pick a model during setup, but the whole point of the subscription is access to the full catalog — switch any time with `/model` mid-session: ```bash /model anthropic/claude-sonnet-4.6 # best general-purpose agentic diff --git a/website/docs/integrations/nous-portal.md b/website/docs/integrations/nous-portal.md index d6a1ad776..cd1ceff06 100644 --- a/website/docs/integrations/nous-portal.md +++ b/website/docs/integrations/nous-portal.md @@ -14,7 +14,7 @@ If you only have time to set up one thing, set up this. The fastest path: hermes setup --portal ``` -That single command runs the Portal OAuth, sets Nous as your inference provider in `config.yaml`, and turns on the Tool Gateway. You're ready to `hermes chat` immediately after. +That single command runs the Portal OAuth, lets you pick a Nous model, sets Nous as your inference provider in `config.yaml`, and turns on the Tool Gateway. You're ready to `hermes chat` immediately after. Don't have a subscription yet? [portal.nousresearch.com/manage-subscription](https://portal.nousresearch.com/manage-subscription) — sign up, then come back and run the command above. @@ -99,9 +99,10 @@ This runs the full setup in one shot: 1. Opens your browser to portal.nousresearch.com for OAuth login 2. Stores the refresh token at `~/.hermes/auth.json` -3. Sets Nous as your inference provider in `~/.hermes/config.yaml` -4. Turns on the Tool Gateway (web, image, TTS, browser routing) -5. Returns you to your terminal ready to `hermes chat` +3. Lets you pick a Nous model from the curated list (or skip to keep your current one) +4. Sets Nous as your inference provider in `~/.hermes/config.yaml` (when you pick a model) +5. Turns on the Tool Gateway (web, image, TTS, browser routing) +6. Returns you to your terminal ready to `hermes chat` If you don't have a subscription yet, sign up at [portal.nousresearch.com/manage-subscription](https://portal.nousresearch.com/manage-subscription) first. @@ -136,7 +137,7 @@ hermes portal tools # detailed Tool Gateway catalog with per-tool routing hermes portal open # open the subscription management page in your browser ``` -`hermes portal` (with no subcommand) is the human-readable alias for `hermes auth add nous --type oauth` — it logs you in, sets Nous as your inference provider, and offers the Tool Gateway opt-in (identical to `hermes setup --portal`). +`hermes portal` (with no subcommand) is the human-readable alias for `hermes auth add nous --type oauth` — it logs you in, lets you pick a Nous model, sets Nous as your inference provider, and offers the Tool Gateway opt-in (identical to `hermes setup --portal`, and the same Nous flow as the first-time quick setup). `hermes portal info` gives you the high-level overview: diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/run-hermes-with-nous-portal.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/run-hermes-with-nous-portal.md index fefe97df9..41dc86b4b 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/run-hermes-with-nous-portal.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/guides/run-hermes-with-nous-portal.md @@ -95,7 +95,7 @@ Hey, search the web for "Hermes Agent release notes" and summarize the top 3 hit ## 5. 选择你实际需要的模型 -`hermes setup --portal` 后的默认模型是一个合理的通用模型,但订阅的意义在于可以访问完整的模型目录。在会话中使用 `/model` 切换: +`hermes setup --portal` 会在设置过程中让你选择模型,但订阅的意义在于可以访问完整的模型目录——随时可在会话中使用 `/model` 切换: ```bash /model anthropic/claude-sonnet-4.6 # 最佳通用 agentic 模型 diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/integrations/nous-portal.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/integrations/nous-portal.md index 23fc31f8e..8e66915a0 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/integrations/nous-portal.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/integrations/nous-portal.md @@ -14,7 +14,7 @@ description: "一个订阅,300+ 前沿模型,Tool Gateway,以及 Nous Chat hermes setup --portal ``` -这条命令会完成 Portal OAuth 认证,在 `config.yaml` 中将 Nous 设为推理提供商,并开启 Tool Gateway。完成后即可立即运行 `hermes chat`。 +这条命令会完成 Portal OAuth 认证,让你选择一个 Nous 模型,在 `config.yaml` 中将 Nous 设为推理提供商,并开启 Tool Gateway。完成后即可立即运行 `hermes chat`。 还没有订阅?前往 [portal.nousresearch.com/manage-subscription](https://portal.nousresearch.com/manage-subscription) 注册,然后回来运行上面的命令。 @@ -95,9 +95,10 @@ hermes setup --portal 1. 打开浏览器跳转至 portal.nousresearch.com 进行 OAuth 登录 2. 将 refresh token 存储至 `~/.hermes/auth.json` -3. 在 `~/.hermes/config.yaml` 中将 Nous 设为推理提供商 -4. 开启 Tool Gateway(网页、图像、TTS、浏览器路由) -5. 返回终端,即可运行 `hermes chat` +3. 让你从精选列表中选择一个 Nous 模型(也可跳过以保留当前模型) +4. 在 `~/.hermes/config.yaml` 中将 Nous 设为推理提供商(当你选择模型时) +5. 开启 Tool Gateway(网页、图像、TTS、浏览器路由) +6. 返回终端,即可运行 `hermes chat` 如果还没有订阅,请先在 [portal.nousresearch.com/manage-subscription](https://portal.nousresearch.com/manage-subscription) 注册。 @@ -132,7 +133,7 @@ hermes portal tools # 详细的 Tool Gateway 目录及每个工具的路由 hermes portal open # 在浏览器中打开订阅管理页面 ``` -`hermes portal`(不带子命令)是 `hermes auth add nous --type oauth` 的易记别名——它会登录、把 Nous 设为推理服务商,并提供 Tool Gateway 启用选项(与 `hermes setup --portal` 等价)。 +`hermes portal`(不带子命令)是 `hermes auth add nous --type oauth` 的易记别名——它会登录、让你选择 Nous 模型、把 Nous 设为推理服务商,并提供 Tool Gateway 启用选项(与 `hermes setup --portal` 等价,与首次快速设置走的是同一套 Nous 流程)。 `hermes portal info` 给出高层概览: