OpenClaw Fork with Event-Sourced Memory and Multi-Account Matrix
Find a file
2025-11-25 04:15:20 +01:00
.github/workflows Add command modules and tests; commit remaining changes 2025-11-25 00:12:12 +01:00
docs/refactor feat: retries for webhook bring-up and send --json docs 2025-11-25 04:10:20 +01:00
src chore: align web provider logging and relay e2e 2025-11-25 04:15:20 +01:00
test/mocks Switch to Baileys multi-file auth with latest rc 2025-11-25 00:34:55 +01:00
.env.example Add warelay CLI with Twilio webhook support 2025-11-24 11:23:15 +01:00
.gitignore Ignore coverage output and document Claude text mode 2025-11-25 01:38:43 +01:00
biome.json Add Biome linting and improve webhook error handling 2025-11-24 14:06:52 +01:00
LICENSE Initial commit 2025-11-24 11:16:47 +01:00
package.json Add lint:fix to run biome check+format 2025-11-25 00:50:36 +01:00
README.md feat: retries for webhook bring-up and send --json docs 2025-11-25 04:10:20 +01:00
tsconfig.json Update deps to latest and align tests 2025-11-24 17:33:59 +01:00

📡 warelay — WhatsApp Relay CLI (Twilio)

Small TypeScript CLI to send, receive, auto-reply, and inspect WhatsApp messages via Twilio. Works in polling mode or webhook mode (with Tailscale Funnel helper).

You can also talk to WhatsApp directly with a personal WhatsApp Web session (QR login) via --provider web—no Twilio needed for send/receive in that mode.

What it can do

  • Send and track delivery for WhatsApp messages over Twilio.
  • Auto-reply via webhook or polling, with Claude-backed command replies or simple text templates.
  • Run entirely on your personal WhatsApp Web session (--provider web) for direct messaging without Twilio.
  • One-shot up command to launch webhook server, publish via Tailscale Funnel, and point Twilio callbacks automatically.

Quick Start

  1. Install: pnpm install
  2. Configure .env (see .env.example): set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN (or TWILIO_API_KEY/TWILIO_API_SECRET), and TWILIO_WHATSAPP_FROM=whatsapp:+15551234567. Optional: TWILIO_SENDER_SID if you don’t want auto-discovery.
  3. Send a test (Twilio): pnpm warelay send --to +12345550000 --message "Hi from warelay"
    Dry run without sending: pnpm warelay send --to +12345550000 --message "Hi" --dry-run
    Send direct via personal WhatsApp: pnpm warelay web:login (scan QR once) then pnpm warelay send --provider web --to +12345550000 --message "Hi from warelay"
  4. Run auto-replies in polling mode (no public URL needed):
    pnpm warelay relay --provider twilio --interval 5 --lookback 10 --verbose
  5. Prefer webhooks? Launch everything in one step (webhook + Tailscale Funnel + Twilio callback):
    pnpm warelay up --port 42873 --path /webhook/whatsapp --verbose

Modes at a Glance

  • Polling (relay --provider twilio): Periodically fetch inbound messages to your WhatsApp number. Easiest to start; no ingress needed. Auto-replies still run.
  • Webhook (webhook / up): Push delivery from Twilio. webhook runs the server locally; up also enables Tailscale Funnel and points the Twilio sender/webhook to your public Funnel URL (with fallbacks to phone number and messaging service).

Providers (choose per command)

  • Twilio (default) — full feature set: send, wait/poll delivery, status, inbound polling/webhook, auto-replies. Requires .env Twilio creds and a WhatsApp-enabled number (TWILIO_WHATSAPP_FROM).
  • Web (--provider web) — direct messaging through your personal WhatsApp account (no Twilio). Supports outbound sends and inbound auto-replies when you run pnpm warelay relay --provider web. No delivery-status polling for web sends (WhatsApp Web doesn’t expose it). Setup: pnpm warelay web:login (alias: pnpm warelay login) then either send with --provider web or keep relay --provider web running. Session data lives in ~/.warelay/credentials.json; if logged out, rerun web:login/login. Use at your own risk (personal-account automation can be rate-limited or logged out by WhatsApp).

Common Commands

  • Send: pnpm warelay send --to +12345550000 --message "Hello" --wait 20 --poll 2
  • Send (JSON output): pnpm warelay send --to +12345550000 --message "Hello" --json
  • Send via personal WhatsApp Web: first pnpm warelay web:login (alias: pnpm warelay login, scan QR), then pnpm warelay send --provider web --to +12345550000 --message "Hi"
  • Auto-replies (auto provider): pnpm warelay relay (uses web if logged in, otherwise twilio poll)
  • Auto-replies (force web): pnpm warelay relay --provider web
  • Auto-replies (force Twilio poll): pnpm warelay relay --provider twilio --interval 5 --lookback 10 --verbose
  • Webhook only: pnpm warelay webhook --port 42873 --path /webhook/whatsapp --verbose
  • Webhook + Funnel + Twilio update: pnpm warelay up --port 42873 --path /webhook/whatsapp --verbose
  • Status (recent sent/received): pnpm warelay status --limit 20 --lookback 240 (add --json for machine-readable)

