GitHub

@@ -1,133 +1,114 @@

1-

# 📡 warelay — WhatsApp Relay CLI (Twilio)

1+

# 📡 warelay — WhatsApp Relay CLI

2+3+

Small CLI to send, receive, auto-reply, and inspect WhatsApp messages over **Twilio** or your personal **WhatsApp Web** session. Ships with a one-command webhook setup (Tailscale Funnel + Twilio callback) and a configurable auto-reply engine (plain text or command/Claude driven).

4+5+

## Quick Start (5 steps)

6+

1) Prereqs: Node 22+, `pnpm`, a Twilio account with a WhatsApp-enabled number; Tailscale optional for webhooks.

7+

2) Install deps: `pnpm install`

8+

3) Copy `.env.example``.env`; set `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN` **or** `TWILIO_API_KEY`/`TWILIO_API_SECRET`, and `TWILIO_WHATSAPP_FROM=whatsapp:+15551234567` (plus optional `TWILIO_SENDER_SID`).

9+

4) Send a test: `pnpm warelay send --to +12345550000 --message "Hi from warelay"`

10+

5) Choose how to receive replies:

11+

- Polling (no ingress): `pnpm warelay relay --provider twilio --interval 5 --lookback 10`

12+

- Webhook (automatic): `pnpm warelay up --port 42873 --path /webhook/whatsapp --verbose`

13+

- Personal WhatsApp (no Twilio): `pnpm warelay web:login` then `pnpm warelay send --provider web ...`

14+15+

## Main Features

16+

- **Two providers:** Twilio (default) for reliable delivery + status; Web provider for quick personal sends/receives via QR login.

17+

- **Auto-replies:** Static templates or external commands (Claude-aware), with per-sender or global sessions and `/new` resets.

18+

- **Webhook in one go:** `warelay up` enables Tailscale Funnel, runs the webhook server, and updates the Twilio sender callback URL.

19+

- **Polling fallback:** `relay` polls Twilio when webhooks aren’t available; works headless.

20+

- **Status + delivery tracking:** `status` shows recent inbound/outbound; `send` can wait for final Twilio status.

21+22+

## Command Cheat Sheet

23+

| Command | What it does | Core flags |

24+

| --- | --- | --- |

25+

| `warelay send` | Send a WhatsApp message (Twilio or Web) | `--to <e164>` `--message <text>` `--wait <sec>` `--poll <sec>` `--provider twilio|web` `--json` `--dry-run` |

26+

| `warelay relay` | Auto-reply loop (poll Twilio or listen on Web) | `--provider auto|twilio|web` `--interval <sec>` `--lookback <min>` `--verbose` |

27+

| `warelay status` | Show recent sent/received messages | `--limit <n>` `--lookback <min>` `--json` |

28+

| `warelay webhook` | Run local inbound webhook server | `--port <port>` `--path <path>` `--reply <text>` `--verbose` `--yes` `--dry-run` |

29+

| `warelay up` | Turn on webhook + Tailscale Funnel + Twilio callback | `--port <port>` `--path <path>` `--verbose` `--yes` `--dry-run` |

30+

| `warelay web:login` (`login`) | Link personal WhatsApp Web via QR | `--verbose` |

31+32+

## Providers

33+

- **Twilio (default):** needs `.env` creds + WhatsApp-enabled number; supports delivery tracking, polling, webhooks, and auto-reply typing indicators.

34+

- **Web (`--provider web`):** uses your personal WhatsApp via Baileys; supports send/receive + auto-reply, but no delivery-status wait; cache lives in `~/.warelay/credentials/` (rerun `web:login` if logged out).

35+

- **Auto-select (`relay` only):** `--provider auto` uses Web when logged in, otherwise Twilio polling.

36+37+

## Configuration

38+39+

### Environment (.env)

40+

| Variable | Required | Description |

41+

| --- | --- | --- |

42+

| `TWILIO_ACCOUNT_SID` | Yes (Twilio provider) | Twilio Account SID |

43+

| `TWILIO_AUTH_TOKEN` | Yes* | Auth token (or use API key/secret) |

44+

| `TWILIO_API_KEY` | Yes* | API key if not using auth token |

45+

| `TWILIO_API_SECRET` | Yes* | API secret paired with `TWILIO_API_KEY` |

46+

| `TWILIO_WHATSAPP_FROM` | Yes (Twilio provider) | WhatsApp-enabled sender, e.g. `whatsapp:+15551234567` |

47+

| `TWILIO_SENDER_SID` | Optional | Overrides auto-discovery of the sender SID |

48+49+

(*Provide either auth token OR api key/secret.)

50+51+

### Auto-reply config (`~/.warelay/warelay.json`, JSON5)

