feat(cli): make hermes portal run the full quick-setup Nous flow (model picker)
`hermes portal` / `hermes setup --portal` previously logged in and set provider=nous but left the model UNSELECTED (blank -> runtime default) and never showed a picker — unlike the first-time quick setup, which runs the model picker. Route `_run_portal_one_shot` through `_model_flow_nous` — the exact same routine quick setup (`_run_first_time_quick_setup`) and `hermes model` -> Nous use. It handles both the logged-out path (device-code OAuth, which picks a model internally) and the logged-in path (curated Nous model picker), then offers the Tool Gateway opt-in and sets provider=nous. Net effect: `hermes portal` now offers a model picker every time and is a true single-command collapse of quick setup's Nous step. Removes the hand-rolled auth_add_command + manual provider write + separate Tool Gateway prompt (now a single source of truth). Re-syncs the in-memory config from disk afterward so a caller's later save_config can't clobber the model/provider written by the login flow. Docs (CLI help, portal_cli docstrings, nous-portal EN + zh-Hans) updated to mention model selection. New regression test asserts `_run_portal_one_shot` delegates to `_model_flow_nous`. Verified live: `hermes portal` now shows the 27-model curated picker, 'Skip (keep current)' preserves prior provider/model.
This commit is contained in:
@ -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")
|
||||
|
||||
@ -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,37 @@ 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)
|
||||
_model_flow_nous(config)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
pass
|
||||
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.")
|
||||
|
||||
@ -109,3 +109,29 @@ 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)."
|
||||
)
|
||||
|
||||
@ -136,7 +136,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:
|
||||
|
||||
|
||||
@ -132,7 +132,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` 给出高层概览:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user