Auto-Reply Config (JSON5 at ~/.warelay/warelay.json)

Claude-style example (your current setup)

{
  inbound: {
    allowFrom: ["***REMOVED***"], // optional allowlist (E.164, no whatsapp: prefix)
    reply: {
      mode: "command",
      bodyPrefix: "You are a helpful assistant running on the user's Mac. User writes messages via WhatsApp and you respond. You want to be concise in your responses, at most 1000 characters.\n\n",
      command: [
        "claude",
        "--dangerously-skip-permissions",
        "{{BodyStripped}}"
      ],
      claudeOutputFormat: "text", // forces --output-format text and adds -p/--print when missing
      session: {
        scope: "per-sender",
        resetTriggers: ["/new"],
        idleMinutes: 60,
        sessionArgNew: ["--session-id", "{{SessionId}}"],
        sessionArgResume: ["--resume", "{{SessionId}}"],
        sessionArgBeforeBody: true
      }
    }
  }
}

Claude CLI integration

  • When command[0] is claude, set claudeOutputFormat to "text", "json", or "stream-json" and warelay will inject --output-format and -p/--print automatically.
  • For "json"/"stream-json", warelay parses Claude's JSON payload and sends just the text content to WhatsApp while keeping the full JSON in logs for debugging.
  • If you omit claudeOutputFormat, warelay leaves your args untouched (useful for custom Claude flags).
  • The config loader validates warelay.json (mode/text/command/claudeOutputFormat/session shape) and logs warnings for invalid combos instead of failing later at runtime.

Running without Twilio (personal WhatsApp Web)

  • Log in once: pnpm warelay web:login (or pnpm warelay login), scan the QR in your terminal/browser. Credentials are stored locally at ~/.warelay/credentials.json.
  • Send: pnpm warelay send --provider web --to +12345550000 --message "Hi".
  • Auto-reply loop: pnpm warelay relay --provider web --interval 5 --lookback 10. Typing indicators are skipped in this mode, but text replies still work.
  • You can mix modes: use Twilio for reliable delivery/status, switch to web for quick personal sends. Each command decides the provider independently.

Simple text echo

{
  inbound: {
    reply: { mode: "text", text: "Echo: {{Body}}" }
  }
}

Notes:

  • Templates support {{Body}}, {{BodyStripped}}, {{From}}, {{To}}, {{MessageSid}}, plus {{SessionId}}/{{IsNewSession}} when session reuse is enabled.
  • /new (or any resetTriggers value) resets the session. /new ask… resets and sends ask… as the prompt (via BodyStripped).
  • When an auto-reply starts (text or command), warelay sends a WhatsApp typing indicator tied to the inbound MessageSid.

Troubleshooting Delivery

  • Auto-reply send failures now print in red with Twilio code/status and the response body (e.g., policy violation 63112). Watch terminal output when running relay, webhook, or up.
  • Check recent messages: pnpm warelay status --limit 20 --lookback 240.
  • If you must resend while a reply is long-running, keep messages <1600 chars (WhatsApp limit) and avoid restricted content/templates.

Options Reference

Field Type / Values Default Description
inbound.allowFrom string[] empty Allowlist of E.164 numbers (no whatsapp:). If set, only these trigger auto-replies.
inbound.reply.mode "text" | "command" — Auto-reply type.
inbound.reply.text string — Reply body for text mode; templated.
inbound.reply.command string[] — Argv to run for command mode; templated per element. Stdout (trimmed) is sent.
inbound.reply.template string — Optional string inserted as second argv element (prompt prefix).
inbound.reply.bodyPrefix string — Prepends to Body before templating (ideal for system instructions).
inbound.reply.session.scope "per-sender" | "global" per-sender Session key: one per sender or single global chat.
inbound.reply.session.resetTriggers string[] ["/new"] Any entry acts as both exact reset token and prefix (/new hi).
inbound.reply.session.idleMinutes number 60 Expire and recreate session after this idle time.
inbound.reply.session.sessionArgNew string[] ["--session-id","{{SessionId}}"] Args inserted for a new session run.
inbound.reply.session.sessionArgResume string[] ["--resume","{{SessionId}}"] Args inserted when resuming an existing session.
inbound.reply.session.sessionArgBeforeBody boolean true Place session args before the final body argument.
inbound.reply.claudeOutputFormat "text" | "json" | "stream-json" — When command[0] is claude, force this output format and auto-add -p/--print so Claude exits after emitting output.
inbound.reply.timeoutSeconds number 600 Command timeout.

Dev Notes

  • During dev you can run without building: pnpm dev -- <subcommand> (e.g., pnpm dev -- send --to +1...).
  • Stop relay/webhook with Ctrl+C. CLI uses pnpm and tsx; no build required for local runs.