From bcddf6a084c92bfa7a06c4891f7119de6996a848 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:25:24 -0700 Subject: [PATCH] =?UTF-8?q?refactor(hermes=5Fcli):=20setup=20cluster=20?= =?UTF-8?q?=E2=80=94=20drop=20statement-separating=20blanks=20inside=20fun?= =?UTF-8?q?ction=20bodies=20(AST-neutral)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hermes_cli/secret_prompt.py | 2 -- hermes_cli/secrets_cli.py | 26 -------------------------- hermes_cli/send_cmd.py | 19 ------------------- hermes_cli/setup.py | 16 ---------------- hermes_cli/setup_migration.py | 7 ------- hermes_cli/setup_platforms.py | 22 ---------------------- hermes_cli/setup_quick.py | 16 ---------------- 7 files changed, 108 deletions(-) diff --git a/hermes_cli/secret_prompt.py b/hermes_cli/secret_prompt.py index b274c8f0b9..62191dea36 100644 --- a/hermes_cli/secret_prompt.py +++ b/hermes_cli/secret_prompt.py @@ -19,7 +19,6 @@ def _collect_masked_input( """Read one secret line while writing a mask character per typed char.""" value: list[str] = [] write(prompt) - while True: ch = read_char() if ch in _ENTER_CHARS: @@ -53,7 +52,6 @@ def masked_secret_prompt(prompt: str, *, mask: str = "*") -> str: """ if not _stream_is_tty(sys.stdin) or not _stream_is_tty(sys.stdout): return getpass.getpass(prompt) - masked = _masked_secret_prompt_windows if os.name == "nt" else _masked_secret_prompt_posix try: return masked(prompt, mask=mask) diff --git a/hermes_cli/secrets_cli.py b/hermes_cli/secrets_cli.py index 3b528899b0..ecf6ef08b5 100644 --- a/hermes_cli/secrets_cli.py +++ b/hermes_cli/secrets_cli.py @@ -165,11 +165,9 @@ def cmd_setup(args: argparse.Namespace) -> int: " Access tokens → Create access token\n\n" "Copy the token (starts with [cyan]0.[/cyan]…) — it cannot be retrieved later.", border_style="cyan")) - binary = _setup_binary(bw, console) if binary is None: return 1 - if not sys.stdin.isatty(): missing = _missing_noninteractive_flags(args) if missing: @@ -182,28 +180,24 @@ def cmd_setup(args: argparse.Namespace) -> int: " --server-url 'https://vault.bitwarden.com' \\\n" " --project-id 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'") return 1 - cfg = load_config() secrets_cfg = cfg.setdefault("secrets", {}).setdefault("bitwarden", {}) token_env = secrets_cfg.get("access_token_env", _DEFAULT_TOKEN_ENV) token = _setup_token(args, console, token_env) if token is None: return 1 - _step(console, 3, "Pick a Bitwarden region") server_url = _resolve_server_url(args, secrets_cfg, console) if server_url is None: return 1 console.print(f" [green]✓[/green] using {server_url}" if server_url else " [green]✓[/green] using bws default (US Cloud, https://vault.bitwarden.com)") - project_id = (args.project_id or "").strip() project_given = bool(project_id) if not project_given: project_id = _setup_project(binary, token, console, server_url) if project_id is None: return 1 - _step(console, 4 if project_given else 5, "Test fetch") try: secrets, warnings = bw.fetch_bitwarden_secrets( @@ -211,7 +205,6 @@ def cmd_setup(args: argparse.Namespace) -> int: except Exception as exc: # noqa: BLE001 console.print(f" [red]✗ Fetch failed: {exc}[/red]") return 1 - if not secrets: console.print(" [yellow]Fetch succeeded but the project has no secrets.[/yellow]") else: @@ -219,13 +212,11 @@ def cmd_setup(args: argparse.Namespace) -> int: ((key, _fetch_status(key, token_env)) for key in sorted(secrets))) for w in warnings: console.print(f" [yellow]warning:[/yellow] {w}") - secrets_cfg.update(enabled=True, project_id=project_id, server_url=server_url) for key, default in (("access_token_env", token_env), ("cache_ttl_seconds", 300), ("override_existing", True), ("auto_install", True)): secrets_cfg.setdefault(key, default) save_config(cfg) - console.print() console.print("[green]✓ Bitwarden Secrets Manager is enabled.[/green] " "Secrets will be pulled at the start of every Hermes process.") @@ -251,7 +242,6 @@ def cmd_status(args: argparse.Namespace) -> int: bw = _load_bw() console = Console() bw_cfg = _bw_cfg(load_config()) - enabled = bool(bw_cfg.get("enabled")) token_env = bw_cfg.get("access_token_env", _DEFAULT_TOKEN_ENV) project_id = bw_cfg.get("project_id", "") @@ -260,7 +250,6 @@ def cmd_status(args: argparse.Namespace) -> int: binary = bw.find_bws(install_if_missing=False) token_validation, validation_messages = _token_validation_status( enabled=enabled, binary=binary, token=token, server_url=server_url) - print_status_panel(console, "Bitwarden Secrets Manager", ( ("Enabled", _yn(enabled)), ("Token env var", token_env), @@ -275,7 +264,6 @@ def cmd_status(args: argparse.Namespace) -> int: )) for message in validation_messages: console.print(message) - if not enabled: console.print("\n Run [cyan]hermes secrets bitwarden setup[/cyan] to enable.") return 0 @@ -346,7 +334,6 @@ def cmd_sync(args: argparse.Namespace) -> int: bw_cfg = _bw_cfg(load_config()) if not require_enabled(console, bw_cfg, "Bitwarden", "bitwarden"): return 1 - token_env = bw_cfg.get("access_token_env", _DEFAULT_TOKEN_ENV) token = os.environ.get(token_env, "").strip() if not token: @@ -356,7 +343,6 @@ def cmd_sync(args: argparse.Namespace) -> int: if not project_id: console.print("[red]No project_id configured.[/red]") return 1 - try: secrets, warnings = bw.fetch_bitwarden_secrets( access_token=token, project_id=project_id, use_cache=False, server_url=cfg_str(bw_cfg, "server_url"), @@ -364,11 +350,9 @@ def cmd_sync(args: argparse.Namespace) -> int: except Exception as exc: # noqa: BLE001 console.print(f"[red]Fetch failed: {exc}[/red]") return 1 - if not secrets: console.print("[yellow]No secrets in project.[/yellow]") return 0 - override = bool(bw_cfg.get("override_existing", False)) or args.apply rows = [] applied = 0 @@ -385,9 +369,7 @@ def cmd_sync(args: argparse.Namespace) -> int: else: action = "[green]would export[/green]" + (" (overrides)" if already else "") rows.append((key, action)) - print_table(console, (("Name", {"style": "cyan"}), "Action"), rows, warnings) - if not args.apply: console.print("\n This was a dry-run — secrets are picked up automatically on the " "next [cyan]hermes[/cyan] invocation. Re-run with [cyan]--apply[/cyan] " @@ -428,11 +410,9 @@ def _token_validation_status( (binary is None, "bws not installed")): if skipped: return f"[dim]not checked[/dim] ({reason})", [] - messages: list[str] = [] if not token.startswith("0."): messages.append(_NOT_BSM_TOKEN_WARNING_CONTINUING) - probe_console = Console(file=io.StringIO(), record=True, width=200) if _list_projects(binary, token, probe_console, server_url=server_url) is None: details = probe_console.export_text(styles=False).strip() @@ -471,7 +451,6 @@ def _list_projects( except (OSError, subprocess.TimeoutExpired) as exc: console.print(f" [red]Couldn't list projects: {exc}[/red]") return None - if res.returncode != 0: err = (res.stderr or res.stdout).strip()[:300] console.print(f" [red]bws project list failed: {err}[/red]") @@ -481,7 +460,6 @@ def _list_projects( console.print(hint) break return None - try: data = json.loads(res.stdout or "[]") except json.JSONDecodeError as exc: @@ -507,17 +485,14 @@ def _resolve_server_url( printing) when a custom URL is left empty.""" if args.server_url and args.server_url.strip(): return args.server_url.strip() - env_url = os.environ.get("BWS_SERVER_URL", "").strip() if env_url: console.print(f" Detected [cyan]BWS_SERVER_URL[/cyan]={env_url} in your shell — using it.") return env_url - existing = cfg_str(secrets_cfg, "server_url") if existing: console.print(f" Existing config: [cyan]{existing}[/cyan]. " "Press Enter to keep, or pick a different option below.") - table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2)) table.add_column("#", style="cyan", width=4) table.add_column("Region / endpoint") @@ -526,7 +501,6 @@ def _resolve_server_url( custom_idx = len(_REGION_PRESETS) + 1 table.add_row(str(custom_idx), "Self-hosted / custom URL") console.print(table) - prompt = f" Select region [1-{custom_idx}]" + (" (Enter to keep current)" if existing else "") idx = prompt_index(console, prompt + ": ", custom_idx, allow_empty=bool(existing), empty_message=" [red]Enter a number.[/red]") diff --git a/hermes_cli/send_cmd.py b/hermes_cli/send_cmd.py index 49dea16a32..b62775a5fd 100644 --- a/hermes_cli/send_cmd.py +++ b/hermes_cli/send_cmd.py @@ -29,7 +29,6 @@ def _read_message_body(positional: Optional[str], file_path: Optional[str]) -> O piped stdin when not attached to a TTY. ``None`` when nothing is available (a usage error).""" if positional: return positional - if file_path: if file_path == "-": return sys.stdin.read() @@ -61,7 +60,6 @@ def _emit_result(result_json: str, *, json_mode: bool, quiet: bool) -> int: except json.JSONDecodeError: # Pass the raw string through so the user can still see what went wrong. payload = {"error": "invalid JSON from send_message_tool", "raw": result_json} - if json_mode: print(json.dumps(payload, indent=2)) elif not quiet: @@ -71,7 +69,6 @@ def _emit_result(result_json: str, *, json_mode: bool, quiet: bool) -> int: print(payload.get("note") or "sent") else: print(json.dumps(payload, indent=2)) # unknown shape — dump it, drop nothing - if not payload.get("error") and (payload.get("skipped") or payload.get("success")): return _SUCCESS_EXIT return _FAILURE_EXIT @@ -84,12 +81,10 @@ def _list_targets(platform_filter: Optional[str], *, json_mode: bool) -> int: from gateway.channel_directory import format_directory_for_display, load_directory except Exception as exc: return _fail(f"hermes send: failed to load channel directory: {exc}") - try: raw = load_directory() except Exception as exc: return _fail(f"hermes send: failed to read channel directory: {exc}") - platforms = dict(raw.get("platforms") or {}) # Merge in configured-but-undiscovered platforms (e.g. a fresh SimpleX setup used only for @@ -102,7 +97,6 @@ def _list_targets(platform_filter: Optional[str], *, json_mode: bool) -> int: platforms.setdefault(plat_name, []) except Exception: pass # directory contents alone are still useful; don't fail --list on a config problem - if platform_filter: key = platform_filter.strip().lower() filtered = {k: v for k, v in platforms.items() if k.lower() == key} @@ -111,11 +105,9 @@ def _list_targets(platform_filter: Optional[str], *, json_mode: bool) -> int: f"hermes send: no targets found for platform '{platform_filter}'. " f"Configured: {', '.join(sorted(platforms)) or '(none)'}") platforms = filtered - if json_mode: print(json.dumps({"platforms": platforms}, indent=2, default=str)) return _SUCCESS_EXIT - if not platforms: print("No messaging platforms configured or no channels discovered yet.") print("Set one up with `hermes gateway setup`, or run the gateway once so") @@ -126,7 +118,6 @@ def _list_targets(platform_filter: Optional[str], *, json_mode: bool) -> int: if platform_filter is None: print(format_directory_for_display(platforms)) return _SUCCESS_EXIT - for plat_name in sorted(platforms): print(f"{plat_name}:") if not platforms[plat_name]: @@ -147,13 +138,11 @@ def _load_hermes_env() -> None: from dotenv import load_dotenv except Exception: load_dotenv = None # type: ignore[assignment] - try: from hermes_cli.config import get_hermes_home home = get_hermes_home() except Exception: return - env_path = home / ".env" if load_dotenv and env_path.exists(): try: @@ -179,14 +168,12 @@ def _load_hermes_env() -> None: config_path = home / "config.yaml" if not config_path.exists(): return - try: # Raw read is deliberate — only keys the user actually wrote get bridged. from hermes_cli.config import read_user_config_raw raw = read_user_config_raw(config_path) except Exception: return - try: from hermes_cli.config import _expand_env_vars raw = _expand_env_vars(raw) @@ -199,10 +186,8 @@ def _load_hermes_env() -> None: raw = managed_scope.apply_managed_overlay(raw if isinstance(raw, dict) else {}) except Exception: pass - if not isinstance(raw, dict): return - for key, val in raw.items(): if isinstance(val, (str, int, float, bool)) and key not in os.environ: os.environ[key] = str(val) @@ -211,12 +196,10 @@ def _load_hermes_env() -> None: def cmd_send(args: argparse.Namespace) -> None: """Entry point wired into the top-level argparse dispatcher.""" _load_hermes_env() # the downstream gateway config loader reads credentials from os.environ - if getattr(args, "list_targets", False): # --list short-circuits everything else # `hermes send --list telegram` lands "telegram" in the `message` positional. exit_code = _list_targets(getattr(args, "message", None), json_mode=getattr(args, "json", False)) sys.exit(exit_code) - target = (getattr(args, "to", None) or "").strip() if not target: _fail( @@ -226,7 +209,6 @@ def cmd_send(args: argparse.Namespace) -> None: " hermes send --to discord:#ops --file report.md\n" " hermes send --list # list available targets", _USAGE_EXIT) - message = _read_message_body(getattr(args, "message", None), getattr(args, "file", None)) if message is None or not message.strip(): _fail( @@ -292,7 +274,6 @@ def register_send_subparser(subparsers) -> argparse.ArgumentParser: "Exit codes: 0 ok, 1 delivery/backend error, 2 usage error." ), formatter_class=argparse.RawDescriptionHelpFormatter) - for flags, kwargs in _SEND_ARGUMENTS: parser.add_argument(*flags, **kwargs) parser.set_defaults(func=cmd_send) diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index d3cd3a090a..80ddfc0a05 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -290,7 +290,6 @@ def prompt_yes_no(question: str, default: bool = True) -> bool: # consistently; every other caller keeps the traditional line prompt. if _SETUP_NAVIGATION.get() is not None: return _curses_prompt_choice(question, ["Yes", "No"], 0 if default else 1) == 0 - default_str = "Y/n" if default else "y/N" while True: try: @@ -323,7 +322,6 @@ def _prompt_api_key(var: dict): tools_str = ", ".join(tools[:3]) if len(tools) > 3: tools_str += f", +{len(tools) - 3} more" - print() print(color(f" ─── {var.get('description', var['name'])} ───", Colors.CYAN)) print() @@ -372,7 +370,6 @@ def setup_model_provider(config: dict, *, quick: bool = False): print_header("Inference Provider") _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() @@ -405,7 +402,6 @@ def _apply_default_agent_settings(config: dict): config["compression"]["threshold"] = 0.50 # Never auto-reset (the gateway default); written explicitly so it is visible in config.yaml. config.setdefault("session_reset", {})["mode"] = "none" - save_config(config) print_success("Applied recommended defaults:") _info(" Max iterations: 150", " Tool progress: all", " Compression threshold: 0.50", @@ -456,7 +452,6 @@ _SESSION_RESET_MODES = ("both", "idle", "daily", "none") # index 4 = keep curre def setup_agent_settings(config: dict): """Configure agent behavior: iterations, progress display, compression, session reset.""" - print_header("Agent Settings") _info(f" Guide: {_DOCS_BASE}/user-guide/configuration", None) @@ -465,7 +460,6 @@ def setup_agent_settings(config: dict): _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.") - max_iter = _prompt_number("Max iterations", current_max) if max_iter is None: print_warning("Invalid number, keeping current value") @@ -478,7 +472,6 @@ def setup_agent_settings(config: dict): # ── Tool Progress Display ── _info("", *_TOOL_PROGRESS_HELP) - current_mode = cfg_get(config, "display", "tool_progress", default="all") mode = prompt("Tool progress mode", current_mode) if mode.lower() in {"off", "new", "all", "verbose", "log"}: @@ -492,7 +485,6 @@ def setup_agent_settings(config: dict): print_header("Context Compression") _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 current_threshold = cfg_get(config, "compression", "threshold", default=0.50) threshold = _prompt_number("Compression threshold (0.5-0.95)", current_threshold, float) @@ -563,7 +555,6 @@ def setup_telemetry(config: dict): print_header("Shared Metrics") _info("Shared metrics contain only bounded counters and histograms.", "Collection is local. Sending them to Nous is a separate opt-in.") - shared_metrics = _sub_dict(_sub_dict(config, "telemetry"), "shared_metrics") current = shared_metrics.get("enabled") is True shared_metrics["enabled"] = prompt_yes_no("Enable local shared metrics?", default=current) @@ -577,7 +568,6 @@ def setup_telemetry(config: dict): # key may already be false while the consent window is still open, and it must close. _record_send_consent_change(enabled=False) return - print_success("Local shared metrics enabled.") _info(*_SEND_CONSENT_EXPLAINER) shared_metrics["send"] = prompt_yes_no("Send shared metrics to Nous?", default=shared_metrics.get("send") is True) @@ -675,7 +665,6 @@ def _run_setup_section(config: dict, section: str) -> None: print() print_success(f"{label} configuration complete!") return - print_error(f"Unknown setup section: {section}") print_info(f"Available sections: {', '.join(k for k, _, _ in SETUP_SECTIONS)}") @@ -686,7 +675,6 @@ def _run_full_setup(config: dict, hermes_home, *, is_existing: bool, migration_r _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.", "Each section below will show what was imported — press Enter to keep,", @@ -736,13 +724,11 @@ def _run_setup_wizard_impl(args): managed_error("run setup wizard") return ensure_hermes_home() - 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() @@ -768,7 +754,6 @@ def _run_setup_wizard_impl(args): "├─────────────────────────────────────────────────────────┤", "│ Let's configure your Hermes Agent installation. │", "│ Press Ctrl+C at any time to exit. │") - migration_ran = False if is_existing: # Full reconfigure wizard is the default (Enter keeps each current value); `--quick` @@ -796,7 +781,6 @@ def _run_setup_wizard_impl(args): if runner is not None: _run_setup_steps([(label, lambda: globals()[runner](config, hermes_home, is_existing))]) return - _run_full_setup(config, hermes_home, is_existing=is_existing, migration_ran=migration_ran) # Save and show summary diff --git a/hermes_cli/setup_migration.py b/hermes_cli/setup_migration.py index 3cf0870bbb..295804d42d 100644 --- a/hermes_cli/setup_migration.py +++ b/hermes_cli/setup_migration.py @@ -198,7 +198,6 @@ def _print_migration_preview(report: dict): if not items: print_info("Nothing to migrate.") return - groups = ( ("migrated", " Would import:", Colors.GREEN, _migrated_row), ("conflict", " Would overwrite (conflicts with existing Hermes config):", Colors.YELLOW, @@ -219,7 +218,6 @@ def _print_migration_preview(report: dict): warnings_shown.update( w for kw, w in _HIGH_IMPACT_KIND_KEYWORDS.items() if kw in kind_lower or kw in dest_lower) print() - if warnings_shown: print(color(" ── Warnings ──", Colors.YELLOW)) for warning in sorted(warnings_shown): @@ -264,7 +262,6 @@ def _offer_openclaw_migration(hermes_home: Path) -> bool: openclaw_dir = Path.home() / ".openclaw" if not openclaw_dir.is_dir() or not _OPENCLAW_SCRIPT.exists(): return False - print_header("OpenClaw Installation Detected", gap=True) _info(f"Found OpenClaw data at {openclaw_dir}", "Hermes can preview what would be imported before making any changes.", None) @@ -275,7 +272,6 @@ def _offer_openclaw_migration(hermes_home: Path) -> bool: # Ensure config.yaml exists before migration tries to read it if not get_config_path().exists(): save_config(load_config()) - mod = _migration_step("Could not load migration script", "OpenClaw migration module load error", _load_openclaw_migration_module) if mod is None: @@ -292,7 +288,6 @@ def _offer_openclaw_migration(hermes_home: Path) -> bool: if previewed is _FAILED: return False selected, preview_report = previewed - preview_count = preview_report.get("summary", {}).get("migrated", 0) if preview_count == 0: _info(None, "Nothing to import from OpenClaw.") @@ -313,7 +308,6 @@ def _offer_openclaw_migration(hermes_home: Path) -> bool: mod, openclaw_dir, hermes_home, selected, execute=True, overwrite=False)) if report is _FAILED: return False - summary = report.get("summary", {}) print() for key, printer, text in ( @@ -326,7 +320,6 @@ def _offer_openclaw_migration(hermes_home: Path) -> bool: count = summary.get(key, 0) if count: printer(text.format(n=count)) - output_dir = report.get("output_dir") if output_dir: print_info(f"Full report saved to: {output_dir}") diff --git a/hermes_cli/setup_platforms.py b/hermes_cli/setup_platforms.py index 54a7799e79..fb647cff79 100644 --- a/hermes_cli/setup_platforms.py +++ b/hermes_cli/setup_platforms.py @@ -28,7 +28,6 @@ def _setup_telegram_auto_result(): from hermes_cli.telegram_managed_bot import auto_setup_telegram_bot_result except ImportError: return None - profile_name: str | None = None try: profile_name = _profile_name_from_hermes_home(Path(get_hermes_home())) @@ -126,7 +125,6 @@ def _obtain_telegram_token(): " No token copy-paste needed.", None, " [2] Manual", " Create a bot via @BotFather yourself and paste the token.", None) - token = setup_result = None if prompt("Choice [1/2]", default="1").strip() == "1": setup_result = _setup_telegram_auto_result() @@ -137,7 +135,6 @@ def _obtain_telegram_token(): token = setup_result = None if not token: _info(None, "Falling back to manual setup...", None) - if not token: token = _prompt_telegram_bot_token() return token, setup_result @@ -150,19 +147,15 @@ def _setup_telegram(): if _declines_reconfigure("TELEGRAM_BOT_TOKEN", "Telegram", "Reconfigure Telegram?"): _telegram_allowlist_nudge() return - token, setup_result = _obtain_telegram_token() if not token: return - save_env_value("TELEGRAM_BOT_TOKEN", token) print_success("Telegram token saved") - _info(None, "🔒 Security: Restrict who can use your bot", " To find your Telegram user ID:", " 1. Message @userinfobot on Telegram", " 2. It will reply with your numeric ID (e.g., 123456789)", None) - allowed_users = None detected_id = str(getattr(setup_result, "owner_user_id", None) or "") if detected_id: @@ -174,11 +167,9 @@ def _setup_telegram(): "TELEGRAM_ALLOWED_USERS", "Allowed user IDs (comma-separated, leave empty for open access)", "Telegram allowlist configured - only listed users can use the bot", "⚠️ No allowlist set - anyone who finds your bot can use it!", preset=allowed_users) - _info(None, "📬 Home Channel: where Hermes delivers cron job results,", " cross-platform messages, and notifications.", " For Telegram DMs, this is your user ID (same as above).") - first_user_id = allowed_users.split(",")[0].strip() if allowed_users else "" if not first_user_id: print_info(" You can also set this later by typing /set-home in your Telegram chat.") @@ -196,13 +187,11 @@ def _setup_bluebubbles(): print_header("BlueBubbles (iMessage)") if _declines_reconfigure("BLUEBUBBLES_SERVER_URL", "BlueBubbles", "Reconfigure BlueBubbles?"): return - _info("Connects Hermes to iMessage via BlueBubbles — a free, open-source", "macOS server that bridges iMessage to any device.", " Requires a Mac running BlueBubbles Server v1.0.0+", " Download: https://bluebubbles.app/", None, "In BlueBubbles Server → Settings → API, note your Server URL and Password.", None) - for label, env_var, secret, what, transform in ( ("BlueBubbles server URL (e.g. http://192.168.1.10:1234)", "BLUEBUBBLES_SERVER_URL", False, "Server URL", lambda v: v.rstrip("/")), @@ -212,20 +201,16 @@ def _setup_bluebubbles(): skip_msg=f"{what} is required — skipping BlueBubbles setup"): return print_success("BlueBubbles credentials saved") - _info(None, "🔒 Security: Restrict who can message your bot", " Use iMessage addresses: email (user@icloud.com) or phone (+15551234567)", None) _prompt_allowlist("BLUEBUBBLES_ALLOWED_USERS", "Allowed iMessage addresses (comma-separated, leave empty for open access)", "BlueBubbles allowlist configured", "⚠️ No allowlist set — anyone who can iMessage you can use the bot!") - _info(None, "📬 Home Channel: phone or email for cron job delivery and notifications.", " You can also set this later with /set-home in your iMessage chat.") _save_prompted("BLUEBUBBLES_HOME_CHANNEL", "Home channel address (leave empty to set later)") - _info(None, "Advanced settings (defaults are fine for most setups):") if prompt_yes_no("Configure webhook listener settings?", False): _save_port("BLUEBUBBLES_WEBHOOK_PORT", prompt("Webhook listener port (default: 8645)"), "8645") - _info(None, "Requires the BlueBubbles Private API helper for typing indicators,", "read receipts, and tapback reactions. Basic messaging works without it.", " Install: https://docs.bluebubbles.app/helper-bundle/installation") @@ -237,19 +222,16 @@ def _setup_webhooks(): print_header("Webhooks") if _declines_reconfigure("WEBHOOK_ENABLED", "Webhooks", "Reconfigure webhooks?"): return - print() print_warning("⚠ Webhook and SMS platforms require exposing gateway ports to the") print_warning(" internet. For security, run the gateway in a sandboxed environment") print_warning(" (Docker, VM, etc.) to limit blast radius from prompt injection.") print() _info(" Full guide: https://hermes-agent.nousresearch.com/docs/user-guide/messaging/webhooks/", None) - _save_port("WEBHOOK_PORT", prompt("Webhook port (default 8644)"), "8644") _save_prompted("WEBHOOK_SECRET", "Global HMAC secret (shared across all routes)", password=True, success_msg="Webhook secret saved", skip_msg="No secret set — you must configure per-route secrets in config.yaml") - save_env_value("WEBHOOK_ENABLED", "true") print() print_success("Webhooks enabled! Next steps:") @@ -339,7 +321,6 @@ def setup_gateway(config: dict): print_header("Messaging Platforms") _info("Connect to messaging platforms to chat with Hermes from anywhere.", "Toggle with Space, confirm with Enter.", None) - platforms = _all_platforms() # Build checklist, pre-selecting already-configured platforms. @@ -347,7 +328,6 @@ def setup_gateway(config: dict): items = [f"{plat['emoji']} {plat['label']} ({status})" for plat, status in zip(platforms, statuses)] pre_selected = [i for i, status in enumerate(statuses) if status == "configured"] selected = prompt_checklist("Select platforms to configure:", items, pre_selected) - if not selected: print_info("No platforms selected. Run 'hermes setup gateway' later to configure.") for idx in selected or (): @@ -368,12 +348,10 @@ def setup_gateway(config: dict): # with cron jobs and bot tokens but no process to serve them. from hermes_cli.gateway import _is_service_running, supports_systemd_services, ensure_gateway_service supports_systemd = supports_systemd_services() - print() if _is_service_running(): _restart_running_gateway(any_messaging, supports_systemd) else: # Not running: install (if needed) and start, no questions asked. ensure_gateway_service(context="setup") - print_info(_RULE) diff --git a/hermes_cli/setup_quick.py b/hermes_cli/setup_quick.py index ba8d32e32a..7bcae39fe4 100644 --- a/hermes_cli/setup_quick.py +++ b/hermes_cli/setup_quick.py @@ -77,7 +77,6 @@ def _run_portal_one_shot(config: dict) -> None: _reload_config_into(config, dict_only=True) except Exception: pass - print() print_success("Portal setup complete.") _info(" Run `hermes portal info` to inspect routing.", " Run `hermes` to start chatting.") @@ -116,7 +115,6 @@ def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool): gateway_choice = prompt_choice("Connect a messaging platform? (Telegram, Discord, etc.)", [ "Set up messaging now (recommended)", "Skip — set up later with 'hermes setup gateway'", ], 0) - if gateway_choice == 0: setup_gateway(config) save_config(config) @@ -125,7 +123,6 @@ def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool): # platforms come alive as soon as tokens are added later (e.g. via `hermes import`). from hermes_cli.gateway import ensure_gateway_service ensure_gateway_service(context="setup") - print() print_success("Setup complete! You're ready to go.") _info(None, " Configure all settings: hermes setup") @@ -133,7 +130,6 @@ def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool): print_info(" Connect Telegram/Discord: hermes setup gateway") _print_macos_fda_tip() print() - _print_setup_summary(config, hermes_home) @@ -166,7 +162,6 @@ def _blank_slate_minimal_toolsets(config: dict): in ``_get_platform_tools``, overriding the recovery that would re-add e.g. ``kanban``).""" keep = {"file", "terminal", "vision", "skills"} config.setdefault("platform_toolsets", {})["cli"] = sorted(keep) - try: from toolsets import TOOLSETS from hermes_cli.tools_config import CONFIGURABLE_TOOLSETS, _get_plugin_toolset_keys @@ -179,7 +174,6 @@ def _blank_slate_minimal_toolsets(config: dict): if k.startswith("hermes-") or (isinstance(tdef, dict) and (tdef.get("includes") or tdef.get("posture"))): continue all_keys.add(k) - disabled = sorted(all_keys - keep) if disabled: config.setdefault("agent", {})["disabled_toolsets"] = disabled @@ -257,11 +251,9 @@ def _run_blank_slate_setup(config: dict, hermes_home, is_existing: bool): "Start with everything disabled — finish now (most minimal)", "Walk through all configurations — opt in to tools, skills, plugins, MCP", ], 0) - if path != 0: _blank_slate_walkthrough(config, hermes_home) return - save_config(config) # Blank Slate means no bundled skills; record the opt-out so future `hermes update` runs # don't re-inject them. @@ -327,7 +319,6 @@ def _blank_slate_walkthrough(config: dict, hermes_home): print() if prompt_yes_no("Connect a messaging platform (Telegram, Discord, …)?", default=False): setup_gateway(config) - save_config(config) _blank_slate_done(config, hermes_home, " Enable more tools: hermes tools") @@ -346,13 +337,11 @@ def _run_quick_setup(config: dict, hermes_home): missing_optional = [v for v in get_missing_env_vars(required_only=False) if not v.get("is_required")] missing_config = get_missing_config_fields() current_ver, latest_ver = check_config_version() - if not (missing_required or missing_optional or missing_config or current_ver < latest_ver): print_success("Everything is configured! Nothing to do.") _info(None, "Run 'hermes setup' and choose 'Full Setup' to reconfigure,", "or pick a specific section from the menu.") return - if missing_required: _info(None, f"{len(missing_required)} required setting(s) missing:") for var in missing_required: @@ -365,17 +354,14 @@ def _run_quick_setup(config: dict, hermes_home): 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']}") - 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")] - if missing_tools: # checklist, then the API-key screen for each pick print_header("Tool API Keys", gap=True) labels = [var.get("description", var["name"]) + (f" → {', '.join(var['tools'][:2])}" if var.get("tools") else "") for var in missing_tools] for idx in prompt_checklist("Which tools would you like to configure?", labels): _prompt_api_key(missing_tools[idx]) - if missing_messaging: # checklist, then prompt for each selected platform's vars print_header("Messaging Platforms", gap=True) _info("Connect Hermes to messaging apps to chat from anywhere.", @@ -407,8 +393,6 @@ def _run_quick_setup(config: dict, hermes_home): _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']}") - config["_config_version"] = latest_ver save_config(config) - _print_setup_summary(config, hermes_home)