#!/usr/bin/env node import crypto from "node:crypto"; import fs from "node:fs"; import net from "node:net"; import os from "node:os"; import path from "node:path"; import process, { stdin as input, stdout as output } from "node:process"; import readline from "node:readline/promises"; import { fileURLToPath } from "node:url"; import chalk from "chalk"; import dotenv from "dotenv"; import JSON5 from "json5"; import Twilio from "twilio"; import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message.js"; import type { TwilioSenderListClient, TwilioRequester } from "./twilio/types.js"; import { runCommandWithTimeout, runExec, type SpawnResult, } from "./process/exec.js"; import { defaultRuntime, type RuntimeEnv } from "./runtime.js"; import { sendTypingIndicator } from "./twilio/typing.js"; import { autoReplyIfConfigured, getReplyFromConfig, } from "./auto-reply/reply.js"; import { readEnv, ensureTwilioEnv, type EnvConfig } from "./env.js"; import { createClient } from "./twilio/client.js"; import { logTwilioSendError, formatTwilioError } from "./twilio/utils.js"; import { monitorTwilio as monitorTwilioImpl } from "./twilio/monitor.js"; import { sendMessage, waitForFinalStatus } from "./twilio/send.js"; import { startWebhook as startWebhookImpl } from "./twilio/webhook.js"; import { updateWebhook as updateWebhookImpl, findIncomingNumberSid as findIncomingNumberSidImpl, findMessagingServiceSid as findMessagingServiceSidImpl, setMessagingServiceWebhook as setMessagingServiceWebhookImpl, } from "./twilio/update-webhook.js"; import { findIncomingNumberSid as findIncomingNumberSid, findMessagingServiceSid as findMessagingServiceSid, } from "./twilio/update-webhook.js"; import { CLAUDE_BIN, parseClaudeJsonText } from "./auto-reply/claude.js"; import { applyTemplate, type MsgContext, type TemplateContext, } from "./auto-reply/templating.js"; import { CONFIG_PATH, type WarelayConfig, type SessionConfig, type SessionScope, type ReplyMode, type ClaudeOutputFormat, loadConfig, } from "./config/config.js"; import { sendCommand } from "./commands/send.js"; import { statusCommand } from "./commands/status.js"; import { upCommand } from "./commands/up.js"; import { webhookCommand } from "./commands/webhook.js"; import { danger, info, isVerbose, isYes, logVerbose, setVerbose, setYes, success, warn, } from "./globals.js"; import { loginWeb, monitorWebInbox, sendMessageWeb, WA_WEB_AUTH_DIR, webAuthExists, } from "./provider-web.js"; import type { Provider } from "./utils.js"; import { assertProvider, CONFIG_DIR, jidToE164, normalizeE164, normalizePath, sleep, toWhatsappJid, withWhatsAppPrefix, } from "./utils.js"; import { DEFAULT_IDLE_MINUTES, DEFAULT_RESET_TRIGGER, deriveSessionKey, loadSessionStore, resolveStorePath, saveSessionStore, SESSION_STORE_DEFAULT, } from "./config/sessions.js"; dotenv.config({ quiet: true }); import { buildProgram } from "./cli/program.js"; const program = buildProgram(); type CliDeps = { sendMessage: typeof sendMessage; sendMessageWeb: typeof sendMessageWeb; waitForFinalStatus: typeof waitForFinalStatus; assertProvider: typeof assertProvider; createClient?: typeof createClient; monitorTwilio: typeof monitorTwilio; listRecentMessages: typeof listRecentMessages; ensurePortAvailable: typeof ensurePortAvailable; startWebhook: typeof startWebhook; waitForever: typeof waitForever; ensureBinary: typeof ensureBinary; ensureFunnel: typeof ensureFunnel; getTailnetHostname: typeof getTailnetHostname; readEnv: typeof readEnv; findWhatsappSenderSid: typeof findWhatsappSenderSid; updateWebhook: typeof updateWebhook; handlePortError: typeof handlePortError; monitorWebProvider: typeof monitorWebProvider; }; function createDefaultDeps(): CliDeps { return { sendMessage, sendMessageWeb, waitForFinalStatus, assertProvider, createClient, monitorTwilio, listRecentMessages, ensurePortAvailable, startWebhook, waitForever, ensureBinary, ensureFunnel, getTailnetHostname, readEnv, findWhatsappSenderSid, updateWebhook, handlePortError, monitorWebProvider, }; } class PortInUseError extends Error { port: number; details?: string; constructor(port: number, details?: string) { super(`Port ${port} is already in use.`); this.name = "PortInUseError"; this.port = port; this.details = details; } } function isErrno(err: unknown): err is NodeJS.ErrnoException { return Boolean(err && typeof err === "object" && "code" in err); } async function describePortOwner(port: number): Promise { // Best-effort process info for a listening port (macOS/Linux). try { const { stdout } = await runExec("lsof", [ "-i", `tcp:${port}`, "-sTCP:LISTEN", "-nP", ]); const trimmed = stdout.trim(); if (trimmed) return trimmed; } catch (err) { logVerbose(`lsof unavailable: ${String(err)}`); } return undefined; } async function ensurePortAvailable(port: number): Promise { // Detect EADDRINUSE early with a friendly message. try { await new Promise((resolve, reject) => { const tester = net .createServer() .once("error", (err) => reject(err)) .once("listening", () => { tester.close(() => resolve()); }) .listen(port); }); } catch (err) { if (isErrno(err) && err.code === "EADDRINUSE") { const details = await describePortOwner(port); throw new PortInUseError(port, details); } throw err; } } async function handlePortError( err: unknown, port: number, context: string, runtime: RuntimeEnv = defaultRuntime, ): Promise { if ( err instanceof PortInUseError || (isErrno(err) && err.code === "EADDRINUSE") ) { const details = err instanceof PortInUseError ? err.details : await describePortOwner(port); runtime.error(danger(`${context} failed: port ${port} is already in use.`)); if (details) { runtime.error(info("Port listener details:")); runtime.error(details); if (/warelay|src\/index\.ts|dist\/index\.js/.test(details)) { runtime.error( warn( "It looks like another warelay instance is already running. Stop it or pick a different port.", ), ); } } runtime.error( info( "Resolve by stopping the process using the port or passing --port .", ), ); runtime.exit(1); } runtime.error(danger(`${context} failed: ${String(err)}`)); return runtime.exit(1); } async function ensureBinary( name: string, exec: typeof runExec = runExec, runtime: RuntimeEnv = defaultRuntime, ): Promise { // Abort early if a required CLI tool is missing. await exec("which", [name]).catch(() => { runtime.error(`Missing required binary: ${name}. Please install it.`); runtime.exit(1); }); } async function promptYesNo( question: string, defaultYes = false, ): Promise { if (isVerbose() && isYes()) return true; // redundant guard when both flags set if (isYes()) return true; const rl = readline.createInterface({ input, output }); const suffix = defaultYes ? " [Y/n] " : " [y/N] "; const answer = (await rl.question(`${question}${suffix}`)) .trim() .toLowerCase(); rl.close(); if (!answer) return defaultYes; return answer.startsWith("y"); } function createClient(env: EnvConfig) { // Twilio client using either auth token or API key/secret. if ("authToken" in env.auth) { return Twilio(env.accountSid, env.auth.authToken, { accountSid: env.accountSid, }); } return Twilio(env.auth.apiKey, env.auth.apiSecret, { accountSid: env.accountSid, }); } // sendMessage / waitForFinalStatus now live in src/twilio/send.ts and are imported above. // startWebhook now lives in src/twilio/webhook.ts; keep shim for existing imports/tests. async function startWebhook( port: number, path = "/webhook/whatsapp", autoReply: string | undefined, verbose: boolean, runtime: RuntimeEnv = defaultRuntime, ): Promise { return startWebhookImpl(port, path, autoReply, verbose, runtime); } function waitForever() { // Keep event loop alive via an unref'ed interval plus a pending promise. const interval = setInterval(() => {}, 1_000_000); interval.unref(); return new Promise(() => { /* never resolve */ }); } async function getTailnetHostname(exec: typeof runExec = runExec) { // Derive tailnet hostname (or IP fallback) from tailscale status JSON. const { stdout } = await exec("tailscale", ["status", "--json"]); const parsed = stdout ? (JSON.parse(stdout) as Record) : {}; const self = typeof parsed.Self === "object" && parsed.Self !== null ? (parsed.Self as Record) : undefined; const dns = typeof self?.DNSName === "string" ? (self.DNSName as string) : undefined; const ips = Array.isArray(self?.TailscaleIPs) ? (self.TailscaleIPs as string[]) : []; if (dns && dns.length > 0) return dns.replace(/\.$/, ""); if (ips.length > 0) return ips[0]; throw new Error("Could not determine Tailscale DNS or IP"); } async function ensureGoInstalled( exec: typeof runExec = runExec, prompt: typeof promptYesNo = promptYesNo, runtime: RuntimeEnv = defaultRuntime, ) { // Ensure Go toolchain is present; offer Homebrew install if missing. const hasGo = await exec("go", ["version"]).then( () => true, () => false, ); if (hasGo) return; const install = await prompt( "Go is not installed. Install via Homebrew (brew install go)?", true, ); if (!install) { runtime.error("Go is required to build tailscaled from source. Aborting."); runtime.exit(1); } logVerbose("Installing Go via Homebrew…"); await exec("brew", ["install", "go"]); } async function ensureTailscaledInstalled( exec: typeof runExec = runExec, prompt: typeof promptYesNo = promptYesNo, runtime: RuntimeEnv = defaultRuntime, ) { // Ensure tailscaled binary exists; install via Homebrew tailscale if missing. const hasTailscaled = await exec("tailscaled", ["--version"]).then( () => true, () => false, ); if (hasTailscaled) return; const install = await prompt( "tailscaled not found. Install via Homebrew (tailscale package)?", true, ); if (!install) { runtime.error("tailscaled is required for user-space funnel. Aborting."); runtime.exit(1); } logVerbose("Installing tailscaled via Homebrew…"); await exec("brew", ["install", "tailscale"]); } async function ensureFunnel( port: number, exec: typeof runExec = runExec, runtime: RuntimeEnv = defaultRuntime, prompt: typeof promptYesNo = promptYesNo, ) { // Ensure Funnel is enabled and publish the webhook port. try { const statusOut = ( await exec("tailscale", ["funnel", "status", "--json"]) ).stdout.trim(); const parsed = statusOut ? (JSON.parse(statusOut) as Record) : {}; if (!parsed || Object.keys(parsed).length === 0) { runtime.error( danger("Tailscale Funnel is not enabled on this tailnet/device."), ); runtime.error( info( "Enable in admin console: https://login.tailscale.com/admin (see https://tailscale.com/kb/1223/funnel)", ), ); runtime.error( info( "macOS user-space tailscaled docs: https://github.com/tailscale/tailscale/wiki/Tailscaled-on-macOS", ), ); const proceed = await prompt( "Attempt local setup with user-space tailscaled?", true, ); if (!proceed) runtime.exit(1); await ensureGoInstalled(exec, prompt, runtime); await ensureTailscaledInstalled(exec, prompt, runtime); } logVerbose(`Enabling funnel on port ${port}…`); const { stdout } = await exec( "tailscale", ["funnel", "--yes", "--bg", `${port}`], { maxBuffer: 200_000, timeoutMs: 15_000, }, ); if (stdout.trim()) console.log(stdout.trim()); } catch (err) { const errOutput = err as { stdout?: unknown; stderr?: unknown }; const stdout = typeof errOutput.stdout === "string" ? errOutput.stdout : ""; const stderr = typeof errOutput.stderr === "string" ? errOutput.stderr : ""; if (stdout.includes("Funnel is not enabled")) { console.error(danger("Funnel is not enabled on this tailnet/device.")); const linkMatch = stdout.match(/https?:\/\/\S+/); if (linkMatch) { console.error(info(`Enable it here: ${linkMatch[0]}`)); } else { console.error( info( "Enable in admin console: https://login.tailscale.com/admin (see https://tailscale.com/kb/1223/funnel)", ), ); } } if ( stderr.includes("client version") || stdout.includes("client version") ) { console.error( warn( "Tailscale client/server version mismatch detected; try updating tailscale/tailscaled.", ), ); } runtime.error( "Failed to enable Tailscale Funnel. Is it allowed on your tailnet?", ); runtime.error( info( "Tip: you can fall back to polling (no webhooks needed): `pnpm warelay relay --provider twilio --interval 5 --lookback 10`", ), ); if (isVerbose()) { if (stdout.trim()) runtime.error(chalk.gray(`stdout: ${stdout.trim()}`)); if (stderr.trim()) runtime.error(chalk.gray(`stderr: ${stderr.trim()}`)); runtime.error(err as Error); } runtime.exit(1); } } async function findWhatsappSenderSid( client: ReturnType, from: string, explicitSenderSid?: string, runtime: RuntimeEnv = defaultRuntime, ) { // Use explicit sender SID if provided, otherwise list and match by sender_id. if (explicitSenderSid) { logVerbose(`Using TWILIO_SENDER_SID from env: ${explicitSenderSid}`); return explicitSenderSid; } try { // Prefer official SDK list helper to avoid request-shape mismatches. // Twilio helper types are broad; we narrow to expected shape. const senderClient = client as unknown as TwilioSenderListClient; const senders = await senderClient.messaging.v2.channelsSenders.list({ channel: "whatsapp", pageSize: 50, }); if (!senders) { throw new Error('List senders response missing "senders" array'); } const match = senders.find( (s) => (typeof s.senderId === "string" && s.senderId === withWhatsAppPrefix(from)) || (typeof s.sender_id === "string" && s.sender_id === withWhatsAppPrefix(from)), ); if (!match || typeof match.sid !== "string") { throw new Error( `Could not find sender ${withWhatsAppPrefix(from)} in Twilio account`, ); } return match.sid; } catch (err) { runtime.error(danger("Unable to list WhatsApp senders via Twilio API.")); if (isVerbose()) { runtime.error(err as Error); } runtime.error( info( "Set TWILIO_SENDER_SID in .env to skip discovery (Twilio Console → Messaging → Senders → WhatsApp).", ), ); runtime.exit(1); } } async function setMessagingServiceWebhook( client: TwilioSenderListClient, url: string, method: "POST" | "GET" = "POST", ): Promise { return setMessagingServiceWebhookImpl(client, url, method); } async function updateWebhook( client: ReturnType, senderSid: string, url: string, method: "POST" | "GET" = "POST", runtime: RuntimeEnv = defaultRuntime, ) { return updateWebhookImpl(client, senderSid, url, method, runtime); } function ensureTwilioEnv(runtime: RuntimeEnv = defaultRuntime) { const required = ["TWILIO_ACCOUNT_SID", "TWILIO_WHATSAPP_FROM"]; const missing = required.filter((k) => !process.env[k]); const hasToken = Boolean(process.env.TWILIO_AUTH_TOKEN); const hasKey = Boolean( process.env.TWILIO_API_KEY && process.env.TWILIO_API_SECRET, ); if (missing.length > 0 || (!hasToken && !hasKey)) { runtime.error( danger( `Missing Twilio env: ${missing.join(", ") || "auth token or api key/secret"}. Set them in .env before using provider=twilio.`, ), ); runtime.exit(1); } } async function pickProvider(pref: Provider | "auto"): Promise { if (pref !== "auto") return pref; const hasWeb = await webAuthExists(); if (hasWeb) return "web"; return "twilio"; } function readWebSelfId() { const credsPath = path.join(WA_WEB_AUTH_DIR, "creds.json"); try { if (!fs.existsSync(credsPath)) { return { e164: null, jid: null }; } const raw = fs.readFileSync(credsPath, "utf-8"); const parsed = JSON.parse(raw) as { me?: { id?: string } } | undefined; const jid = parsed?.me?.id ?? null; const e164 = jid ? jidToE164(jid) : null; return { e164, jid }; } catch { return { e164: null, jid: null }; } } function logWebSelfId(runtime: RuntimeEnv = defaultRuntime) { const { e164, jid } = readWebSelfId(); const details = e164 || jid ? `${e164 ?? "unknown"}${jid ? ` (jid ${jid})` : ""}` : "unknown"; runtime.log(info(`Listening on web session: ${details}`)); } function logTwilioFrom(runtime: RuntimeEnv = defaultRuntime) { const env = readEnv(runtime); runtime.log( info(`Provider: twilio (polling inbound) | from ${env.whatsappFrom}`), ); } async function monitorTwilio( intervalSeconds: number, lookbackMinutes: number, clientOverride?: ReturnType, maxIterations = Infinity, ) { // Delegate to the refactored monitor in src/twilio/monitor.ts. return monitorTwilioImpl( intervalSeconds, lookbackMinutes, { client: clientOverride, maxIterations, deps: { autoReplyIfConfigured, listRecentMessages, readEnv, createClient, sleep, }, runtime: defaultRuntime, }, ); } async function monitorWebProvider( verbose: boolean, listenerFactory = monitorWebInbox, keepAlive = true, replyResolver: typeof getReplyFromConfig = getReplyFromConfig, ) { // Listen for inbound personal WhatsApp Web messages and auto-reply if configured. const listener = await listenerFactory({ verbose, onMessage: async (msg) => { const ts = msg.timestamp ? new Date(msg.timestamp).toISOString() : new Date().toISOString(); console.log(`\n[${ts}] ${msg.from} -> ${msg.to}: ${msg.body}`); const replyText = await replyResolver( { Body: msg.body, From: msg.from, To: msg.to, MessageSid: msg.id, }, { onReplyStart: msg.sendComposing, }, ); if (!replyText) return; try { await msg.reply(replyText); if (isVerbose()) { console.log(success(`↩️ Auto-replied to ${msg.from} (web)`)); } } catch (err) { console.error( danger( `Failed sending web auto-reply to ${msg.from}: ${String(err)}`, ), ); } }, }); console.log( info( "📡 Listening for personal WhatsApp Web inbound messages. Leave this running; Ctrl+C to stop.", ), ); process.on("SIGINT", () => { void listener.close().finally(() => { console.log("\n👋 Web monitor stopped"); defaultRuntime.exit(0); }); }); if (keepAlive) { await waitForever(); } } async function performSend( opts: { to: string; message: string; wait: string; poll: string; provider: Provider; }, deps: CliDeps, _exitFn: (code: number) => never = defaultRuntime.exit, _runtime: RuntimeEnv = defaultRuntime, ) { deps.assertProvider(opts.provider); const waitSeconds = Number.parseInt(opts.wait, 10); const pollSeconds = Number.parseInt(opts.poll, 10); if (Number.isNaN(waitSeconds) || waitSeconds < 0) { throw new Error("Wait must be >= 0 seconds"); } if (Number.isNaN(pollSeconds) || pollSeconds <= 0) { throw new Error("Poll must be > 0 seconds"); } if (opts.provider === "web") { if (waitSeconds !== 0) { console.log(info("Wait/poll are Twilio-only; ignored for provider=web.")); } await deps.sendMessageWeb(opts.to, opts.message, { verbose: isVerbose() }); return; } const result = await deps.sendMessage(opts.to, opts.message, runtime); if (!result) return; if (waitSeconds === 0) return; await deps.waitForFinalStatus( result.client, result.sid, waitSeconds, pollSeconds, ); } async function performStatus( opts: { limit: string; lookback: string; json?: boolean }, deps: CliDeps, _exitFn: (code: number) => never = defaultRuntime.exit, _runtime: RuntimeEnv = defaultRuntime, ) { const limit = Number.parseInt(opts.limit, 10); const lookbackMinutes = Number.parseInt(opts.lookback, 10); if (Number.isNaN(limit) || limit <= 0 || limit > 200) { throw new Error("limit must be between 1 and 200"); } if (Number.isNaN(lookbackMinutes) || lookbackMinutes <= 0) { throw new Error("lookback must be > 0 minutes"); } const messages = await deps.listRecentMessages(lookbackMinutes, limit); if (opts.json) { console.log(JSON.stringify(messages, null, 2)); return; } if (messages.length === 0) { console.log("No messages found in the requested window."); return; } for (const m of messages) { console.log(formatMessageLine(m)); } } async function performWebhookSetup( opts: { port: string; path: string; reply?: string; verbose?: boolean; }, deps: CliDeps, _exitFn: (code: number) => never = defaultRuntime.exit, _runtime: RuntimeEnv = defaultRuntime, ) { const port = Number.parseInt(opts.port, 10); if (Number.isNaN(port) || port <= 0 || port >= 65536) { throw new Error("Port must be between 1 and 65535"); } await deps.ensurePortAvailable(port); const server = await deps.startWebhook( port, opts.path, opts.reply, Boolean(opts.verbose), ); return server; } async function performUp( opts: { port: string; path: string; verbose?: boolean; yes?: boolean; }, deps: CliDeps, _exitFn: (code: number) => never = defaultRuntime.exit, _runtime: RuntimeEnv = defaultRuntime, ) { const port = Number.parseInt(opts.port, 10); if (Number.isNaN(port) || port <= 0 || port >= 65536) { throw new Error("Port must be between 1 and 65535"); } await deps.ensurePortAvailable(port); // Validate env and binaries const env = deps.readEnv(runtime); await deps.ensureBinary("tailscale", runExec, runtime); // Enable Funnel first so we don't keep a webhook running on failure await deps.ensureFunnel(port, runExec, runtime, promptYesNo); const host = await deps.getTailnetHostname(runExec); const publicUrl = `https://${host}${opts.path}`; console.log(`🌐 Public webhook URL (via Funnel): ${publicUrl}`); // Start webhook locally (after funnel success) const server = await deps.startWebhook( port, opts.path, undefined, Boolean(opts.verbose), ); // Configure Twilio sender webhook const client = createClient(env); const senderSid = await deps.findWhatsappSenderSid( client, env.whatsappFrom, env.whatsappSenderSid, ); await deps.updateWebhook(client, senderSid, publicUrl, "POST", runtime); console.log( "\nSetup complete. Leave this process running to keep the webhook online. Ctrl+C to stop.", ); return { server, publicUrl, senderSid }; } type ListedMessage = { sid: string; status: string | null; direction: string | null; dateCreated?: Date | null; from?: string | null; to?: string | null; body?: string | null; errorCode?: number | null; errorMessage?: string | null; }; function uniqueBySid(messages: ListedMessage[]): ListedMessage[] { const seen = new Set(); const deduped: ListedMessage[] = []; for (const m of messages) { if (seen.has(m.sid)) continue; seen.add(m.sid); deduped.push(m); } return deduped; } function sortByDateDesc(messages: ListedMessage[]): ListedMessage[] { return [...messages].sort((a, b) => { const da = a.dateCreated?.getTime() ?? 0; const db = b.dateCreated?.getTime() ?? 0; return db - da; }); } function formatMessageLine(m: ListedMessage): string { const ts = m.dateCreated?.toISOString() ?? "unknown-time"; const dir = m.direction === "inbound" ? "⬅️ " : m.direction === "outbound-api" || m.direction === "outbound-reply" ? "➡️ " : "↔️ "; const status = m.status ?? "unknown"; const err = m.errorCode != null ? ` error ${m.errorCode}${m.errorMessage ? ` (${m.errorMessage})` : ""}` : ""; const body = (m.body ?? "").replace(/\s+/g, " ").trim(); const bodyPreview = body.length > 140 ? `${body.slice(0, 137)}…` : body || ""; return `[${ts}] ${dir}${m.from ?? "?"} -> ${m.to ?? "?"} | ${status}${err} | ${bodyPreview} (sid ${m.sid})`; } async function listRecentMessages( lookbackMinutes: number, limit: number, clientOverride?: ReturnType, ): Promise { const env = readEnv(); const client = clientOverride ?? createClient(env); const from = withWhatsAppPrefix(env.whatsappFrom); const since = new Date(Date.now() - lookbackMinutes * 60_000); // Fetch inbound (to our WA number) and outbound (from our WA number), merge, sort, limit. const fetchLimit = Math.min(Math.max(limit * 2, limit + 10), 100); const inbound = await client.messages.list({ to: from, dateSentAfter: since, limit: fetchLimit, }); const outbound = await client.messages.list({ from, dateSentAfter: since, limit: fetchLimit, }); const inboundArr = Array.isArray(inbound) ? inbound : []; const outboundArr = Array.isArray(outbound) ? outbound : []; const combined = uniqueBySid( [...inboundArr, ...outboundArr].map((m) => ({ sid: m.sid, status: m.status ?? null, direction: m.direction ?? null, dateCreated: m.dateCreated, from: m.from, to: m.to, body: m.body, errorCode: m.errorCode ?? null, errorMessage: m.errorMessage ?? null, })), ); return sortByDateDesc(combined).slice(0, limit); } export { assertProvider, autoReplyIfConfigured, applyTemplate, createClient, deriveSessionKey, describePortOwner, ensureBinary, ensureFunnel, ensureGoInstalled, ensurePortAvailable, ensureTailscaledInstalled, findIncomingNumberSid, findMessagingServiceSid, findWhatsappSenderSid, formatMessageLine, formatTwilioError, getReplyFromConfig, getTailnetHostname, handlePortError, logTwilioSendError, listRecentMessages, loadConfig, loadSessionStore, monitorTwilio, monitorWebProvider, normalizeE164, PortInUseError, promptYesNo, createDefaultDeps, performSend, performStatus, performUp, performWebhookSetup, readEnv, resolveStorePath, runCommandWithTimeout, runExec, saveSessionStore, sendMessage, sendTypingIndicator, setMessagingServiceWebhook, sortByDateDesc, startWebhook, updateWebhook, uniqueBySid, waitForFinalStatus, waitForever, toWhatsappJid, program, }; const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; if (isMain) { program.parseAsync(process.argv); }