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:
@@ -6097,6 +6097,13 @@ def cmd_webhook(args):
|
|||||||
webhook_command(args)
|
webhook_command(args)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_portal(args):
|
||||||
|
"""Nous Portal status and Tool Gateway routing surface."""
|
||||||
|
from hermes_cli.portal_cli import portal_command
|
||||||
|
|
||||||
|
return portal_command(args)
|
||||||
|
|
||||||
|
|
||||||
def cmd_slack(args):
|
def cmd_slack(args):
|
||||||
"""Slack integration helpers.
|
"""Slack integration helpers.
|
||||||
|
|
||||||
@@ -10647,7 +10654,7 @@ _BUILTIN_SUBCOMMANDS = frozenset(
|
|||||||
"config", "cron", "curator", "dashboard", "debug", "doctor",
|
"config", "cron", "curator", "dashboard", "debug", "doctor",
|
||||||
"dump", "fallback", "gateway", "hooks", "import", "insights",
|
"dump", "fallback", "gateway", "hooks", "import", "insights",
|
||||||
"kanban", "login", "logout", "logs", "lsp", "mcp", "memory", "migrate",
|
"kanban", "login", "logout", "logs", "lsp", "mcp", "memory", "migrate",
|
||||||
"model", "pairing", "plugins", "postinstall", "profile", "proxy",
|
"model", "pairing", "plugins", "portal", "postinstall", "profile", "proxy",
|
||||||
"send", "sessions", "setup",
|
"send", "sessions", "setup",
|
||||||
"skills", "slack", "status", "tools", "uninstall", "update",
|
"skills", "slack", "status", "tools", "uninstall", "update",
|
||||||
"version", "webhook", "whatsapp", "chat", "secrets",
|
"version", "webhook", "whatsapp", "chat", "secrets",
|
||||||
@@ -11384,6 +11391,13 @@ def main():
|
|||||||
help="On existing installs: only prompt for items that are missing "
|
help="On existing installs: only prompt for items that are missing "
|
||||||
"or unset, instead of running the full reconfigure wizard.",
|
"or unset, instead of running the full reconfigure wizard.",
|
||||||
)
|
)
|
||||||
|
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.",
|
||||||
|
)
|
||||||
setup_parser.set_defaults(func=cmd_setup)
|
setup_parser.set_defaults(func=cmd_setup)
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -11859,6 +11873,12 @@ def main():
|
|||||||
|
|
||||||
webhook_parser.set_defaults(func=cmd_webhook)
|
webhook_parser.set_defaults(func=cmd_webhook)
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# portal command — Nous Portal status + Tool Gateway routing
|
||||||
|
# =========================================================================
|
||||||
|
from hermes_cli.portal_cli import add_parser as _add_portal_parser
|
||||||
|
_add_portal_parser(subparsers)
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# kanban command — multi-profile collaboration board
|
# kanban command — multi-profile collaboration board
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
219
hermes_cli/portal_cli.py
Normal file
219
hermes_cli/portal_cli.py
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
"""``hermes portal`` — small CLI surface for Nous Portal users.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
status Show Portal auth state + which Tool Gateway tools are routed.
|
||||||
|
open Open the Portal subscription page in the user's default browser.
|
||||||
|
tools List Tool Gateway tools and which are active in the current config.
|
||||||
|
|
||||||
|
This command is intentionally minimal — it does not duplicate functionality
|
||||||
|
already in ``hermes auth`` or ``hermes tools``. It's a discovery + status
|
||||||
|
surface for the Portal subscription itself.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import webbrowser
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from hermes_cli.colors import Colors, color
|
||||||
|
from hermes_cli.config import load_config
|
||||||
|
|
||||||
|
DEFAULT_PORTAL_URL = "https://portal.nousresearch.com"
|
||||||
|
SUBSCRIPTION_URL = "https://portal.nousresearch.com/manage-subscription"
|
||||||
|
DOCS_URL = "https://hermes-agent.nousresearch.com/docs/user-guide/features/tool-gateway"
|
||||||
|
|
||||||
|
|
||||||
|
def _nous_portal_base_url() -> str:
|
||||||
|
"""Resolve the Portal base URL from auth state or default."""
|
||||||
|
try:
|
||||||
|
from hermes_cli.auth import get_nous_auth_status
|
||||||
|
status = get_nous_auth_status() or {}
|
||||||
|
url = status.get("portal_base_url")
|
||||||
|
if isinstance(url, str) and url.strip():
|
||||||
|
return url.rstrip("/")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return DEFAULT_PORTAL_URL
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_status(args) -> int:
|
||||||
|
"""Show Portal auth + Tool Gateway routing summary."""
|
||||||
|
from hermes_cli.auth import get_nous_auth_status
|
||||||
|
from hermes_cli.nous_subscription import get_nous_subscription_features
|
||||||
|
|
||||||
|
config = load_config() or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth = get_nous_auth_status() or {}
|
||||||
|
except Exception:
|
||||||
|
auth = {}
|
||||||
|
|
||||||
|
logged_in = bool(auth.get("logged_in"))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(color(" Nous Portal", Colors.MAGENTA))
|
||||||
|
print(color(" ───────────", Colors.MAGENTA))
|
||||||
|
if logged_in:
|
||||||
|
portal = auth.get("portal_base_url") or DEFAULT_PORTAL_URL
|
||||||
|
print(f" Auth: {color('✓ logged in', Colors.GREEN)}")
|
||||||
|
print(f" Portal: {portal}")
|
||||||
|
inference = auth.get("inference_base_url")
|
||||||
|
if inference:
|
||||||
|
print(f" API: {inference}")
|
||||||
|
else:
|
||||||
|
print(f" Auth: {color('not logged in', Colors.YELLOW)}")
|
||||||
|
print(f" Sign up: {SUBSCRIPTION_URL}")
|
||||||
|
print(f" Login: hermes auth add nous --type oauth")
|
||||||
|
|
||||||
|
# Provider selection (independent of auth)
|
||||||
|
model_cfg = config.get("model") if isinstance(config.get("model"), dict) else {}
|
||||||
|
provider = str(model_cfg.get("provider") or "").strip().lower()
|
||||||
|
if provider == "nous":
|
||||||
|
print(f" Model: {color('✓ using Nous as inference provider', Colors.GREEN)}")
|
||||||
|
elif provider:
|
||||||
|
print(f" Model: currently {provider} (switch with `hermes model`)")
|
||||||
|
|
||||||
|
# Tool Gateway routing
|
||||||
|
print()
|
||||||
|
print(color(" Tool Gateway", Colors.MAGENTA))
|
||||||
|
print(color(" ────────────", Colors.MAGENTA))
|
||||||
|
try:
|
||||||
|
features = get_nous_subscription_features(config)
|
||||||
|
except Exception:
|
||||||
|
features = None
|
||||||
|
|
||||||
|
if features is None:
|
||||||
|
print(" (could not resolve subscription state)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for feat in features.items():
|
||||||
|
if feat.managed_by_nous:
|
||||||
|
state = color("via Nous Portal", Colors.GREEN)
|
||||||
|
elif feat.active and feat.current_provider:
|
||||||
|
state = feat.current_provider
|
||||||
|
elif feat.active:
|
||||||
|
state = "active"
|
||||||
|
else:
|
||||||
|
state = color("not configured", Colors.DIM)
|
||||||
|
rows.append((feat.label, state))
|
||||||
|
|
||||||
|
width = max((len(r[0]) for r in rows), default=0)
|
||||||
|
for label, state in rows:
|
||||||
|
print(f" {label:<{width}} {state}")
|
||||||
|
|
||||||
|
if not logged_in:
|
||||||
|
print()
|
||||||
|
print(color(f" Docs: {DOCS_URL}", Colors.DIM))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_open(args) -> int:
|
||||||
|
"""Open the Portal subscription page in the default browser."""
|
||||||
|
target = SUBSCRIPTION_URL
|
||||||
|
print(f"Opening {target}")
|
||||||
|
try:
|
||||||
|
opened = webbrowser.open(target)
|
||||||
|
except Exception:
|
||||||
|
opened = False
|
||||||
|
if not opened:
|
||||||
|
print()
|
||||||
|
print("Could not launch a browser. Visit the URL above manually.")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd_tools(args) -> int:
|
||||||
|
"""List the Tool Gateway catalog + current routing."""
|
||||||
|
from hermes_cli.nous_subscription import get_nous_subscription_features
|
||||||
|
|
||||||
|
config = load_config() or {}
|
||||||
|
try:
|
||||||
|
features = get_nous_subscription_features(config)
|
||||||
|
except Exception:
|
||||||
|
print("Could not resolve Tool Gateway state.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Static catalog — the partners Tool Gateway routes to today.
|
||||||
|
catalog = [
|
||||||
|
("web", "Web search & extract", "Firecrawl"),
|
||||||
|
("image_gen", "Image generation", "FAL"),
|
||||||
|
("tts", "Text-to-speech", "OpenAI TTS"),
|
||||||
|
("browser", "Browser automation", "Browser Use"),
|
||||||
|
("modal", "Cloud terminal", "Modal"),
|
||||||
|
]
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(color(" Tool Gateway catalog", Colors.MAGENTA))
|
||||||
|
print(color(" ────────────────────", Colors.MAGENTA))
|
||||||
|
|
||||||
|
if not features.nous_auth_present:
|
||||||
|
print(color(" Not logged into Nous Portal — sign in with `hermes auth add nous --type oauth`.", Colors.YELLOW))
|
||||||
|
print()
|
||||||
|
|
||||||
|
label_width = max(len(label) for _, label, _ in catalog)
|
||||||
|
for key, label, partner in catalog:
|
||||||
|
feat = features.features.get(key)
|
||||||
|
if feat is None:
|
||||||
|
state = color("unknown", Colors.DIM)
|
||||||
|
elif feat.managed_by_nous:
|
||||||
|
state = color("✓ via Nous Portal", Colors.GREEN)
|
||||||
|
elif feat.active and feat.current_provider:
|
||||||
|
state = feat.current_provider
|
||||||
|
elif feat.active:
|
||||||
|
state = "active"
|
||||||
|
else:
|
||||||
|
state = color("not configured", Colors.DIM)
|
||||||
|
print(f" {label:<{label_width}} partner: {partner:<14} {state}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(color(f" Manage your subscription: {SUBSCRIPTION_URL}", Colors.DIM))
|
||||||
|
print(color(f" Docs: {DOCS_URL}", Colors.DIM))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def portal_command(args) -> int:
|
||||||
|
"""Top-level dispatch for `hermes portal <subcommand>`."""
|
||||||
|
sub = getattr(args, "portal_command", None)
|
||||||
|
if sub in {None, ""}:
|
||||||
|
# Default to status — matches gh / kubectl conventions where the
|
||||||
|
# subcommand-less form gives a useful overview.
|
||||||
|
return _cmd_status(args)
|
||||||
|
if sub == "status":
|
||||||
|
return _cmd_status(args)
|
||||||
|
if sub == "open":
|
||||||
|
return _cmd_open(args)
|
||||||
|
if sub == "tools":
|
||||||
|
return _cmd_tools(args)
|
||||||
|
print(f"Unknown portal subcommand: {sub}", file=sys.stderr)
|
||||||
|
print("Run `hermes portal -h` for usage.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(subparsers) -> None:
|
||||||
|
"""Register `hermes portal` on the given argparse subparsers object."""
|
||||||
|
portal_parser = subparsers.add_parser(
|
||||||
|
"portal",
|
||||||
|
help="Nous Portal status, subscription, and Tool Gateway routing",
|
||||||
|
description=(
|
||||||
|
"Inspect Nous Portal auth, Tool Gateway routing, and open the "
|
||||||
|
"Portal subscription page. Subcommands: status (default), "
|
||||||
|
"open, tools."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
portal_sub = portal_parser.add_subparsers(dest="portal_command")
|
||||||
|
|
||||||
|
portal_sub.add_parser(
|
||||||
|
"status",
|
||||||
|
help="Show Portal auth + Tool Gateway routing summary (default)",
|
||||||
|
)
|
||||||
|
portal_sub.add_parser(
|
||||||
|
"open",
|
||||||
|
help="Open the Portal subscription page in your default browser",
|
||||||
|
)
|
||||||
|
portal_sub.add_parser(
|
||||||
|
"tools",
|
||||||
|
help="List Tool Gateway tools and which are routed via Nous",
|
||||||
|
)
|
||||||
|
|
||||||
|
portal_parser.set_defaults(func=portal_command)
|
||||||
@@ -3060,6 +3060,119 @@ SETUP_SECTIONS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _run_portal_one_shot(config: dict) -> None:
|
||||||
|
"""One-shot Nous Portal setup — OAuth + provider switch + 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.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(
|
||||||
|
color(
|
||||||
|
"┌─────────────────────────────────────────────────────────┐",
|
||||||
|
Colors.MAGENTA,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print(color("│ ⚕ Hermes Setup — Nous Portal (one-shot) │", Colors.MAGENTA))
|
||||||
|
print(
|
||||||
|
color(
|
||||||
|
"└─────────────────────────────────────────────────────────┘",
|
||||||
|
Colors.MAGENTA,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
print_info(" One subscription, 300+ models, plus the Tool Gateway:")
|
||||||
|
print_info(" web search, image generation, TTS, browser automation")
|
||||||
|
print_info(" — all routed through your Nous Portal sub.")
|
||||||
|
print()
|
||||||
|
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
|
||||||
|
try:
|
||||||
|
already_logged_in = bool((get_nous_auth_status() or {}).get("logged_in"))
|
||||||
|
except Exception:
|
||||||
|
already_logged_in = False
|
||||||
|
|
||||||
|
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,
|
||||||
|
min_key_ttl_seconds=5 * 60,
|
||||||
|
)
|
||||||
|
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 auth add nous --type oauth`.")
|
||||||
|
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 auth add nous --type oauth`.")
|
||||||
|
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
|
||||||
|
except Exception as exc:
|
||||||
|
print_warning(f" Tool Gateway prompt skipped: {exc}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print_success("Portal setup complete.")
|
||||||
|
print_info(" Run `hermes portal status` to inspect routing.")
|
||||||
|
print_info(" Run `hermes` to start chatting.")
|
||||||
|
|
||||||
|
|
||||||
def run_setup_wizard(args):
|
def run_setup_wizard(args):
|
||||||
"""Run the interactive setup wizard.
|
"""Run the interactive setup wizard.
|
||||||
|
|
||||||
@@ -3115,6 +3228,11 @@ def run_setup_wizard(args):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# --portal: one-shot Nous Portal setup. Skips the rest of the wizard.
|
||||||
|
if bool(getattr(args, "portal", False)):
|
||||||
|
_run_portal_one_shot(config)
|
||||||
|
return
|
||||||
|
|
||||||
# Check if a specific section was requested
|
# Check if a specific section was requested
|
||||||
section = getattr(args, "section", None)
|
section = getattr(args, "section", None)
|
||||||
if section:
|
if section:
|
||||||
|
|||||||
@@ -1925,6 +1925,16 @@ def _configure_tool_category(ts_key: str, cat: dict, config: dict):
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
# Plain text labels only (no ANSI codes in menu items)
|
# 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 = []
|
provider_choices = []
|
||||||
for p in providers:
|
for p in providers:
|
||||||
badge = f" [{p['badge']}]" if p.get("badge") else ""
|
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 = ""
|
configured = ""
|
||||||
else:
|
else:
|
||||||
configured = " [configured]"
|
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
|
# Add skip option
|
||||||
provider_choices.append("Skip — keep defaults / configure later")
|
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
|
# Prompt for each required env var
|
||||||
all_configured = True
|
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:
|
for var in env_vars:
|
||||||
existing = get_env_value(var["key"])
|
existing = get_env_value(var["key"])
|
||||||
if existing:
|
if existing:
|
||||||
|
|||||||
Reference in New Issue
Block a user