13 KiB
Repository Guidelines
What is Warelay?
Warelay is a WhatsApp relay CLI tool for sending, receiving, and auto-replying to WhatsApp messages. It supports two provider backends:
- Twilio: Enterprise-grade WhatsApp Business API with delivery tracking, webhooks, and polling
- Web (Baileys): Personal WhatsApp Web session via QR code linking (unofficial @whiskeysockets/baileys library)
Primary use case: Running AI assistants (like "Clawd" - Claude-powered) that respond to WhatsApp messages with configurable sessions, transcription, and heartbeats.
Project Structure & Module Organization
Core Architecture
src/
├── index.ts # Main entry point + public API exports
├── provider-web.ts # Barrel exports for Web provider (from src/web/)
├── cli/
│ ├── program.ts # Commander.js CLI definition (all commands)
│ ├── deps.ts # Dependency injection via createDefaultDeps()
│ ├── prompt.ts # Interactive prompts (promptYesNo)
│ └── relay_tmux.ts # tmux session management
├── commands/
│ ├── send.ts # warelay send
│ ├── status.ts # warelay status (Twilio only)
│ ├── webhook.ts # warelay webhook
│ └── up.ts # (startup helpers)
├── auto-reply/
│ ├── reply.ts # getReplyFromConfig() - main reply engine
│ ├── command-reply.ts # runCommandReply() - external command execution
│ ├── claude.ts # Claude CLI JSON output parsing
│ ├── templating.ts # {{Placeholder}} template substitution
│ └── transcription.ts # Audio-to-text transcription
├── config/
│ ├── config.ts # ~/.warelay/warelay.json schema + loadConfig()
│ └── sessions.ts # ~/.warelay/sessions.json (Claude session state)
├── providers/
│ ├── provider.types.ts # Provider = "twilio" | "web"
│ ├── twilio/index.ts # Twilio re-exports
│ └── web/index.ts # Web re-exports
├── twilio/
│ ├── client.ts # createClient() - Twilio SDK init
│ ├── send.ts # sendMessage(), waitForFinalStatus()
│ ├── messages.ts # listRecentMessages(), formatMessageLine()
│ ├── monitor.ts # monitorTwilio() - polling loop
│ ├── webhook.ts # Express server for Twilio callbacks
│ ├── heartbeat.ts # runTwilioHeartbeatOnce()
│ ├── typing.ts # sendTypingIndicator()
│ └── update-webhook.ts # Twilio callback URL management
├── web/
│ ├── session.ts # createWaSocket(), pickProvider(), webAuthExists()
│ ├── login.ts # loginWeb() - QR code flow
│ ├── inbound.ts # monitorWebInbox() - message listener
│ ├── outbound.ts # sendMessageWeb()
│ ├── auto-reply.ts # monitorWebProvider() - main relay loop
│ ├── media.ts # Download/resize helpers
│ └── reconnect.ts # Exponential backoff math
├── media/
│ ├── store.ts # saveMediaSource(), saveMediaBuffer(), cleanOldMedia()
│ ├── host.ts # ensureMediaHosted() - Tailscale Funnel hosting
│ ├── server.ts # Express routes for serving media
│ └── constants.ts # MAX_IMAGE_BYTES (6MB), MAX_AUDIO_BYTES (16MB), etc.
├── infra/
│ ├── tailscale.ts # ensureFunnel(), getTailnetHostname()
│ ├── ports.ts # ensurePortAvailable(), PortInUseError
│ └── binaries.ts # ensureBinary() - external tool checks
├── process/
│ ├── exec.ts # runCommandWithTimeout(), runExec()
│ └── command-queue.ts # Command queueing
├── webhook/
│ ├── server.ts # Webhook server setup
│ └── update.ts # Webhook URL updates
├── env.ts # Zod schema for TWILIO_* env vars
├── globals.ts # setVerbose(), isVerbose(), info(), danger()
├── runtime.ts # RuntimeEnv type for testable I/O
├── logger.ts # Pino logger setup
├── logging.ts # getResolvedLoggerSettings()
├── utils.ts # assertProvider(), normalizeE164(), toWhatsappJid()
└── version.ts # VERSION constant
Key Files by Function
| Function | Primary Files |
|---|---|
| CLI entry | src/index.ts, src/cli/program.ts |
| Twilio send | src/twilio/send.ts, src/commands/send.ts |
| Web send | src/web/outbound.ts |
| Auto-reply | src/auto-reply/reply.ts, src/auto-reply/command-reply.ts |
| Claude integration | src/auto-reply/claude.ts |
| Config | src/config/config.ts (schema), src/config/sessions.ts (state) |
| Web relay loop | src/web/auto-reply.ts (monitorWebProvider) |
| Media pipeline | src/media/store.ts, src/media/host.ts |
CLI Commands Reference
| Command | Description | Key Options |
|---|---|---|
login |
Link WhatsApp via QR | --verbose |
logout |
Clear web credentials | |
send |
Send message | --to, --message, --media, --provider, --wait, --poll, --json |
relay |
Auto-reply loop | --provider auto|web|twilio, --interval, --lookback, --heartbeat-now |
relay:heartbeat |
Relay + immediate heartbeat | --provider auto|web, --verbose |
relay:tmux |
Relay in tmux session | |
relay:heartbeat:tmux |
Relay + heartbeat in tmux | |
status |
Show recent messages | --limit, --lookback, --json |
webhook |
Run inbound webhook | --ingress tailscale|none, --port, --path |
heartbeat |
Trigger one heartbeat | --to, --session-id, --all, --message |
Build, Test, and Development Commands
- Install deps:
pnpm install - Run CLI in dev:
pnpm warelay ...(tsx entry) orpnpm devforsrc/index.ts - Type-check/build:
bun run build(tsc) - Relink globally after code changes:
bun run build && bun link - Lint/format:
pnpm lint(biome check),pnpm format(biome format) - Fix lint/format:
pnpm lint:fix,pnpm format:fix - Tests:
pnpm test(vitest); coverage:pnpm test:coverage - Node requirement: >=22.0.0
Important
: Always use
bunfor building and linking. After modifying source code, runbun run build && bun linkto rebuild and update the globalwarelaycommand.
Key Dependencies
| Package | Purpose |
|---|---|
@whiskeysockets/baileys |
WhatsApp Web protocol (unofficial) |
twilio |
Twilio SDK for WhatsApp Business API |
commander |
CLI argument parsing |
express |
Webhook server |
zod |
Schema validation (config, env) |
json5 |
Config file parsing (allows comments) |
pino |
Structured logging |
sharp |
Image resizing for media limits |
qrcode-terminal |
QR code display for login |
chalk |
CLI colorization |
Coding Style & Naming Conventions
- Language: TypeScript (ESM). Prefer strict typing; avoid
any. - Formatting/linting via Biome; run
pnpm lintbefore commits. - Keep files concise; extract helpers instead of "V2" copies.
- Use existing patterns for CLI options and dependency injection via
createDefaultDeps().
Architectural Patterns
Dependency Injection
Commands use CliDeps from src/cli/deps.ts for testability:
export type CliDeps = {
sendMessage: typeof sendMessage;
sendMessageWeb: typeof sendMessageWeb;
waitForFinalStatus: typeof waitForFinalStatus;
monitorTwilio: typeof monitorTwilio;
// ...
};
export function createDefaultDeps(): CliDeps { /* ... */ }
Runtime Abstraction
RuntimeEnv from src/runtime.ts abstracts I/O for testing:
export type RuntimeEnv = {
log: (msg: string) => void;
error: (msg: string) => void;
exit: (code: number) => never;
};
Provider Selection
pickProvider() in src/web/session.ts:
"auto"→ web if~/.warelay/credentials/exists, else twilio- Web sessions don't fall back to Twilio on disconnect (exits instead)
Templating
src/auto-reply/templating.ts supports {{Placeholder}} tokens:
{{Body}},{{BodyStripped}}(with reset trigger removed){{From}},{{To}},{{MessageSid}}{{SessionId}},{{IsNewSession}}{{MediaPath}},{{MediaUrl}},{{MediaType}}
Session Management
Sessions in ~/.warelay/sessions.json:
type SessionEntry = {
sessionId: string; // UUID for Claude --session-id
updatedAt: number; // Timestamp for idle expiration
systemSent?: boolean; // For sendSystemOnce feature
};
- Per-sender or global scope
- Idle expiration (default 60 minutes)
- Reset triggers (
/newby default)
Testing Guidelines
- Framework: Vitest with V8 coverage thresholds (70% lines/branches/functions/statements).
- Naming: match source names with
*.test.ts; e2e in*.e2e.test.ts. - Run
pnpm test(orpnpm test:coverage) before pushing when you touch logic. - Pure test additions/fixes generally do not need a changelog entry unless they alter user-facing behavior or the user asks for one.
Test Patterns
import { describe, expect, it, vi } from "vitest";
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(() => { throw new Error("exit"); }),
};
const baseDeps = {
assertProvider: vi.fn(),
sendMessageWeb: vi.fn(),
// ... mock dependencies
} as unknown as CliDeps;
Configuration Files
Environment (.env)
Required for Twilio provider:
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=... # OR
TWILIO_API_KEY=SK...
TWILIO_API_SECRET=...
TWILIO_WHATSAPP_FROM=whatsapp:+19995550123
TWILIO_SENDER_SID=... # optional
Config (~/.warelay/warelay.json)
JSON5 format with Zod validation:
{
logging: { level: "debug", file: "/tmp/warelay/warelay.log" },
inbound: {
allowFrom: ["+12345550000"], // E.164, or "*" for all
messagePrefix: "",
responsePrefix: "",
timestampPrefix: true, // or IANA timezone string
transcribeAudio: { command: ["openai", "..."], timeoutSeconds: 45 },
reply: {
mode: "command", // or "text"
command: ["claude", "--dangerously-skip-permissions", "{{BodyStripped}}"],
bodyPrefix: "You are a concise assistant.\n\n",
claudeOutputFormat: "text", // or "json" / "stream-json"
timeoutSeconds: 600,
session: {
scope: "per-sender", // or "global"
resetTriggers: ["/new"],
idleMinutes: 60,
sendSystemOnce: true,
sessionIntro: "New conversation started."
},
heartbeatMinutes: 10
}
},
web: {
heartbeatSeconds: 120,
reconnect: { initialMs: 1000, maxMs: 30000, factor: 2, jitter: 0.2, maxAttempts: 10 }
}
}
Commit & Pull Request Guidelines
- Follow concise, action-oriented commit messages (e.g.,
CLI: add verbose flag to send). - Group related changes; avoid bundling unrelated refactors.
- PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags.
Security & Configuration Tips
- Environment: copy
.env.example; set Twilio creds and WhatsApp sender (TWILIO_WHATSAPP_FROM). - Web provider stores creds at
~/.warelay/credentials/; rerunwarelay loginif logged out. - Media hosting relies on Tailscale Funnel when using Twilio; use
warelay webhook --ingress tailscaleor--serve-mediafor local hosting. - Media limits: images ≤6MB, audio/video ≤16MB, documents ≤100MB.
Agent-Specific Notes
- If the relay is running in tmux (
warelay-relay), restart it after code changes: kill pane/session and runwarelay relay --provider twilio --verboseinside tmux. Check tmux before editing; keep the watcher healthy if you start it. - Always use
--provider twiliowhen starting the relay (notautoorweb). - warelay is installed globally, so use
warelaydirectly instead ofpnpm warelay. - Also read the shared guardrails at
~/Projects/oracle/AGENTS.mdand~/Projects/agent-scripts/AGENTS.MDbefore making changes; align with any cross-repo rules noted there.
Common Development Tasks
- Adding a new CLI command: Add to
src/cli/program.ts, implement insrc/commands/, add deps tosrc/cli/deps.ts - Modifying auto-reply logic:
src/auto-reply/reply.tsis the entry point;command-reply.tshandles external commands - Changing config schema: Update
src/config/config.ts(Zod schema + WarelayConfig type) - Web provider changes: Files under
src/web/; barrel exports viasrc/provider-web.ts - Twilio provider changes: Files under
src/twilio/; barrel exports viasrc/providers/twilio/index.ts
Debugging Tips
- Enable verbose logging:
--verboseflag orlogging.level: "debug"in config - Check logs at
/tmp/warelay/warelay.log(or configured path) - Web relay health logs show heartbeat intervals and reconnect policy
- Use
warelay status --jsonto inspect recent Twilio traffic - Session state in
~/.warelay/sessions.jsonfor Claude session debugging
Exclamation Mark Escaping Workaround
The Claude Code Bash tool escapes ! to \! in command arguments. When using warelay send with messages containing exclamation marks, use heredoc syntax:
# WRONG - will send "Hello\!" with backslash
warelay send --provider web --to "+1234" --message 'Hello!'
# CORRECT - use heredoc to avoid escaping
warelay send --provider web --to "+1234" --message "$(cat <<'EOF'
Hello!
EOF
)"
This is a Claude Code quirk, not a warelay bug.