52+

- Controls who is allowed to trigger replies (`allowFrom`), reply mode (`text` or `command`), templates, and session behavior.

53+

- Example (Claude command):

2543-

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).

4-5-

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.

6-7-

## What it can do

8-9-

- Send and track delivery for WhatsApp messages over Twilio.

10-

- Auto-reply via webhook or polling, with Claude-backed command replies or simple text templates.

11-

- Run entirely on your personal WhatsApp Web session (`--provider web`) for direct messaging without Twilio.

12-

- One-shot `up` command to launch webhook server, publish via Tailscale Funnel, and point Twilio callbacks automatically.

13-14-

## Quick Start

15-16-

1) Install: `pnpm install`

17-

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.

18-

3) Send a test (Twilio): `pnpm warelay send --to +12345550000 --message "Hi from warelay"`

19-

Dry run without sending: `pnpm warelay send --to +12345550000 --message "Hi" --dry-run`

20-

Send direct via personal WhatsApp: `pnpm warelay web:login` (scan QR once) then `pnpm warelay send --provider web --to +12345550000 --message "Hi from warelay"`

21-

4) Run auto-replies in polling mode (no public URL needed):

22-

`pnpm warelay relay --provider twilio --interval 5 --lookback 10 --verbose`

23-

5) Prefer webhooks? Launch everything in one step (webhook + Tailscale Funnel + Twilio callback):

24-

`pnpm warelay up --port 42873 --path /webhook/whatsapp --verbose`

25-26-

## Modes at a Glance

27-28-

- **Polling (`relay --provider twilio`)**: Periodically fetch inbound messages to your WhatsApp number. Easiest to start; no ingress needed. Auto-replies still run.

29-

- **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).

30-31-

## Providers (choose per command)

32-33-

- **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`).

34-

- **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).

35-36-

## Common Commands

37-38-

- Send: `pnpm warelay send --to +12345550000 --message "Hello" --wait 20 --poll 2`

39-

- Send (JSON output): `pnpm warelay send --to +12345550000 --message "Hello" --json`

40-

- 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"`

41-

- Auto-replies (auto provider): `pnpm warelay relay` (uses web if logged in, otherwise twilio poll)

42-

- Auto-replies (force web): `pnpm warelay relay --provider web`

43-

- Auto-replies (force Twilio poll): `pnpm warelay relay --provider twilio --interval 5 --lookback 10 --verbose`

44-

- Webhook only: `pnpm warelay webhook --port 42873 --path /webhook/whatsapp --verbose`

45-

- Webhook + Funnel + Twilio update: `pnpm warelay up --port 42873 --path /webhook/whatsapp --verbose`

46-

- Status (recent sent/received): `pnpm warelay status --limit 20 --lookback 240` (add `--json` for machine-readable)

47-48-

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

49-50-

### Claude-style example (your current setup)

5155

```json5

5256

{

5357

inbound: {

54-

allowFrom: ["***REMOVED***"], // optional allowlist (E.164, no whatsapp: prefix)

58+

allowFrom: ["+12345550000"],

5559

reply: {

5660

mode: "command",

57-

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",

58-

command: [

59-

"claude",

60-

"--dangerously-skip-permissions",

61-

"{{BodyStripped}}"

62-

],

63-

claudeOutputFormat: "text", // forces --output-format text and adds -p/--print when missing

64-

session: {

65-

scope: "per-sender",

66-

resetTriggers: ["/new"],

67-

idleMinutes: 60,

68-

sessionArgNew: ["--session-id", "{{SessionId}}"],

69-

sessionArgResume: ["--resume", "{{SessionId}}"],

70-

sessionArgBeforeBody: true

71-

}

61+

bodyPrefix: "You are a concise WhatsApp assistant.\n\n",

62+

command: ["claude", "--dangerously-skip-permissions", "{{BodyStripped}}"],

63+

claudeOutputFormat: "text",

64+

session: { scope: "per-sender", resetTriggers: ["/new"], idleMinutes: 60 }

7265

}

7366

}

7467

}

7568

```

766977-

### Claude CLI integration

78-79-

- When `command[0]` is `claude`, set `claudeOutputFormat` to `"text"`, `"json"`, or `"stream-json"` and warelay will inject `--output-format` and `-p/--print` automatically.

80-

- 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.

81-

- If you omit `claudeOutputFormat`, warelay leaves your args untouched (useful for custom Claude flags).

82-

- The config loader validates `warelay.json` (mode/text/command/claudeOutputFormat/session shape) and logs warnings for invalid combos instead of failing later at runtime.

83-84-

### Running without Twilio (personal WhatsApp Web)

85-86-

