diff --git a/README.md b/README.md index 018120389..d038cd582 100644 --- a/README.md +++ b/README.md @@ -271,22 +271,30 @@ SLACK_ALLOWED_USERS=U01234ABCDE # Comma-separated Slack user IDs ### WhatsApp Setup -WhatsApp doesn't have a simple bot API like Telegram or Discord. Hermes includes a built-in bridge using [Baileys](https://github.com/WhiskeySockets/Baileys) that connects via WhatsApp Web. The agent links to your WhatsApp account and responds to incoming messages. +WhatsApp doesn't have a simple bot API like Telegram or Discord. Hermes includes a built-in bridge using [Baileys](https://github.com/WhiskeySockets/Baileys) that connects via WhatsApp Web. -1. **Run the setup command:** +**Two modes are supported:** + +| Mode | How it works | Best for | +|------|-------------|----------| +| **Separate bot number** (recommended) | Dedicate a phone number to the bot. People message that number directly. | Clean UX, multiple users | +| **Personal self-chat** | Use your own WhatsApp. You message yourself to talk to the agent. | Quick setup, single user | + +**Setup:** ```bash hermes whatsapp ``` -This will: -- Enable WhatsApp in your config -- Ask for your phone number (for the allowlist) -- Install bridge dependencies (Node.js required) -- Display a QR code — scan it with your phone (WhatsApp → Settings → Linked Devices → Link a Device) -- Exit automatically once paired +The wizard will: +1. Ask which mode you want +2. For **bot mode**: guide you through getting a second number (WhatsApp Business app on a dual-SIM, Google Voice, or cheap prepaid SIM) +3. Configure the allowlist +4. Install bridge dependencies (Node.js required) +5. Display a QR code — scan from WhatsApp (or WhatsApp Business) → Settings → Linked Devices → Link a Device +6. Exit once paired -2. **Start the gateway:** +**Start the gateway:** ```bash hermes gateway # Foreground @@ -295,7 +303,7 @@ hermes gateway install # Or install as a system service (Linux) The gateway starts the WhatsApp bridge automatically using the saved session. -> **Note:** WhatsApp Web sessions can disconnect if WhatsApp updates their protocol. The gateway reconnects automatically. If you see persistent failures, re-pair with `hermes whatsapp`. Agent responses are prefixed with "⚕ Hermes Agent" so you can distinguish them from your own messages in self-chat. +> **Note:** WhatsApp Web sessions can disconnect if WhatsApp updates their protocol. The gateway reconnects automatically. If you see persistent failures, re-pair with `hermes whatsapp`. Agent responses are prefixed with "⚕ Hermes Agent" for easy identification. See [docs/messaging.md](docs/messaging.md) for advanced WhatsApp configuration. @@ -1635,6 +1643,7 @@ All variables go in `~/.hermes/.env`. Run `hermes config set VAR value` to set t | `SLACK_ALLOWED_USERS` | Comma-separated Slack user IDs | | `SLACK_HOME_CHANNEL` | Default Slack channel for cron delivery | | `WHATSAPP_ENABLED` | Enable WhatsApp bridge (`true`/`false`) | +| `WHATSAPP_MODE` | `bot` (separate number, recommended) or `self-chat` (message yourself) | | `WHATSAPP_ALLOWED_USERS` | Comma-separated phone numbers (with country code) | | `MESSAGING_CWD` | Working directory for terminal in messaging (default: ~) | | `GATEWAY_ALLOW_ALL_USERS` | Allow all users without allowlist (`true`/`false`, default: `false`) | diff --git a/docs/messaging.md b/docs/messaging.md index e695308b4..afcebc474 100644 --- a/docs/messaging.md +++ b/docs/messaging.md @@ -141,7 +141,12 @@ pip install discord.py>=2.0 ### WhatsApp -WhatsApp uses a built-in bridge powered by [Baileys](https://github.com/WhiskeySockets/Baileys) that connects via WhatsApp Web. The agent links to your WhatsApp account and responds to incoming messages. +WhatsApp uses a built-in bridge powered by [Baileys](https://github.com/WhiskeySockets/Baileys) that connects via WhatsApp Web. + +**Two modes:** + +- **`bot` mode (recommended):** Use a dedicated phone number for the bot. Other people message that number directly. All `fromMe` messages are treated as bot echo-backs and ignored. +- **`self-chat` mode:** Use your own WhatsApp account. You talk to the agent by messaging yourself (WhatsApp → "Message Yourself"). **Setup:** @@ -149,12 +154,7 @@ WhatsApp uses a built-in bridge powered by [Baileys](https://github.com/WhiskeyS hermes whatsapp ``` -This will: -- Enable WhatsApp in your `.env` -- Ask for your phone number (for the allowlist) -- Install bridge dependencies (Node.js required) -- Display a QR code — scan it with your phone (WhatsApp → Settings → Linked Devices → Link a Device) -- Exit automatically once paired +The wizard walks you through mode selection, allowlist configuration, dependency installation, and QR code pairing. For bot mode, you'll need a second phone number with WhatsApp installed on some device (dual-SIM with WhatsApp Business app is the easiest approach). Then start the gateway: @@ -162,16 +162,23 @@ Then start the gateway: hermes gateway ``` -The gateway starts the WhatsApp bridge automatically using the saved session credentials in `~/.hermes/whatsapp/session/`. - **Environment variables:** ```bash WHATSAPP_ENABLED=true -WHATSAPP_ALLOWED_USERS=15551234567 # Comma-separated phone numbers with country code +WHATSAPP_MODE=bot # "bot" (separate number) or "self-chat" (message yourself) +WHATSAPP_ALLOWED_USERS=15551234567 # Comma-separated phone numbers with country code ``` -Agent responses are prefixed with "⚕ **Hermes Agent**" so you can distinguish them from your own messages when messaging yourself. +**Getting a second number for bot mode:** + +| Option | Cost | Notes | +|--------|------|-------| +| WhatsApp Business app + dual-SIM | Free (if you have dual-SIM) | Install alongside personal WhatsApp, no second phone needed | +| Google Voice | Free (US only) | voice.google.com, verify WhatsApp via the Google Voice app | +| Prepaid SIM | $3-10/month | Any carrier; verify once, phone can go in a drawer on WiFi | + +Agent responses are prefixed with "⚕ **Hermes Agent**" for easy identification. > **Re-pairing:** If WhatsApp Web sessions disconnect (protocol updates, phone reset), re-pair with `hermes whatsapp`. diff --git a/gateway/platforms/whatsapp.py b/gateway/platforms/whatsapp.py index 17bb3ecb4..7ffa57437 100644 --- a/gateway/platforms/whatsapp.py +++ b/gateway/platforms/whatsapp.py @@ -160,12 +160,14 @@ class WhatsAppAdapter(BasePlatformAdapter): pass # Start the bridge process in its own process group + whatsapp_mode = os.getenv("WHATSAPP_MODE", "self-chat") self._bridge_process = subprocess.Popen( [ "node", str(bridge_path), "--port", str(self._bridge_port), "--session", str(self._session_path), + "--mode", whatsapp_mode, ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 107450935..57ab222bf 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -168,7 +168,7 @@ def cmd_gateway(args): def cmd_whatsapp(args): - """Set up WhatsApp: enable, configure allowed users, install bridge, pair via QR.""" + """Set up WhatsApp: choose mode, configure, install bridge, pair via QR.""" import os import subprocess from pathlib import Path @@ -177,12 +177,55 @@ def cmd_whatsapp(args): print() print("⚕ WhatsApp Setup") print("=" * 50) - print() - print("This will link your WhatsApp account to Hermes Agent.") - print("The agent will respond to messages sent to your WhatsApp number.") - print() - # Step 1: Enable WhatsApp + # ── Step 1: Choose mode ────────────────────────────────────────────── + current_mode = get_env_value("WHATSAPP_MODE") or "" + if not current_mode: + print() + print("How will you use WhatsApp with Hermes?") + print() + print(" 1. Separate bot number (recommended)") + print(" People message the bot's number directly — cleanest experience.") + print(" Requires a second phone number with WhatsApp installed on a device.") + print() + print(" 2. Personal number (self-chat)") + print(" You message yourself to talk to the agent.") + print(" Quick to set up, but the UX is less intuitive.") + print() + try: + choice = input(" Choose [1/2]: ").strip() + except (EOFError, KeyboardInterrupt): + print("\nSetup cancelled.") + return + + if choice == "1": + save_env_value("WHATSAPP_MODE", "bot") + wa_mode = "bot" + print(" ✓ Mode: separate bot number") + print() + print(" ┌─────────────────────────────────────────────────┐") + print(" │ Getting a second number for the bot: │") + print(" │ │") + print(" │ Easiest: Install WhatsApp Business (free app) │") + print(" │ on your phone with a second number: │") + print(" │ • Dual-SIM: use your 2nd SIM slot │") + print(" │ • Google Voice: free US number (voice.google) │") + print(" │ • Prepaid SIM: $3-10, verify once │") + print(" │ │") + print(" │ WhatsApp Business runs alongside your personal │") + print(" │ WhatsApp — no second phone needed. │") + print(" └─────────────────────────────────────────────────┘") + else: + save_env_value("WHATSAPP_MODE", "self-chat") + wa_mode = "self-chat" + print(" ✓ Mode: personal number (self-chat)") + else: + wa_mode = current_mode + mode_label = "separate bot number" if wa_mode == "bot" else "personal number (self-chat)" + print(f"\n✓ Mode: {mode_label}") + + # ── Step 2: Enable WhatsApp ────────────────────────────────────────── + print() current = get_env_value("WHATSAPP_ENABLED") if current and current.lower() == "true": print("✓ WhatsApp is already enabled") @@ -190,26 +233,36 @@ def cmd_whatsapp(args): save_env_value("WHATSAPP_ENABLED", "true") print("✓ WhatsApp enabled") - # Step 2: Allowed users + # ── Step 3: Allowed users ──────────────────────────────────────────── current_users = get_env_value("WHATSAPP_ALLOWED_USERS") or "" if current_users: print(f"✓ Allowed users: {current_users}") - response = input("\n Update allowed users? [y/N] ").strip() + try: + response = input("\n Update allowed users? [y/N] ").strip() + except (EOFError, KeyboardInterrupt): + response = "n" if response.lower() in ("y", "yes"): - phone = input(" Phone number(s) (e.g. 15551234567, comma-separated): ").strip() + if wa_mode == "bot": + phone = input(" Phone numbers that can message the bot (comma-separated): ").strip() + else: + phone = input(" Your phone number (e.g. 15551234567): ").strip() if phone: save_env_value("WHATSAPP_ALLOWED_USERS", phone.replace(" ", "")) print(f" ✓ Updated to: {phone}") else: print() - phone = input(" Your phone number (e.g. 15551234567): ").strip() + if wa_mode == "bot": + print(" Who should be allowed to message the bot?") + phone = input(" Phone numbers (comma-separated, or * for anyone): ").strip() + else: + phone = 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") - # Step 3: Install bridge deps + # ── Step 4: Install bridge dependencies ────────────────────────────── project_root = Path(__file__).resolve().parents[1] bridge_dir = project_root / "scripts" / "whatsapp-bridge" bridge_script = bridge_dir / "bridge.js" @@ -234,13 +287,16 @@ def cmd_whatsapp(args): else: print("✓ Bridge dependencies already installed") - # Step 4: Check for existing session + # ── Step 5: Check for existing session ─────────────────────────────── session_dir = Path.home() / ".hermes" / "whatsapp" / "session" session_dir.mkdir(parents=True, exist_ok=True) if (session_dir / "creds.json").exists(): print("✓ Existing WhatsApp session found") - response = input("\n Re-pair? This will clear the existing session. [y/N] ").strip() + try: + response = input("\n Re-pair? This will clear the existing session. [y/N] ").strip() + except (EOFError, KeyboardInterrupt): + response = "n" if response.lower() in ("y", "yes"): import shutil shutil.rmtree(session_dir, ignore_errors=True) @@ -251,11 +307,16 @@ def cmd_whatsapp(args): print(" Start the gateway with: hermes gateway") return - # Step 5: Run bridge in pair-only mode (no HTTP server, exits after QR scan) + # ── Step 6: QR code pairing ────────────────────────────────────────── print() print("─" * 50) - print("📱 Scan the QR code with your phone:") - print(" WhatsApp → Settings → Linked Devices → Link a Device") + if wa_mode == "bot": + print("📱 Open WhatsApp (or WhatsApp Business) on the") + print(" phone with the BOT's number, then scan:") + else: + print("📱 Open WhatsApp on your phone, then scan:") + print() + print(" Settings → Linked Devices → Link a Device") print("─" * 50) print() @@ -267,12 +328,28 @@ def cmd_whatsapp(args): except KeyboardInterrupt: pass + # ── Step 7: Post-pairing ───────────────────────────────────────────── print() if (session_dir / "creds.json").exists(): print("✓ WhatsApp paired successfully!") print() - print("Start the gateway with: hermes gateway") - print("Or install as a service: hermes gateway install") + if wa_mode == "bot": + print(" Next steps:") + print(" 1. Start the gateway: hermes gateway") + print(" 2. Send a message to the bot's WhatsApp number") + print(" 3. The agent will reply automatically") + print() + print(" Tip: Agent responses are prefixed with '⚕ Hermes Agent'") + else: + print(" Next steps:") + print(" 1. Start the gateway: hermes gateway") + print(" 2. Open WhatsApp → Message Yourself") + print(" 3. Type a message — the agent will reply") + print() + print(" Tip: Agent responses are prefixed with '⚕ Hermes Agent'") + print(" so you can tell them apart from your own messages.") + print() + print(" Or install as a service: hermes gateway install") else: print("⚠ Pairing may not have completed. Run 'hermes whatsapp' to try again.") diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index fa4dcebb4..b49285930 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -1382,21 +1382,13 @@ def run_setup_wizard(args): existing_whatsapp = get_env_value('WHATSAPP_ENABLED') if not existing_whatsapp and prompt_yes_no("Set up WhatsApp?", False): print_info("WhatsApp connects via a built-in bridge (Baileys).") - print_info("Requires Node.js (already installed if you have browser tools).") - print_info("On first gateway start, you'll scan a QR code with your phone.") + print_info("Requires Node.js. Run 'hermes whatsapp' for guided setup.") print() - if prompt_yes_no("Enable WhatsApp?", True): + if prompt_yes_no("Enable WhatsApp now?", True): save_env_value("WHATSAPP_ENABLED", "true") print_success("WhatsApp enabled") - - allowed_users = prompt(" Your phone number (e.g. 15551234567, comma-separated for multiple)") - if allowed_users: - save_env_value("WHATSAPP_ALLOWED_USERS", allowed_users.replace(" ", "")) - print_success("WhatsApp allowlist configured") - else: - print_info("⚠️ No allowlist set — anyone who messages your WhatsApp will get a response!") - - print_info("Start the gateway with 'hermes gateway' and scan the QR code.") + print_info("Run 'hermes whatsapp' to choose your mode (separate bot number") + print_info("or personal self-chat) and pair via QR code.") # Gateway reminder any_messaging = ( diff --git a/scripts/whatsapp-bridge/bridge.js b/scripts/whatsapp-bridge/bridge.js index 48e4d880b..951a62154 100644 --- a/scripts/whatsapp-bridge/bridge.js +++ b/scripts/whatsapp-bridge/bridge.js @@ -34,6 +34,7 @@ function getArg(name, defaultVal) { const PORT = parseInt(getArg('port', '3000'), 10); const SESSION_DIR = getArg('session', path.join(process.env.HOME || '~', '.hermes', 'whatsapp', 'session')); const PAIR_ONLY = args.includes('--pair-only'); +const WHATSAPP_MODE = getArg('mode', process.env.WHATSAPP_MODE || 'self-chat'); // "bot" or "self-chat" const ALLOWED_USERS = (process.env.WHATSAPP_ALLOWED_USERS || '').split(',').map(s => s.trim()).filter(Boolean); mkdirSync(SESSION_DIR, { recursive: true }); @@ -110,11 +111,16 @@ async function startSocket() { const isGroup = chatId.endsWith('@g.us'); const senderNumber = senderId.replace(/@.*/, ''); - // Skip own messages UNLESS it's a self-chat ("Message Yourself") + // Handle fromMe messages based on mode if (msg.key.fromMe) { - // Always skip in groups and status if (isGroup || chatId.includes('status')) continue; - // In DMs: only allow self-chat (remoteJid matches our own number) + + if (WHATSAPP_MODE === 'bot') { + // Bot mode: separate number. ALL fromMe are echo-backs of our own replies — skip. + continue; + } + + // Self-chat mode: only allow messages in the user's own self-chat const myNumber = (sock.user?.id || '').replace(/:.*@/, '@').replace(/@.*/, ''); const chatNumber = chatId.replace(/@.*/, ''); const isSelfChat = myNumber && chatNumber === myNumber; @@ -270,7 +276,7 @@ if (PAIR_ONLY) { startSocket(); } else { app.listen(PORT, () => { - console.log(`🌉 WhatsApp bridge listening on port ${PORT}`); + console.log(`🌉 WhatsApp bridge listening on port ${PORT} (mode: ${WHATSAPP_MODE})`); console.log(`📁 Session stored in: ${SESSION_DIR}`); if (ALLOWED_USERS.length > 0) { console.log(`🔒 Allowed users: ${ALLOWED_USERS.join(', ')}`);