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:
kshitijk4poor 2026-06-04 02:20:31 +05:30
parent d4787d3e2e
commit cd188b814e
5 changed files with 85 additions and 89 deletions

View file

@ -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.")