- 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`.

87-

- Send: `pnpm warelay send --provider web --to +12345550000 --message "Hi"`.

88-

- Auto-reply loop: `pnpm warelay relay --provider web --interval 5 --lookback 10`. Typing indicators are skipped in this mode, but text replies still work.

89-

- You can mix modes: use Twilio for reliable delivery/status, switch to web for quick personal sends. Each command decides the provider independently.

90-91-

### Simple text echo

92-

```json5

93-

{

94-

inbound: {

95-

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

96-

}

97-

}

98-

```

99-100-

Notes:

101-

- Templates support `{{Body}}`, `{{BodyStripped}}`, `{{From}}`, `{{To}}`, `{{MessageSid}}`, plus `{{SessionId}}`/`{{IsNewSession}}` when session reuse is enabled.

102-

- `/new` (or any `resetTriggers` value) resets the session. `/new ask…` resets and sends `ask…` as the prompt (via `BodyStripped`).

103-

- When an auto-reply starts (text or command), warelay sends a WhatsApp typing indicator tied to the inbound `MessageSid`.

104-105-

## Troubleshooting Delivery

106-107-

- 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`.

108-

- Check recent messages: `pnpm warelay status --limit 20 --lookback 240`.

109-

- If you must resend while a reply is long-running, keep messages <1600 chars (WhatsApp limit) and avoid restricted content/templates.

110-111-

## Options Reference

70+

### Claude CLI setup (how we run it)

71+

1) Install the official Claude CLI (e.g., `brew install anthropic-ai/cli/claude` or follow the Anthropic docs) and run `claude login` so it can read your API key.

72+

