From 42f64279d564357120e05c0ff249574ecef4fb18 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 24 Nov 2025 16:00:56 +0100 Subject: [PATCH] Add claude auto-reply allowlist and verbose hooks --- README.md | 3 ++- src/index.ts | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9797e9935..48bedaf23 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Small TypeScript CLI to send, monitor, and webhook WhatsApp messages via Twilio. - Requires Tailscale Funnel to be enabled for your tailnet/device (admin setting). If it isn’t enabled, the command will exit with instructions; alternatively expose the webhook via your own tunnel and set the Twilio URL manually. - Polling mode (no webhooks/funnel): `pnpm warelay poll --interval 5 --lookback 10 --verbose` - Useful fallback if Twilio webhook can’t reach you. + - Still runs config-driven auto-replies (including command-mode/Claude) for new inbound messages. ## Config-driven auto-replies @@ -51,7 +52,7 @@ Put a JSON5 config at `~/.warelay/warelay.json`. Examples: } ``` -During dev you can run without building: `pnpm dev -- ` (e.g. `pnpm dev -- send --to +1...`). +During dev you can run without building: `pnpm dev -- ` (e.g. `pnpm dev -- send --to +1...`). Auto-replies apply in webhook and polling modes. ## Notes diff --git a/src/index.ts b/src/index.ts index 39ed92159..334936929 100644 --- a/src/index.ts +++ b/src/index.ts @@ -314,6 +314,7 @@ type ReplyMode = "text" | "command"; type WarelayConfig = { inbound?: { + allowFrom?: string[]; // E.164 numbers allowed to trigger auto-reply (without whatsapp:) reply?: { mode: ReplyMode; text?: string; // for mode=text, can contain {{Body}} @@ -358,6 +359,18 @@ async function getReplyFromConfig( // Choose reply from config: static text or external command stdout. const cfg = loadConfig(); const reply = cfg.inbound?.reply; + + // Optional allowlist by origin number (E.164 without whatsapp: prefix) + const allowFrom = cfg.inbound?.allowFrom; + if (Array.isArray(allowFrom) && allowFrom.length > 0) { + const from = (ctx.From ?? "").replace(/^whatsapp:/, ""); + if (!allowFrom.includes(from)) { + logVerbose( + `Skipping auto-reply: sender ${from || ""} not in allowFrom list`, + ); + return undefined; + } + } if (!reply) { logVerbose("No inbound.reply configured; skipping auto-reply"); return undefined; @@ -379,7 +392,7 @@ async function getReplyFromConfig( : argv; try { if (globalVerbose) console.log(`RUN `); - const { stdout } = await execFileAsync(finalArgv[0], finalArgv.slice(1), { + const { stdout } = await execFileAsync(finalArgv[0], finalArgv.slice(1), { maxBuffer: 1024 * 1024, }); const trimmed = stdout.trim(); @@ -952,7 +965,8 @@ async function monitor(intervalSeconds: number, lookbackMinutes: number) { }; let keepRunning = true; - process.on("SIGINT", () => { + process.once("SIGINT", () => { + if (!keepRunning) return; keepRunning = false; console.log("\n👋 Stopping monitor"); });