refactor(hermes_cli): setup cluster — drop statement-separating blanks inside function bodies (AST-neutral)

This commit is contained in:
Teknium
2026-09-02 22:25:24 -07:00
parent 17d20ae1e5
commit bcddf6a084
7 changed files with 0 additions and 108 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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