2) In `warelay.json`, set `reply.mode` to `"command"` and point `command[0]` to `"claude"`; set `claudeOutputFormat` to `"text"` (or `"json"/`"stream-json"` if you want warelay to parse and trim the JSON output).

73+

3) (Optional) Add `bodyPrefix` to inject a system prompt and `session` settings to keep multi-turn context (`/new` resets by default).

74+

4) Run `pnpm warelay relay --provider auto` (or `--provider web|twilio`) and send a WhatsApp message; warelay will queue the Claude call, stream typing indicators (Twilio provider), parse the result, and send back the text.

11275113-

| Field | Type / Values | Default | Description |

76+

### Auto-reply parameter table

77+

| Key | Type | Default | Notes |

11478

| --- | --- | --- | --- |

115-

| `inbound.allowFrom` | `string[]` | empty | Allowlist of E.164 numbers (no `whatsapp:`). If set, only these trigger auto-replies. |

116-

| `inbound.reply.mode` | `"text"` \| `"command"` || Auto-reply type. |

117-

| `inbound.reply.text` | `string` || Reply body for text mode; templated. |

118-

| `inbound.reply.command` | `string[]` || Argv to run for command mode; templated per element. Stdout (trimmed) is sent. |

119-

| `inbound.reply.template` | `string` || Optional string inserted as second argv element (prompt prefix). |

120-

| `inbound.reply.bodyPrefix` | `string` || Prepends to `Body` before templating (ideal for system instructions). |

121-

| `inbound.reply.session.scope` | `"per-sender" \| "global"` | `per-sender` | Session key: one per sender or single global chat. |

122-

| `inbound.reply.session.resetTriggers` | `string[]` | `["/new"]` | Any entry acts as both exact reset token and prefix (`/new hi`). |

123-

| `inbound.reply.session.idleMinutes` | `number` | `60` | Expire and recreate session after this idle time. |

124-

| `inbound.reply.session.sessionArgNew` | `string[]` | `["--session-id","{{SessionId}}"]` | Args inserted for a new session run. |

125-

| `inbound.reply.session.sessionArgResume` | `string[]` | `["--resume","{{SessionId}}"]` | Args inserted when resuming an existing session. |

126-

| `inbound.reply.session.sessionArgBeforeBody` | `boolean` | `true` | Place session args before the final body argument. |

127-

| `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. |

128-

| `inbound.reply.timeoutSeconds` | `number` | 600 | Command timeout. |

129-130-

## Dev Notes

131-132-

- During dev you can run without building: `pnpm dev -- <subcommand>` (e.g., `pnpm dev -- send --to +1...`).

133-

- Stop relay/webhook with `Ctrl+C`. CLI uses `pnpm` and `tsx`; no build required for local runs.

79+

| `inbound.allowFrom` | `string[]` | empty | E.164 numbers allowed to trigger auto-reply (no `whatsapp:`). |

80+

| `inbound.reply.mode` | `"text" | "command"` || Reply style. |

81+

| `inbound.reply.text` | `string` || Used when `mode=text`; templating supported. |

82+

| `inbound.reply.command` | `string[]` || argv for `mode=command`; each element templated. Stdout (trimmed) is sent. |

83+

| `inbound.reply.template` | `string` || Injected as argv[1] (prompt prefix) before the body. |

84+

| `inbound.reply.bodyPrefix` | `string` || Prepended to `Body` before templating (great for system prompts). |

85+

| `inbound.reply.timeoutSeconds` | `number` | `600` | Command timeout. |

86+

| `inbound.reply.claudeOutputFormat` | `"text"|"json"|"stream-json"` || When command starts with `claude`, auto-adds `--output-format` + `-p/--print` and trims reply text. |

87+

| `inbound.reply.session.scope` | `"per-sender"|"global"` | `per-sender` | Session bucket for conversation memory. |

88+

| `inbound.reply.session.resetTriggers` | `string[]` | `["/new"]` | Exact match or prefix (`/new hi`) resets session. |

89+

| `inbound.reply.session.idleMinutes` | `number` | `60` | Session expires after idle period. |

90+

| `inbound.reply.session.store` | `string` | `~/.warelay/sessions.json` | Custom session store path. |

91+

| `inbound.reply.session.sessionArgNew` | `string[]` | `["--session-id","{{SessionId}}"]` | Args injected for a new session run. |

92+

| `inbound.reply.session.sessionArgResume` | `string[]` | `["--resume","{{SessionId}}"]` | Args for resumed sessions. |

93+

| `inbound.reply.session.sessionArgBeforeBody` | `boolean` | `true` | Place session args before final body arg. |

94+95+

Templating tokens: `{{Body}}`, `{{BodyStripped}}`, `{{From}}`, `{{To}}`, `{{MessageSid}}`, plus `{{SessionId}}` and `{{IsNewSession}}` when sessions are enabled.

96+97+

## Webhook & Tailscale Flow

98+

- `warelay webhook` starts the local Express server on your chosen port/path; add `--reply "Got it"` for a static reply when no config file is present.

99+

- `warelay up` adds Funnel: checks `tailscale`, enables `tailscale funnel <port>`, prints the public URL (`https://<tailnet-host><path>`), starts the webhook, discovers the WhatsApp sender SID, and updates Twilio callbacks to the Funnel URL.

100+

- If Funnel is not allowed on your tailnet, the CLI exits with guidance; you can still use `relay --provider twilio` to poll without webhooks.

101+102+

## Troubleshooting Tips

103+

- Send/receive issues: run `pnpm warelay status --limit 20 --lookback 240 --json` to inspect recent traffic.

104+

- Auto-reply not firing: ensure sender is in `allowFrom` (or unset), and confirm `.env` + `warelay.json` are loaded (reload shell after edits).

105+

- Web provider dropped: rerun `pnpm warelay web:login`; credentials live in `~/.warelay/credentials/`.

106+

- Tailscale Funnel errors: update tailscale/tailscaled; check admin console that Funnel is enabled for this device.

107+108+

## FAQ & Safety (quick answers)

109+

- Twilio errors: **63016 “permission to send an SMS has not been enabled”** → ensure your number is WhatsApp-enabled; **63007 template not approved** → send a free-form session message within 24h or use an approved template; **63112 policy violation** → adjust content, shorten to <1600 chars, avoid links that trigger spam filters. Re-run `pnpm warelay status` to see the exact Twilio response body.

110+

- Does this store my messages? Warelay only writes `~/.warelay/warelay.json` (config), `~/.warelay/credentials/` (WhatsApp Web auth), and `~/.warelay/sessions.json` (session IDs + timestamps). It does **not** persist message bodies beyond the session store. Logs print to stdout/stderr; redirect or rotate if needed.

111+

- Personal WhatsApp safety: Automation on personal accounts can be rate-limited or logged out by WhatsApp. Use `--provider web` sparingly, keep messages human-like, and re-run `web:login` if the session is dropped.

112+

- Limits to remember: WhatsApp text limit ~1600 chars; avoid rapid bursts—space sends by a few seconds; keep webhook replies under a couple seconds for good UX; command auto-replies time out after 600s by default.

113+

- Deploy / keep running: Use `tmux` or `screen` for ad-hoc (`tmux new -s warelay -- pnpm warelay relay --provider twilio`). For long-running hosts, wrap `pnpm warelay relay ...` or `pnpm warelay up ...` in a systemd service or macOS LaunchAgent; ensure environment variables are loaded in that context.

114+

- Rotating credentials: Update `.env` (Twilio keys), rerun your process; for Web provider, delete `~/.warelay/credentials/` and rerun `pnpm warelay web:login` to relink.

Read the original on github.com ↗