refactor(cli/setup.py): first-time mode table, quick-setup env-var helper, agent-settings summary table
This commit is contained in:
@@ -306,8 +306,7 @@ def prompt_choice(question: str, choices: list, default: int = 0, description: s
|
||||
idx = _curses_prompt_choice(question, choices, default, description=description)
|
||||
if idx >= 0:
|
||||
if idx == default:
|
||||
print_info(" Skipped (keeping current)")
|
||||
print()
|
||||
_info(" Skipped (keeping current)", None)
|
||||
return default
|
||||
print()
|
||||
return idx
|
||||
@@ -436,16 +435,14 @@ def setup_model_provider(config: dict, *, quick: bool = False):
|
||||
"""
|
||||
from hermes_cli.config import load_config, save_config
|
||||
print_header("Inference Provider")
|
||||
print_info("Choose how to connect to your main chat model.")
|
||||
print_info(f" Guide: {_DOCS_BASE}/integrations/providers")
|
||||
print()
|
||||
_info("Choose how to connect to your main chat model.",
|
||||
f" Guide: {_DOCS_BASE}/integrations/providers", None)
|
||||
|
||||
from hermes_cli.main import select_provider_and_model
|
||||
try:
|
||||
select_provider_and_model()
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
print()
|
||||
print_info("Provider setup skipped.")
|
||||
_info(None, "Provider setup skipped.")
|
||||
except Exception as exc:
|
||||
logger.debug("select_provider_and_model error during setup: %s", exc)
|
||||
print_warning(f"Provider setup encountered an error: {exc}")
|
||||
@@ -525,14 +522,13 @@ def setup_agent_settings(config: dict):
|
||||
"""Configure agent behavior: iterations, progress display, compression, session reset."""
|
||||
|
||||
print_header("Agent Settings")
|
||||
print_info(f" Guide: {_DOCS_BASE}/user-guide/configuration")
|
||||
print()
|
||||
_info(f" Guide: {_DOCS_BASE}/user-guide/configuration", None)
|
||||
|
||||
# ── Max Iterations ── (config.yaml is authoritative; never surface a stale legacy .env value)
|
||||
current_max = str(cfg_get(config, "agent", "max_turns", default=90))
|
||||
print_info("Maximum tool-calling iterations per conversation.")
|
||||
print_info("Higher = more complex tasks, but costs more tokens.")
|
||||
print_info(f"Press Enter to keep {current_max}. Use 90 for most tasks or 150+ for open exploration.")
|
||||
_info("Maximum tool-calling iterations per conversation.",
|
||||
"Higher = more complex tasks, but costs more tokens.",
|
||||
f"Press Enter to keep {current_max}. Use 90 for most tasks or 150+ for open exploration.")
|
||||
|
||||
try:
|
||||
max_iter = int(prompt("Max iterations", current_max))
|
||||
@@ -561,8 +557,8 @@ def setup_agent_settings(config: dict):
|
||||
|
||||
# ── Context Compression ──
|
||||
print_header("Context Compression")
|
||||
print_info("Automatically summarizes old messages when context gets too long.")
|
||||
print_info("Higher threshold = compress later (use more context). Lower = compress sooner.")
|
||||
_info("Automatically summarizes old messages when context gets too long.",
|
||||
"Higher threshold = compress later (use more context). Lower = compress sooner.")
|
||||
|
||||
config.setdefault("compression", {})["enabled"] = True
|
||||
|
||||
@@ -598,15 +594,15 @@ def setup_agent_settings(config: dict):
|
||||
if mode in ("both", "daily"):
|
||||
_prompt_int_setting(reset_cfg, "at_hour", " Daily reset hour (0-23, local time)", current_hour, lambda v: 0 <= v <= 23)
|
||||
idle_now, hour_now = reset_cfg.get("idle_minutes", 1440), reset_cfg.get("at_hour", 4)
|
||||
if mode == "both":
|
||||
print_success(f"Sessions reset after {idle_now} min idle or daily at {hour_now}:00")
|
||||
elif mode == "idle":
|
||||
print_success(f"Sessions reset after {idle_now} min of inactivity")
|
||||
elif mode == "daily":
|
||||
print_success(f"Sessions reset daily at {hour_now}:00")
|
||||
elif mode == "none":
|
||||
if mode == "none":
|
||||
print_info("Sessions will never auto-reset. Context is managed only by compression.")
|
||||
print_warning("Long conversations will grow in cost. Use /reset manually when needed.")
|
||||
elif mode is not None:
|
||||
print_success({
|
||||
"both": f"Sessions reset after {idle_now} min idle or daily at {hour_now}:00",
|
||||
"idle": f"Sessions reset after {idle_now} min of inactivity",
|
||||
"daily": f"Sessions reset daily at {hour_now}:00",
|
||||
}[mode])
|
||||
|
||||
save_config(config)
|
||||
|
||||
@@ -641,8 +637,8 @@ _SEND_CONSENT_EXPLAINER = (
|
||||
def setup_telemetry(config: dict):
|
||||
"""Configure the local shared-metrics subscriber and optional sending."""
|
||||
print_header("Shared Metrics")
|
||||
print_info("Shared metrics contain only bounded counters and histograms.")
|
||||
print_info("Collection is local. Sending them to Nous is a separate opt-in.")
|
||||
_info("Shared metrics contain only bounded counters and histograms.",
|
||||
"Collection is local. Sending them to Nous is a separate opt-in.")
|
||||
|
||||
telemetry = config.get("telemetry")
|
||||
if not isinstance(telemetry, dict):
|
||||
@@ -742,8 +738,7 @@ def run_setup_wizard(args):
|
||||
try:
|
||||
return _run_setup_wizard_impl(args)
|
||||
except _SetupCancelled:
|
||||
print()
|
||||
print_info("Setup cancelled. Remaining sections were not changed.")
|
||||
_info(None, "Setup cancelled. Remaining sections were not changed.")
|
||||
return None
|
||||
|
||||
|
||||
@@ -779,12 +774,9 @@ def _run_setup_section(config: dict, section: str) -> None:
|
||||
def _run_full_setup(config: dict, hermes_home, *, is_existing: bool, migration_ran: bool) -> None:
|
||||
"""Full Setup — run all sections, honoring post-migration skips."""
|
||||
print_header("Configuration Location")
|
||||
print_info(f"Config file: {get_config_path()}")
|
||||
print_info(f"Secrets file: {get_env_path()}")
|
||||
print_info(f"Data folder: {hermes_home}")
|
||||
print_info(f"Install dir: {PROJECT_ROOT}")
|
||||
print()
|
||||
print_info("You can edit these files directly or use 'hermes config edit'")
|
||||
_info(f"Config file: {get_config_path()}", f"Secrets file: {get_env_path()}",
|
||||
f"Data folder: {hermes_home}", f"Install dir: {PROJECT_ROOT}", None,
|
||||
"You can edit these files directly or use 'hermes config edit'")
|
||||
|
||||
if migration_ran:
|
||||
_info(None, "Settings were imported from OpenClaw.",
|
||||
@@ -831,6 +823,16 @@ def _run_full_setup(config: dict, hermes_home, *, is_existing: bool, migration_r
|
||||
)
|
||||
|
||||
|
||||
# First-time mode picker: (menu label, runner) — a None runner falls through to Full Setup.
|
||||
_FIRST_TIME_MODES = (
|
||||
("Quick Setup (Nous Portal) — free OAuth login, no API keys, model + tools (recommended)",
|
||||
lambda config, home, is_existing: _run_first_time_quick_setup(config, home, is_existing)),
|
||||
("Full setup — configure every provider, tool & option yourself (bring your own keys)", None),
|
||||
("Blank Slate — everything off except the bare minimum; opt in to each capability",
|
||||
lambda config, home, is_existing: _run_blank_slate_setup(config, home, is_existing)),
|
||||
)
|
||||
|
||||
|
||||
def _run_setup_wizard_impl(args):
|
||||
"""Run the interactive setup wizard: full/quick (auto-detected), ``--portal``, or one
|
||||
``hermes setup <section>`` from SETUP_SECTIONS."""
|
||||
@@ -840,43 +842,35 @@ def _run_setup_wizard_impl(args):
|
||||
return
|
||||
ensure_hermes_home()
|
||||
|
||||
if bool(getattr(args, "reset", False)):
|
||||
if getattr(args, "reset", False):
|
||||
save_config(copy.deepcopy(DEFAULT_CONFIG))
|
||||
print_success("Configuration reset to defaults.")
|
||||
|
||||
reconfigure_requested = bool(getattr(args, "reconfigure", False))
|
||||
quick_requested = bool(getattr(args, "quick", False))
|
||||
|
||||
config = load_config()
|
||||
hermes_home = get_hermes_home()
|
||||
|
||||
config_path = get_config_path()
|
||||
_backup_path = _backup_config_file(config_path)
|
||||
|
||||
# Detect non-interactive environments (headless SSH, Docker, CI/CD)
|
||||
# Non-interactive environments (headless SSH, Docker, CI/CD)
|
||||
if getattr(args, 'non_interactive', False) or not is_interactive_stdin():
|
||||
print_noninteractive_setup_guidance("Running in a non-interactive environment (no TTY detected).")
|
||||
return
|
||||
|
||||
# --portal: one-shot Nous Portal setup. Skips the rest of the wizard.
|
||||
if bool(getattr(args, "portal", False)):
|
||||
if getattr(args, "portal", False): # one-shot Nous Portal setup; skips the rest
|
||||
_run_portal_one_shot(config)
|
||||
return
|
||||
|
||||
section = getattr(args, "section", None)
|
||||
if section:
|
||||
_run_setup_section(config, section)
|
||||
return
|
||||
|
||||
# Check if this is an existing installation with a provider configured
|
||||
# Existing installation == a provider is configured
|
||||
from hermes_cli.auth import get_active_provider
|
||||
|
||||
is_existing = (
|
||||
bool(get_env_value("OPENROUTER_API_KEY"))
|
||||
or bool(get_env_value("OPENAI_BASE_URL"))
|
||||
or get_active_provider() is not None
|
||||
is_existing = bool(
|
||||
get_env_value("OPENROUTER_API_KEY") or get_env_value("OPENAI_BASE_URL") or get_active_provider() is not None
|
||||
)
|
||||
|
||||
_print_banner(
|
||||
"│ ⚕ Hermes Agent Setup Wizard │",
|
||||
"├─────────────────────────────────────────────────────────┤",
|
||||
@@ -885,14 +879,13 @@ def _run_setup_wizard_impl(args):
|
||||
)
|
||||
|
||||
migration_ran = False
|
||||
|
||||
if is_existing:
|
||||
# Existing install — the full reconfigure wizard is the default (Enter keeps each current
|
||||
# value); `--quick` narrows it to missing items (partial OpenClaw import, cleared key).
|
||||
# Full reconfigure wizard is the default (Enter keeps each current value); `--quick`
|
||||
# narrows it to missing items (partial OpenClaw import, cleared key). --reconfigure is a
|
||||
# backwards-compatible no-op here.
|
||||
if quick_requested:
|
||||
_run_setup_steps([("Quick Setup", lambda: _run_quick_setup(config, hermes_home))])
|
||||
return
|
||||
|
||||
print()
|
||||
print_header("Reconfigure")
|
||||
print_success("You already have Hermes configured.")
|
||||
@@ -900,38 +893,18 @@ def _run_setup_wizard_impl(args):
|
||||
"Press Enter to keep it, or type a new value to change it.", "",
|
||||
"Tip: jump straight to a section with 'hermes setup model|terminal|",
|
||||
" gateway|tools|agent', or fill only missing items with --quick.")
|
||||
# --reconfigure is kept for backwards compatibility and is a no-op here.
|
||||
else:
|
||||
# ── First-Time Setup ── (--reconfigure / --quick are meaningless here; fall through)
|
||||
# First-time setup (--reconfigure / --quick are meaningless here; fall through)
|
||||
print()
|
||||
if reconfigure_requested or quick_requested:
|
||||
print_info("No existing configuration found — running first-time setup.")
|
||||
print()
|
||||
|
||||
# Offer OpenClaw migration before configuration begins
|
||||
migration_ran = _offer_openclaw_migration(hermes_home)
|
||||
_info("No existing configuration found — running first-time setup.", None)
|
||||
migration_ran = _offer_openclaw_migration(hermes_home) # before configuration begins
|
||||
if migration_ran:
|
||||
config = load_config()
|
||||
|
||||
setup_mode = prompt_choice(
|
||||
"How would you like to set up Hermes?",
|
||||
[
|
||||
"Quick Setup (Nous Portal) — free OAuth login, no API keys, model + tools (recommended)",
|
||||
"Full setup — configure every provider, tool & option yourself (bring your own keys)",
|
||||
"Blank Slate — everything off except the bare minimum; opt in to each capability",
|
||||
],
|
||||
0,
|
||||
)
|
||||
|
||||
if setup_mode == 0:
|
||||
_run_setup_steps(
|
||||
[("Quick Setup", lambda: _run_first_time_quick_setup(config, hermes_home, is_existing))]
|
||||
)
|
||||
return
|
||||
if setup_mode == 2:
|
||||
_run_setup_steps(
|
||||
[("Blank Slate", lambda: _run_blank_slate_setup(config, hermes_home, is_existing))]
|
||||
)
|
||||
setup_mode = prompt_choice("How would you like to set up Hermes?", [label for label, _ in _FIRST_TIME_MODES], 0)
|
||||
label, runner = _FIRST_TIME_MODES[setup_mode]
|
||||
if runner is not None:
|
||||
_run_setup_steps([(label, lambda: runner(config, hermes_home, is_existing))])
|
||||
return
|
||||
|
||||
_run_full_setup(config, hermes_home, is_existing=is_existing, migration_ran=migration_ran)
|
||||
@@ -939,7 +912,7 @@ def _run_setup_wizard_impl(args):
|
||||
# Save and show summary
|
||||
save_config(config)
|
||||
if _backup_path and _backup_path.exists():
|
||||
print_info(f"Previous config backed up to: {_backup_path}")
|
||||
print_info("If setup changed a value you customized, restore it with:")
|
||||
print_info(f" cp {_backup_path} {config_path}")
|
||||
_info(f"Previous config backed up to: {_backup_path}",
|
||||
"If setup changed a value you customized, restore it with:",
|
||||
f" cp {_backup_path} {config_path}")
|
||||
_print_setup_summary(config, hermes_home)
|
||||
|
||||
@@ -274,8 +274,8 @@ def _offer_openclaw_migration(hermes_home: Path) -> bool:
|
||||
successfully, False otherwise.
|
||||
"""
|
||||
from hermes_cli.setup import (
|
||||
_OPENCLAW_SCRIPT, get_config_path, load_config, print_header, print_info,
|
||||
print_success, print_warning, prompt_yes_no, save_config,
|
||||
_OPENCLAW_SCRIPT, get_config_path, _info, load_config, print_header, print_info, print_success,
|
||||
print_warning, prompt_yes_no, save_config,
|
||||
)
|
||||
openclaw_dir = Path.home() / ".openclaw"
|
||||
if not openclaw_dir.is_dir() or not _OPENCLAW_SCRIPT.exists():
|
||||
@@ -283,9 +283,8 @@ def _offer_openclaw_migration(hermes_home: Path) -> bool:
|
||||
|
||||
print()
|
||||
print_header("OpenClaw Installation Detected")
|
||||
print_info(f"Found OpenClaw data at {openclaw_dir}")
|
||||
print_info("Hermes can preview what would be imported before making any changes.")
|
||||
print()
|
||||
_info(f"Found OpenClaw data at {openclaw_dir}",
|
||||
"Hermes can preview what would be imported before making any changes.", None)
|
||||
if not prompt_yes_no("Would you like to see what can be imported?", default=True):
|
||||
print_info("Skipping migration. You can run it later with: hermes claw migrate --dry-run")
|
||||
return False
|
||||
@@ -315,19 +314,17 @@ def _offer_openclaw_migration(hermes_home: Path) -> bool:
|
||||
|
||||
preview_count = preview_report.get("summary", {}).get("migrated", 0)
|
||||
if preview_count == 0:
|
||||
print()
|
||||
print_info("Nothing to import from OpenClaw.")
|
||||
_info(None, "Nothing to import from OpenClaw.")
|
||||
return False
|
||||
print()
|
||||
print_header(f"Migration Preview — {preview_count} item(s) would be imported")
|
||||
print_info("No changes have been made yet. Review the list below:")
|
||||
print()
|
||||
_info("No changes have been made yet. Review the list below:", None)
|
||||
_print_migration_preview(preview_report)
|
||||
|
||||
# ── Phase 2: Confirm and execute ──
|
||||
if not prompt_yes_no("Proceed with migration?", default=False):
|
||||
print_info("Migration cancelled. You can run it later with: hermes claw migrate")
|
||||
print_info("Use --dry-run to preview again, or --preset minimal for a lighter import.")
|
||||
_info("Migration cancelled. You can run it later with: hermes claw migrate",
|
||||
"Use --dry-run to preview again, or --preset minimal for a lighter import.")
|
||||
return False
|
||||
|
||||
# overwrite=False so existing Hermes configs are preserved. The user saw the
|
||||
|
||||
@@ -23,20 +23,23 @@ def _reload_config_into(config: dict) -> None:
|
||||
config.update(_refreshed)
|
||||
|
||||
|
||||
def _prompt_env_var(var: dict):
|
||||
"""Prompt for one env-var value (masked when the var is a secret)."""
|
||||
from hermes_cli.setup import prompt
|
||||
return prompt(f" {var.get('prompt', var['name'])}", password=bool(var.get("password")))
|
||||
def _prompt_and_save_env_var(var: dict, saved_msg: str, skipped_msg: str) -> None:
|
||||
"""Prompt for one env-var value (masked when secret); persist and confirm, or report the skip."""
|
||||
from hermes_cli.setup import print_success, print_warning, prompt, save_env_value
|
||||
value = prompt(f" {var.get('prompt', var['name'])}", password=bool(var.get("password")))
|
||||
if value:
|
||||
save_env_value(var["name"], value)
|
||||
print_success(saved_msg)
|
||||
else:
|
||||
print_warning(skipped_msg)
|
||||
|
||||
|
||||
def _run_portal_one_shot(config: dict) -> None:
|
||||
"""One-shot Nous Portal setup — OAuth + model pick + provider + Tool Gateway.
|
||||
"""One-shot Nous Portal setup (``hermes setup --portal`` / ``hermes portal``).
|
||||
|
||||
Wired into ``hermes setup --portal`` and ``hermes portal``: the Nous-Portal slice
|
||||
of first-time quick setup as one shareable command. Login, model selection,
|
||||
provider switch and Tool Gateway opt-in are all delegated to ``_model_flow_nous``
|
||||
— the same flow ``_run_first_time_quick_setup`` and ``hermes model`` use for Nous —
|
||||
so there is a single source of truth and ``hermes portal`` always offers a picker.
|
||||
Login, model pick, provider switch and Tool Gateway opt-in are all delegated to
|
||||
``_model_flow_nous`` — the same flow quick setup and ``hermes model`` use for Nous — so
|
||||
there is one source of truth and ``hermes portal`` always offers a picker.
|
||||
"""
|
||||
from hermes_cli.setup import Colors, color, _info, load_config, print_error, print_info, print_success
|
||||
print()
|
||||
@@ -48,16 +51,12 @@ def _run_portal_one_shot(config: dict) -> None:
|
||||
" — all routed through your Nous Portal sub.", None,
|
||||
" Sign up: https://portal.nousresearch.com/manage-subscription", None)
|
||||
|
||||
# Handles both the logged-out path (device-code OAuth, selects a model) and the
|
||||
# logged-in path (curated picker), then Tool Gateway opt-in; sets provider=nous.
|
||||
try:
|
||||
from hermes_cli.main import _model_flow_nous
|
||||
|
||||
_model_flow_nous(config)
|
||||
except (KeyboardInterrupt, EOFError, SystemExit):
|
||||
# _login_nous raises SystemExit(130)/(1) on cancel/failure; the expired-
|
||||
# session re-login path inside _model_flow_nous only catches Exception,
|
||||
# so a SystemExit there would otherwise escape and kill the whole CLI.
|
||||
# _login_nous raises SystemExit(130)/(1) on cancel/failure; the expired-session re-login
|
||||
# path inside _model_flow_nous only catches Exception, so SystemExit would kill the CLI.
|
||||
_info(None, " Setup cancelled.", " You can retry later with `hermes portal`.")
|
||||
return
|
||||
except Exception as exc:
|
||||
@@ -67,8 +66,7 @@ def _run_portal_one_shot(config: dict) -> None:
|
||||
print_info(" You can retry later with `hermes portal`.")
|
||||
return
|
||||
|
||||
# Re-sync from disk — _model_flow_nous writes via its own load/save cycle, so
|
||||
# a later save_config(config) by a caller must not clobber those values.
|
||||
# Re-sync from disk so a caller's later save_config(config) can't clobber the login save.
|
||||
try:
|
||||
_refreshed = load_config()
|
||||
if isinstance(_refreshed, dict):
|
||||
@@ -79,8 +77,7 @@ def _run_portal_one_shot(config: dict) -> None:
|
||||
|
||||
print()
|
||||
print_success("Portal setup complete.")
|
||||
print_info(" Run `hermes portal info` to inspect routing.")
|
||||
print_info(" Run `hermes` to start chatting.")
|
||||
_info(" Run `hermes portal info` to inspect routing.", " Run `hermes` to start chatting.")
|
||||
|
||||
|
||||
def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool):
|
||||
@@ -104,8 +101,7 @@ def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool):
|
||||
from hermes_cli.main import _model_flow_nous
|
||||
_model_flow_nous(config)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print()
|
||||
print_info("Nous Portal setup cancelled.")
|
||||
_info(None, "Nous Portal setup cancelled.")
|
||||
except Exception as exc:
|
||||
logger.debug("_model_flow_nous error during quick setup: %s", exc)
|
||||
print_warning(f"Nous Portal setup encountered an error: {exc}")
|
||||
@@ -137,8 +133,7 @@ def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool):
|
||||
|
||||
print()
|
||||
print_success("Setup complete! You're ready to go.")
|
||||
print()
|
||||
print_info(" Configure all settings: hermes setup")
|
||||
_info(None, " Configure all settings: hermes setup")
|
||||
if gateway_choice != 0:
|
||||
print_info(" Connect Telegram/Discord: hermes setup gateway")
|
||||
_print_macos_fda_tip()
|
||||
@@ -379,8 +374,7 @@ def _blank_slate_walkthrough(config: dict, hermes_home):
|
||||
def _run_quick_setup(config: dict, hermes_home):
|
||||
"""Quick setup — only configure items that are missing."""
|
||||
from hermes_cli.setup import (
|
||||
Colors, color, _info, print_header, print_info, _print_setup_summary, print_success, print_warning,
|
||||
_prompt_api_key, prompt_checklist, save_config, save_env_value,
|
||||
Colors, color, _info, print_header, print_info, _print_setup_summary, print_success, _prompt_api_key, prompt_checklist, save_config,
|
||||
)
|
||||
from hermes_cli.config import (get_missing_env_vars, get_missing_config_fields, check_config_version)
|
||||
print()
|
||||
@@ -398,94 +392,66 @@ def _run_quick_setup(config: dict, hermes_home):
|
||||
"or pick a specific section from the menu.")
|
||||
return
|
||||
|
||||
# Handle missing required env vars
|
||||
if missing_required:
|
||||
print()
|
||||
print_info(f"{len(missing_required)} required setting(s) missing:")
|
||||
_info(None, f"{len(missing_required)} required setting(s) missing:")
|
||||
for var in missing_required:
|
||||
print(f" • {var['name']}")
|
||||
print()
|
||||
|
||||
for var in missing_required:
|
||||
print()
|
||||
print(color(f" {var['name']}", Colors.CYAN))
|
||||
print_info(f" {var.get('description', '')}")
|
||||
if var.get("url"):
|
||||
print_info(f" Get key at: {var['url']}")
|
||||
_prompt_and_save_env_var(var, f" Saved {var['name']}", f" Skipped {var['name']}")
|
||||
|
||||
value = _prompt_env_var(var)
|
||||
if value:
|
||||
save_env_value(var["name"], value)
|
||||
print_success(f" Saved {var['name']}")
|
||||
else:
|
||||
print_warning(f" Skipped {var['name']}")
|
||||
|
||||
# Split missing optional vars by category
|
||||
missing_tools = [v for v in missing_optional if v.get("category") == "tool"]
|
||||
missing_messaging = [
|
||||
v for v in missing_optional if v.get("category") == "messaging" and not v.get("advanced")
|
||||
]
|
||||
|
||||
# Tool API keys (checklist)
|
||||
if missing_tools:
|
||||
if missing_tools: # checklist, then the API-key screen for each pick
|
||||
print()
|
||||
print_header("Tool API Keys")
|
||||
|
||||
checklist_labels = []
|
||||
labels = []
|
||||
for var in missing_tools:
|
||||
tools = var.get("tools", [])
|
||||
tools_str = f" → {', '.join(tools[:2])}" if tools else ""
|
||||
checklist_labels.append(f"{var.get('description', var['name'])}{tools_str}")
|
||||
|
||||
for idx in prompt_checklist("Which tools would you like to configure?", checklist_labels):
|
||||
labels.append(f"{var.get('description', var['name'])}{tools_str}")
|
||||
for idx in prompt_checklist("Which tools would you like to configure?", labels):
|
||||
_prompt_api_key(missing_tools[idx])
|
||||
|
||||
# Messaging platforms (checklist then prompt for selected)
|
||||
if missing_messaging:
|
||||
if missing_messaging: # checklist, then prompt for each selected platform's vars
|
||||
print()
|
||||
print_header("Messaging Platforms")
|
||||
print_info("Connect Hermes to messaging apps to chat from anywhere.")
|
||||
print_info("You can configure these later with 'hermes setup gateway'.")
|
||||
|
||||
# Group by platform (preserving order); vars matching no platform are dropped.
|
||||
platform_order = []
|
||||
platforms = {}
|
||||
_info("Connect Hermes to messaging apps to chat from anywhere.",
|
||||
"You can configure these later with 'hermes setup gateway'.")
|
||||
# Group by platform in first-seen order; vars matching no platform are dropped.
|
||||
grouped: dict[str, list] = {}
|
||||
emojis = {}
|
||||
for var in missing_messaging:
|
||||
for needle, plat, emoji in _MESSAGING_PLATFORMS:
|
||||
if needle in var["name"]:
|
||||
grouped.setdefault(plat, []).append(var)
|
||||
emojis[plat] = emoji
|
||||
break
|
||||
else:
|
||||
continue
|
||||
if plat not in platforms:
|
||||
platform_order.append(plat)
|
||||
emojis[plat] = emoji
|
||||
platforms.setdefault(plat, []).append(var)
|
||||
|
||||
platform_labels = [f"{emojis[p]} {p}" for p in platform_order]
|
||||
selected_indices = prompt_checklist("Which platforms would you like to set up?", platform_labels)
|
||||
|
||||
for idx in selected_indices:
|
||||
platform_order = list(grouped)
|
||||
labels = [f"{emojis[p]} {p}" for p in platform_order]
|
||||
for idx in prompt_checklist("Which platforms would you like to set up?", labels):
|
||||
plat = platform_order[idx]
|
||||
print()
|
||||
print(color(f" ─── {emojis[plat]} {plat} ───", Colors.CYAN))
|
||||
print()
|
||||
for var in platforms[plat]:
|
||||
for var in grouped[plat]:
|
||||
print_info(f" {var.get('description', '')}")
|
||||
if var.get("url"):
|
||||
print_info(f" {var['url']}")
|
||||
value = _prompt_env_var(var)
|
||||
if value:
|
||||
save_env_value(var["name"], value)
|
||||
print_success(" ✓ Saved")
|
||||
else:
|
||||
print_warning(" Skipped")
|
||||
_prompt_and_save_env_var(var, " ✓ Saved", " Skipped")
|
||||
print()
|
||||
|
||||
# Handle missing config fields
|
||||
if missing_config:
|
||||
print()
|
||||
print_info(f"Adding {len(missing_config)} new config option(s) with defaults...")
|
||||
_info(None, f"Adding {len(missing_config)} new config option(s) with defaults...")
|
||||
for field in missing_config:
|
||||
print_success(f" Added {field['key']} = {field['default']}")
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ def _print_setup_summary(config: dict, hermes_home):
|
||||
"""Print the setup completion summary."""
|
||||
from hermes_cli.setup import (
|
||||
Colors, color, get_config_path, get_env_path, get_nous_subscription_features, _info, print_header,
|
||||
print_info, print_warning,
|
||||
print_warning,
|
||||
)
|
||||
# Provider readiness — the one thing setup absolutely must produce. Previously a user
|
||||
# could cancel the API-key prompt mid-wizard (Enter → "Cancelled."), watch the wizard
|
||||
@@ -289,8 +289,7 @@ def _print_setup_summary(config: dict, hermes_home):
|
||||
tool_status.append(row)
|
||||
|
||||
available_count = sum(1 for _, avail, _ in tool_status if avail)
|
||||
print_info(f"{available_count}/{len(tool_status)} tool categories available:")
|
||||
print()
|
||||
_info(f"{available_count}/{len(tool_status)} tool categories available:", None)
|
||||
for name, available, missing_var in tool_status:
|
||||
if available:
|
||||
print(f" {color('✓', Colors.GREEN)} {name}")
|
||||
|
||||
@@ -238,10 +238,10 @@ def _setup_backend_daytona(config: dict) -> None:
|
||||
|
||||
|
||||
def _setup_backend_vercel(config: dict) -> None:
|
||||
from hermes_cli.setup import print_info, print_success, print_warning
|
||||
from hermes_cli.setup import _info, print_info, print_success, print_warning
|
||||
print_success("Terminal backend: Vercel Sandbox")
|
||||
print_info("Cloud microVM sandboxes with snapshot-backed filesystem persistence.")
|
||||
print_info("Requires the optional SDK: pip install 'hermes-agent[vercel]'")
|
||||
_info("Cloud microVM sandboxes with snapshot-backed filesystem persistence.",
|
||||
"Requires the optional SDK: pip install 'hermes-agent[vercel]'")
|
||||
try:
|
||||
__import__("vercel")
|
||||
except ImportError:
|
||||
@@ -338,14 +338,14 @@ _TERMINAL_BACKEND_SETUP = {
|
||||
def setup_terminal_backend(config: dict):
|
||||
"""Configure the terminal execution backend."""
|
||||
from hermes_cli.setup import (
|
||||
_DOCS_BASE, cfg_get, print_header, print_info, print_success, prompt_choice, save_config, save_env_value,
|
||||
_DOCS_BASE, cfg_get, _info, print_header, print_info, print_success, prompt_choice, save_config,
|
||||
save_env_value,
|
||||
)
|
||||
import platform as _platform
|
||||
print_header("Terminal Backend")
|
||||
print_info("Choose where Hermes runs shell commands and code.")
|
||||
print_info("This affects tool execution, file access, and isolation.")
|
||||
print_info(f" Guide: {_DOCS_BASE}/user-guide/configuration#terminal-backend-configuration")
|
||||
print()
|
||||
_info("Choose where Hermes runs shell commands and code.",
|
||||
"This affects tool execution, file access, and isolation.",
|
||||
f" Guide: {_DOCS_BASE}/user-guide/configuration#terminal-backend-configuration", None)
|
||||
|
||||
current_backend = cfg_get(config, "terminal", "backend", default="local")
|
||||
backends = list(_BUILTIN_TERMINAL_BACKENDS)
|
||||
|
||||
@@ -106,7 +106,7 @@ def _run_xai_oauth_login_from_setup() -> bool:
|
||||
Returns True on success, False on any failure (the caller falls back
|
||||
to whatever the user picked next, e.g. Edge TTS).
|
||||
"""
|
||||
from hermes_cli.setup import print_info, print_warning
|
||||
from hermes_cli.setup import _info, print_warning
|
||||
try:
|
||||
from hermes_cli.auth import (
|
||||
_is_remote_session, _save_xai_oauth_tokens, _xai_oauth_device_code_login,
|
||||
@@ -117,8 +117,7 @@ def _run_xai_oauth_login_from_setup() -> bool:
|
||||
return False
|
||||
|
||||
open_browser = not _is_remote_session()
|
||||
print()
|
||||
print_info("Signing in to xAI Grok OAuth (SuperGrok / Premium+)...")
|
||||
_info(None, "Signing in to xAI Grok OAuth (SuperGrok / Premium+)...")
|
||||
try:
|
||||
creds = _xai_oauth_device_code_login(open_browser=open_browser)
|
||||
_save_xai_oauth_tokens(
|
||||
@@ -276,16 +275,15 @@ def _tts_xai_step(config: dict) -> str:
|
||||
def _setup_tts_provider(config: dict):
|
||||
"""Interactive TTS provider selection with install flow for local engines."""
|
||||
from hermes_cli.setup import (
|
||||
get_env_value, get_nous_subscription_features, managed_nous_tools_enabled, print_header, print_info,
|
||||
print_success, print_warning, prompt_choice, save_config,
|
||||
get_env_value, get_nous_subscription_features, _info, managed_nous_tools_enabled, print_header,
|
||||
print_info, print_success, print_warning, prompt_choice, save_config,
|
||||
)
|
||||
current_provider = config.get("tts", {}).get("provider", "edge")
|
||||
current_label = _TTS_PROVIDER_LABELS.get(current_provider, current_provider)
|
||||
|
||||
print()
|
||||
print_header("Text-to-Speech Provider (optional)")
|
||||
print_info(f"Current: {current_label}")
|
||||
print()
|
||||
_info(f"Current: {current_label}", None)
|
||||
|
||||
options = list(_TTS_PROVIDER_CHOICES)
|
||||
if managed_nous_tools_enabled() and get_nous_subscription_features(config).nous_auth_present:
|
||||
|
||||
Reference in New Issue
Block a user