feat(portal): one-shot setup, status CLI, and Nous-included markers (#30860)

* feat(portal): one-shot setup, status CLI, and Nous-included markers

Four small Portal-aware surfaces that drive subscription value without
adding friction for non-Portal users.

  - hermes setup --portal: one-shot Nous OAuth + provider switch + Tool
    Gateway opt-in. Shareable as a single command from docs/social.
  - hermes portal {status,open,tools}: small surface over Portal auth +
    Tool Gateway routing. Defaults to 'status' when no subcommand.
  - Tool picker (hermes tools): when the user is logged into Nous, mark
    Nous-managed provider rows with a star and 'Included with your Nous
    subscription'. Suppressed when not authed — non-subscribers see the
    picker unchanged.
  - BYOK setup hint: a single dim line 'Available through Nous Portal
    subscription.' appears when the user is being prompted for a paid
    API key (Firecrawl, FAL, ElevenLabs, Browserbase, etc.) AND the
    category has a Nous-managed sibling AND the user is not already
    authed to Nous. Suppressed in all other cases.

Tested live end-to-end in an isolated HERMES_HOME with a simulated
authed and unauthed user. Targeted suite (tests/hermes_cli/
test_tools_config.py + test_setup.py) passes 97/97.

* fix: add portal to _BUILTIN_SUBCOMMANDS so plugin discovery fast-path skips it
This commit is contained in:
Teknium 2026-05-23 02:39:09 -07:00 committed by GitHub
parent 6942b1836e
commit b4cf5b65dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 401 additions and 2 deletions

View file

@ -1925,6 +1925,16 @@ def _configure_tool_category(ts_key: str, cat: dict, config: dict):
print()
# Plain text labels only (no ANSI codes in menu items)
# When the user is logged into Nous, surface a marker on providers
# whose access is included in their subscription so it's visually
# obvious which options cost extra vs. cost nothing on top of Nous.
try:
_nous_logged_in = bool(
get_nous_subscription_features(config).nous_auth_present
)
except Exception:
_nous_logged_in = False
provider_choices = []
for p in providers:
badge = f" [{p['badge']}]" if p.get("badge") else ""
@ -1938,7 +1948,15 @@ def _configure_tool_category(ts_key: str, cat: dict, config: dict):
configured = ""
else:
configured = " [configured]"
provider_choices.append(f"{p['name']}{badge}{tag}{configured}")
# Highlight Nous-managed entries when the user has Portal auth.
# curses_radiolist can't render ANSI inside item strings, so we
# use a plain unicode star + parenthetical phrase. Suppressed
# when no Portal auth is present so non-subscribers see the
# picker unchanged.
sub_marker = ""
if _nous_logged_in and p.get("managed_nous_feature"):
sub_marker = " ★ Included with your Nous subscription"
provider_choices.append(f"{p['name']}{badge}{tag}{configured}{sub_marker}")
# Add skip option
provider_choices.append("Skip — keep defaults / configure later")
@ -2405,6 +2423,30 @@ def _configure_provider(provider: dict, config: dict):
# Prompt for each required env var
all_configured = True
# If this BYOK provider lives in a category that ALSO has a
# Nous-managed sibling, show a single dim hint so users know
# they can avoid the key entirely via a Portal subscription.
# Suppressed when the user is already authed to Nous.
_show_portal_hint = False
if env_vars and not managed_feature and not provider.get("requires_nous_auth"):
try:
_has_managed_sibling = False
for _cat_key, _cat in TOOL_CATEGORIES.items():
_providers = _cat.get("providers", [])
if provider in _providers and any(
sib.get("managed_nous_feature") for sib in _providers
):
_has_managed_sibling = True
break
if _has_managed_sibling:
_features = get_nous_subscription_features(config)
_show_portal_hint = not _features.nous_auth_present
except Exception:
_show_portal_hint = False
if _show_portal_hint:
_print_info(" Available through Nous Portal subscription.")
for var in env_vars:
existing = get_env_value(var["key"])
if existing: