refactor(hermes_cli): setup cluster — drop statement-separating blanks inside function bodies (AST-neutral)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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]")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user