refactor(cli/setup.py): first-time mode table, quick-setup env-var helper, agent-settings summary table

This commit is contained in:
Teknium
2026-09-02 16:01:34 -07:00
parent 3ab00ea93a
commit 1ac48ebe5a
6 changed files with 114 additions and 181 deletions

View File

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