7a5f5b8efedf855bac381ed9d00be70dc98fa356
📡 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 use a personal WhatsApp Web session (QR login) via --provider web for direct sends alongside the Twilio flow.
Quick Start
- Install:
pnpm install - Configure
.env(see.env.example): setTWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN(orTWILIO_API_KEY/TWILIO_API_SECRET), andTWILIO_WHATSAPP_FROM=whatsapp:+15551234567. Optional:TWILIO_SENDER_SIDif you don’t want auto-discovery. - Send a test:
pnpm warelay send --to +12345550000 --message "Hi from warelay" - Run auto-replies in polling mode (no public URL needed):
pnpm warelay relay --provider twilio --interval 5 --lookback 10 --verbose - 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.webhookruns the server locally;upalso 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
.envTwilio creds and a WhatsApp-enabled number (TWILIO_WHATSAPP_FROM). - Web (
--provider web) — personal WhatsApp Web session via QR. Supports outbound sends and inbound auto-replies when you runpnpm warelay relay --provider web. No delivery-status polling for web sends. Setup:pnpm warelay web:login(alias:pnpm warelay login) then either send with--provider webor keeprelay --provider webrunning. Session data lives in~/.warelay/credentials.json; if logged out, rerunweb: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 via personal WhatsApp Web: first
pnpm warelay web:login(alias:pnpm warelay login, scan QR), thenpnpm 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--jsonfor 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",
"-p",
"--output-format",
"json",
"--dangerously-skip-permissions",
"{{BodyStripped}}"
],
session: {
scope: "per-sender",
resetTriggers: ["/new"],
idleMinutes: 60,
sessionArgNew: ["--session-id", "{{SessionId}}"],
sessionArgResume: ["--resume", "{{SessionId}}"],
sessionArgBeforeBody: true
}
}
}
}
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 anyresetTriggersvalue) resets the session./new ask…resets and sendsask…as the prompt (viaBodyStripped).- 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, orup. - 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.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 usespnpmandtsx; no build required for local runs.
Description
Languages
TypeScript
82.5%
Swift
13.5%
Kotlin
1.9%
Shell
0.8%
CSS
0.5%
Other
0.8%