Skill Sync only worked against a Nous Portal sync plane that was never made publicly accessible, so no install has ever used it. We don't want it in core right now, so this deletes it outright instead of moving it behind a plugin hook surface nobody needs yet: - `hermes sync` command, `tools/skills_sync_client*.py` and their tests - the personal pull ticks (gateway housekeeping, classic CLI startup, `hermes serve` maintenance) and the debounced post-write push - the per-skill `sync` usage flag (`set_sync` / `is_sync_enabled`) - the `skills/_org/<org_id>/` mirror: index gating on `.active_org`, the `[org-shared]` / name-collision labels, the org provenance header and `org_provenance` field on skill_view, the org delete guard and the auto-propose `org_sharing` note on skill_manage The code is preserved in the private NousResearch/hermes-skill-sync repo if the feature comes back.
258 lines
11 KiB
Python
258 lines
11 KiB
Python
"""Interactive messaging-platform setup wizards: WhatsApp (bridge + Cloud API), Slack manifest.
|
|
|
|
Split out of ``hermes_cli/main.py``. Names that still live in main are imported lazily at call time.
|
|
"""
|
|
|
|
import contextlib
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
from hermes_cli.cli_output import line_input
|
|
from hermes_cli.model_setup_flows_common import _say
|
|
|
|
|
|
def _err(msg: str) -> None:
|
|
print(msg, file=sys.stderr)
|
|
|
|
|
|
def _yes_no(prompt: str) -> bool:
|
|
"""``[y/N]`` prompt; Ctrl-C/EOF counts as "no"."""
|
|
try:
|
|
response = input(prompt).strip()
|
|
except (EOFError, KeyboardInterrupt):
|
|
response = "n"
|
|
return response.lower() in {"y", "yes"}
|
|
|
|
|
|
def _whatsapp_choose_mode(get_env_value, save_env_value):
|
|
"""Step 1 of ``hermes whatsapp``: ``"bot"`` / ``"self-chat"``, or None when cancelled."""
|
|
current_mode = get_env_value("WHATSAPP_MODE") or ""
|
|
if current_mode:
|
|
mode_label = "separate bot number" if current_mode == "bot" else "personal number (self-chat)"
|
|
print(f"\n✓ Mode: {mode_label}")
|
|
return current_mode
|
|
_say("", "How will you use WhatsApp with Hermes?", "",
|
|
" 1. Separate bot number (recommended)",
|
|
" People message the bot's number directly — cleanest experience.",
|
|
" Requires a second phone number with WhatsApp installed on a device.", "",
|
|
" 2. Personal number (self-chat)",
|
|
" You message yourself to talk to the agent.",
|
|
" Quick to set up, but the UX is less intuitive.", "")
|
|
try:
|
|
choice = input(" Choose [1/2]: ").strip()
|
|
except (EOFError, KeyboardInterrupt):
|
|
print("\nSetup cancelled.")
|
|
return None
|
|
if choice == "1":
|
|
save_env_value("WHATSAPP_MODE", "bot")
|
|
_say(" ✓ Mode: separate bot number", "",
|
|
" ┌─────────────────────────────────────────────────┐",
|
|
" │ Getting a second number for the bot: │",
|
|
" │ │",
|
|
" │ Easiest: Install WhatsApp Business (free app) │",
|
|
" │ on your phone with a second number: │",
|
|
" │ • Dual-SIM: use your 2nd SIM slot │",
|
|
" │ • Google Voice: free US number (voice.google) │",
|
|
" │ • Prepaid SIM: $3-10, verify once │",
|
|
" │ │",
|
|
" │ WhatsApp Business runs alongside your personal │",
|
|
" │ WhatsApp — no second phone needed. │",
|
|
" └─────────────────────────────────────────────────┘")
|
|
return "bot"
|
|
save_env_value("WHATSAPP_MODE", "self-chat")
|
|
print(" ✓ Mode: personal number (self-chat)")
|
|
return "self-chat"
|
|
|
|
|
|
def _whatsapp_allowed_users(wa_mode: str, get_env_value, save_env_value) -> None:
|
|
"""Step 3 of ``hermes whatsapp``: show / set WHATSAPP_ALLOWED_USERS."""
|
|
current_users = get_env_value("WHATSAPP_ALLOWED_USERS") or ""
|
|
if current_users:
|
|
print(f"✓ Allowed users: {current_users}")
|
|
if not _yes_no("\n Update allowed users? [y/N] "):
|
|
return
|
|
if wa_mode == "bot":
|
|
phone = line_input(" Phone numbers that can message the bot (comma-separated): ").strip()
|
|
else:
|
|
phone = line_input(" Your phone number (e.g. 15551234567): ").strip()
|
|
if phone:
|
|
save_env_value("WHATSAPP_ALLOWED_USERS", phone.replace(" ", ""))
|
|
print(f" ✓ Updated to: {phone}")
|
|
return
|
|
print()
|
|
if wa_mode == "bot":
|
|
print(" Who should be allowed to message the bot?")
|
|
phone = line_input(" Phone numbers (comma-separated, or * for anyone): ").strip()
|
|
else:
|
|
phone = line_input(" Your phone number (e.g. 15551234567): ").strip()
|
|
if phone:
|
|
save_env_value("WHATSAPP_ALLOWED_USERS", phone.replace(" ", ""))
|
|
print(f" ✓ Allowed users set: {phone}")
|
|
else:
|
|
print(" ⚠ No allowlist — the agent will respond to ALL incoming messages")
|
|
|
|
|
|
def _whatsapp_install_bridge(bridge_dir) -> bool:
|
|
"""Step 4 of ``hermes whatsapp``: ``npm install`` the bridge when needed. False = stop."""
|
|
from hermes_constants import find_node_executable, with_hermes_node_path
|
|
if (bridge_dir / "node_modules").exists():
|
|
print("✓ Bridge dependencies already installed")
|
|
return True
|
|
print("\n→ Installing WhatsApp bridge dependencies (this can take a few minutes)...")
|
|
import pm
|
|
|
|
npm = find_node_executable("npm")
|
|
try:
|
|
env = with_hermes_node_path()
|
|
if npm is None:
|
|
env = pm.ensure("npm", explicit=True).env
|
|
installed = pm.installed_package("npm")
|
|
if installed is None or installed.binary is None:
|
|
raise pm.InstallError("npm", "npm binary is missing after preparation")
|
|
npm = str(installed.binary)
|
|
result = subprocess.run(
|
|
[npm, "install", "--no-fund", "--no-audit", "--progress=false"],
|
|
cwd=str(bridge_dir), stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True,
|
|
encoding="utf-8", errors="replace", env=env)
|
|
except (pm.InstallError, OSError) as exc:
|
|
print(f" ✗ Bridge dependency preparation failed: {exc}")
|
|
return False
|
|
except KeyboardInterrupt:
|
|
print("\n ✗ Install cancelled")
|
|
return False
|
|
if result.returncode != 0:
|
|
err = (result.stderr or "").strip()
|
|
preview = "\n".join(err.splitlines()[-30:]) if err else "(no output)"
|
|
_say(" ✗ npm install failed:", preview)
|
|
return False
|
|
print(" ✓ Dependencies installed")
|
|
return True
|
|
|
|
|
|
def cmd_whatsapp(args):
|
|
"""Set up WhatsApp: choose mode, configure, install bridge, pair via QR."""
|
|
from hermes_cli.main import _require_tty, get_hermes_home
|
|
_require_tty("whatsapp")
|
|
from hermes_cli.config import get_env_value, save_env_value
|
|
from hermes_constants import find_node_executable, with_hermes_node_path
|
|
_say("", "☤ WhatsApp Setup", "=" * 50)
|
|
|
|
wa_mode = _whatsapp_choose_mode(get_env_value, save_env_value)
|
|
if wa_mode is None:
|
|
return
|
|
|
|
# WHATSAPP_ENABLED=true is deliberately NOT written here: an aborted wizard (Ctrl+C, failed npm
|
|
# install, missed QR scan) would leave .env claiming WhatsApp is ready with no creds.json, and
|
|
# every `hermes gateway` would pay a 30s bridge timeout + indefinite retries. Set only after
|
|
# pairing succeeds; prior successful pairings stay enabled.
|
|
print()
|
|
if (get_env_value("WHATSAPP_ENABLED") or "").lower() == "true":
|
|
print("✓ WhatsApp is already enabled")
|
|
|
|
_whatsapp_allowed_users(wa_mode, get_env_value, save_env_value)
|
|
|
|
from gateway.platforms.whatsapp_common import resolve_whatsapp_bridge_dir
|
|
bridge_dir = resolve_whatsapp_bridge_dir()
|
|
bridge_script = bridge_dir / "bridge.js"
|
|
if not bridge_script.exists():
|
|
print(f"\n✗ Bridge script not found at {bridge_script}")
|
|
return
|
|
if not _whatsapp_install_bridge(bridge_dir):
|
|
return
|
|
|
|
# Existing session: re-pair or keep.
|
|
session_dir = get_hermes_home() / "whatsapp" / "session"
|
|
session_dir.mkdir(parents=True, exist_ok=True)
|
|
if (session_dir / "creds.json").exists():
|
|
print("✓ Existing WhatsApp session found")
|
|
if _yes_no("\n Re-pair? This will clear the existing session. [y/N] "):
|
|
shutil.rmtree(session_dir, ignore_errors=True)
|
|
session_dir.mkdir(parents=True, exist_ok=True)
|
|
print(" ✓ Session cleared")
|
|
else:
|
|
# Older installs may have lost WHATSAPP_ENABLED; a kept pairing re-asserts it.
|
|
if (get_env_value("WHATSAPP_ENABLED") or "").lower() != "true":
|
|
save_env_value("WHATSAPP_ENABLED", "true")
|
|
_say("\n✓ WhatsApp is configured and paired!", " Start the gateway with: hermes gateway")
|
|
return
|
|
|
|
# QR code pairing
|
|
_say("", "─" * 50)
|
|
if wa_mode == "bot":
|
|
_say("📱 Open WhatsApp (or WhatsApp Business) on the", " phone with the BOT's number, then scan:")
|
|
else:
|
|
print("📱 Open WhatsApp on your phone, then scan:")
|
|
_say("", " Settings → Linked Devices → Link a Device", "─" * 50, "")
|
|
import pm
|
|
|
|
node = find_node_executable("node")
|
|
if node is None:
|
|
try:
|
|
pm.ensure("node", explicit=True)
|
|
installed = pm.installed_package("node")
|
|
if installed is None or installed.binary is None:
|
|
raise pm.InstallError("node", "Node.js binary is missing after preparation")
|
|
node = str(installed.binary)
|
|
except pm.InstallError as exc:
|
|
print(f" ✗ Node.js preparation failed: {exc}")
|
|
return
|
|
with contextlib.suppress(KeyboardInterrupt):
|
|
subprocess.run(
|
|
[node, str(bridge_script), "--pair-only", "--session", str(session_dir)],
|
|
cwd=str(bridge_dir), env=with_hermes_node_path())
|
|
|
|
print()
|
|
if not (session_dir / "creds.json").exists():
|
|
print("⚠ Pairing may not have completed. Run 'hermes whatsapp' to try again.")
|
|
return
|
|
# Only enable WhatsApp now that pairing actually succeeded (see above).
|
|
save_env_value("WHATSAPP_ENABLED", "true")
|
|
_say("✓ WhatsApp paired successfully!", "")
|
|
if wa_mode == "bot":
|
|
_say(" Next steps:", " 1. Start the gateway: hermes gateway",
|
|
" 2. Send a message to the bot's WhatsApp number",
|
|
" 3. The agent will reply automatically", "",
|
|
" Tip: Agent responses are prefixed with '☤ Hermes Agent'")
|
|
else:
|
|
_say(" Next steps:", " 1. Start the gateway: hermes gateway",
|
|
" 2. Open WhatsApp → Message Yourself",
|
|
" 3. Type a message — the agent will reply", "",
|
|
" Tip: Agent responses are prefixed with '☤ Hermes Agent'",
|
|
" so you can tell them apart from your own messages.")
|
|
_say("", " Or install as a service: hermes gateway install")
|
|
|
|
|
|
def cmd_whatsapp_cloud(args):
|
|
"""Set up WhatsApp Business Cloud API (official Meta integration) — complementary to the
|
|
``hermes whatsapp`` Baileys bridge wizard. See ``hermes_cli/setup_whatsapp_cloud.py``."""
|
|
from hermes_cli.main import _require_tty
|
|
_require_tty("whatsapp-cloud")
|
|
from hermes_cli.setup_whatsapp_cloud import run_whatsapp_cloud_setup
|
|
return run_whatsapp_cloud_setup()
|
|
|
|
|
|
def cmd_slack(args):
|
|
"""``hermes slack <subcommand>``; ``manifest`` prints or writes a Slack app manifest with
|
|
every gateway command registered as a first-class slash."""
|
|
sub = getattr(args, "slack_command", None)
|
|
if sub in {None, ""}:
|
|
_err("usage: hermes slack <subcommand>\n"
|
|
"\n"
|
|
"subcommands:\n"
|
|
" manifest Generate a Slack app manifest with every gateway\n"
|
|
" command registered as a native slash\n"
|
|
"\n"
|
|
"Run `hermes slack manifest -h` for details.")
|
|
return 1
|
|
|
|
if sub == "manifest":
|
|
from hermes_cli.slack_cli import slack_manifest_command
|
|
status = slack_manifest_command(args)
|
|
if status:
|
|
raise SystemExit(status)
|
|
return status
|
|
|
|
_err(f"Unknown slack subcommand: {sub}")
|
|
return 1
|