diff --git a/CHANGELOG.md b/CHANGELOG.md index f376f83c4..96426ef16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,14 @@ ## 0.1.0 — 2025-11-25 ### CLI & Providers -- Bundles a single `warelay` CLI with commands for `send`, `relay`, `status`, `webhook`, `up`, `login`, and tmux helpers `relay:tmux` / `relay:tmux:attach` (see `src/cli/program.ts`); `webhook` now accepts `--ingress tailscale|none` with `up` as an alias for the Tailscale path. +- Bundles a single `warelay` CLI with commands for `send`, `relay`, `status`, `webhook`, `login`, and tmux helpers `relay:tmux` / `relay:tmux:attach` (see `src/cli/program.ts`); `webhook` accepts `--ingress tailscale|none`. - Supports two messaging backends: **Twilio** (default) and **personal WhatsApp Web**; `relay --provider auto` selects Web when a cached login exists, otherwise falls back to Twilio polling (`provider-web.ts`, `cli/program.ts`). - `send` can target either provider, optionally wait for delivery status (Twilio only), output JSON, dry-run payloads, and attach media (`commands/send.ts`). - `status` merges inbound + outbound Twilio traffic with formatted lines or JSON output (`commands/status.ts`, `twilio/messages.ts`). ### Webhook, Funnel & Port Management - `webhook` starts an Express server for inbound Twilio callbacks, logs requests, and optionally auto-replies with static text or config-driven replies (`twilio/webhook.ts`, `commands/webhook.ts`). -- `up` automates end-to-end webhook setup: ensures required binaries, enables Tailscale Funnel, starts the webhook on the chosen port/path, discovers the WhatsApp sender SID, and updates Twilio webhook URLs with multiple fallbacks (`commands/up.ts`, `infra/tailscale.ts`, `twilio/update-webhook.ts`, `twilio/senders.ts`). +- `webhook --ingress tailscale` automates end-to-end webhook setup: ensures required binaries, enables Tailscale Funnel, starts the webhook on the chosen port/path, discovers the WhatsApp sender SID, and updates Twilio webhook URLs with multiple fallbacks (`commands/up.ts`, `infra/tailscale.ts`, `twilio/update-webhook.ts`, `twilio/senders.ts`). - Guardrails detect busy ports with helpful diagnostics and aborts when conflicts are found (`infra/ports.ts`). ### Auto-Reply Engine diff --git a/README.md b/README.md index f3055e7f7..16e477410 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Install from npm (global): `npm install -g warelay` (Node 22+). Then choose **on 2. Send a message: `warelay send --to +12345550000 --message "Hi from warelay"`. 3. Receive replies: - Polling (no ingress): `warelay relay --provider twilio --interval 5 --lookback 10` - - Webhook + public URL via Tailscale Funnel: `warelay webhook --ingress tailscale --port 42873 --path /webhook/whatsapp --verbose` (alias: `warelay up`) + - Webhook + public URL via Tailscale Funnel: `warelay webhook --ingress tailscale --port 42873 --path /webhook/whatsapp --verbose` > Already developing locally? You can still run `pnpm install` and `pnpm warelay ...` from the repo, but end users only need the npm package. @@ -23,7 +23,7 @@ Install from npm (global): `npm install -g warelay` (Node 22+). Then choose **on - **Two providers:** Twilio (default) for reliable delivery + status; Web provider for quick personal sends/receives via QR login. - **Auto-replies:** Static templates or external commands (Claude-aware), with per-sender or global sessions and `/new` resets. - Claude setup guide: see `docs/claude-config.md` for the exact Claude CLI configuration we support. -- **Webhook in one go:** `warelay webhook --ingress tailscale` enables Tailscale Funnel, runs the webhook server, and updates the Twilio sender callback URL (alias: `warelay up`). +- **Webhook in one go:** `warelay webhook --ingress tailscale` enables Tailscale Funnel, runs the webhook server, and updates the Twilio sender callback URL. - **Polling fallback:** `relay` polls Twilio when webhooks aren’t available; works headless. - **Status + delivery tracking:** `status` shows recent inbound/outbound; `send` can wait for final Twilio status. @@ -34,11 +34,10 @@ Install from npm (global): `npm install -g warelay` (Node 22+). Then choose **on | `warelay relay` | Auto-reply loop (poll Twilio or listen on Web) | `--provider ` `--interval ` `--lookback ` `--verbose` | | `warelay status` | Show recent sent/received messages | `--limit ` `--lookback ` `--json` | | `warelay webhook` | Run inbound webhook (`ingress=tailscale` updates Twilio; `none` is local-only) | `--ingress tailscale\|none` `--port ` `--path ` `--reply ` `--verbose` `--yes` `--dry-run` | -| `warelay up` | Alias: `warelay webhook --ingress tailscale` | `--port ` `--path ` `--verbose` `--yes` `--dry-run` | | `warelay login` | Link personal WhatsApp Web via QR | `--verbose` | ### Sending images -- Twilio: `warelay send --to +1... --message "Hi" --media ./pic.jpg --serve-media` (needs `warelay webhook`/`up` or `--serve-media` to auto-host via Funnel; max 5 MB). +- Twilio: `warelay send --to +1... --message "Hi" --media ./pic.jpg --serve-media` (needs `warelay webhook --ingress tailscale` or `--serve-media` to auto-host via Funnel; max 5 MB). - Web: `warelay send --provider web --media ./pic.jpg --message "Hi"` (local path or URL; no hosting needed). - Auto-replies can attach `mediaUrl` in `~/.warelay/warelay.json` (used alongside `text` when present). @@ -111,7 +110,7 @@ Templating tokens: `{{Body}}`, `{{BodyStripped}}`, `{{From}}`, `{{To}}`, `{{Mess ## Webhook & Tailscale Flow - `warelay webhook --ingress none` starts the local Express server on your chosen port/path; add `--reply "Got it"` for a static reply when no config file is present. -- `warelay webhook --ingress tailscale` (alias: `warelay up`) enables Tailscale Funnel, prints the public URL (`https://`), starts the webhook, discovers the WhatsApp sender SID, and updates Twilio callbacks to the Funnel URL. +- `warelay webhook --ingress tailscale` enables Tailscale Funnel, prints the public URL (`https://`), starts the webhook, discovers the WhatsApp sender SID, and updates Twilio callbacks to the Funnel URL. - If Funnel is not allowed on your tailnet, the CLI exits with guidance; you can still use `relay --provider twilio` to poll without webhooks. ## Troubleshooting Tips @@ -125,5 +124,5 @@ Templating tokens: `{{Body}}`, `{{BodyStripped}}`, `{{From}}`, `{{To}}`, `{{Mess - 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. - 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 `login` if the session is dropped. - 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. -- 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. +- 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 webhook --ingress tailscale ...` in a systemd service or macOS LaunchAgent; ensure environment variables are loaded in that context. - Rotating credentials: Update `.env` (Twilio keys), rerun your process; for Web provider, delete `~/.warelay/credentials/` and rerun `pnpm warelay login` to relink. diff --git a/docs/claude-config.md b/docs/claude-config.md index f816ec762..88ce04f40 100644 --- a/docs/claude-config.md +++ b/docs/claude-config.md @@ -70,7 +70,7 @@ Notes on this configuration: ## Troubleshooting tips - Command takes too long: lower `timeoutSeconds` or simplify the prompt. Timeouts kill the Claude process. - No reply: ensure the sender number is in `allowFrom` (or remove the allowlist), and confirm `claude login` was run in the same environment. -- Media fails on Twilio: run `warelay up` (or `warelay webhook --serve-media` via `send --serve-media`) so the media host is reachable over HTTPS. +- Media fails on Twilio: run `warelay webhook --ingress tailscale` (or `warelay webhook --serve-media` via `send --serve-media`) so the media host is reachable over HTTPS. - Stuck queue: enable `--verbose` to see “queued for …ms” messages and confirm commands are draining. Use `pnpm vitest` to run unit tests if you change queue logic. ## Minimal text-only variant diff --git a/docs/images.md b/docs/images.md index bbe7696d2..cbc8f1ac7 100644 --- a/docs/images.md +++ b/docs/images.md @@ -28,12 +28,12 @@ This document defines how `warelay` should handle sending and replying with imag ### Twilio - Twilio API requires a public HTTPS `MediaUrl`; it will not accept local paths. - Hosting strategy: reuse the webhook/Funnel port. - - When `--media` is a local path, copy to temp dir (`~/.warelay/media/`), serve at `/media/` on the existing Express app started for webhook/up, or spin up a short-lived server on demand for `send`. +- When `--media` is a local path, copy to temp dir (`~/.warelay/media/`), serve at `/media/` on the existing Express app started for webhook, or spin up a short-lived server on demand for `send`. - `MediaUrl` = `https://.ts.net/media/`. - Files auto-removed after TTL (default 2 minutes) or after first successful fetch (best-effort). - Enforce size limit 5 MB; reject early with clear error. - If `--media` is already an HTTPS URL, pass through unchanged. -- Fallback: if Funnel is not enabled (or host unknown) and a local path is provided, fail with guidance to run `warelay up` (or pass a URL instead). +- Fallback: if Funnel is not enabled (or host unknown) and a local path is provided, fail with guidance to run `warelay webhook --ingress tailscale` (or pass a URL instead). ## Hosting/Server Details - Extend `startWebhook` Express app: @@ -58,7 +58,7 @@ This document defines how `warelay` should handle sending and replying with imag - Size guard: only download if ≤5 MB; else skip and log. ## Errors & Messaging -- Local path with twilio + Funnel disabled → error: “Twilio media needs a public URL; start `warelay up`/`warelay webhook` with Funnel or pass an https:// URL.” +- Local path with twilio + Funnel disabled → error: “Twilio media needs a public URL; start `warelay webhook --ingress tailscale` or pass an https:// URL.” - File too large (>5 MB) → “Media exceeds 5 MB limit; resize or host elsewhere.” - Download failure for web provider → “Failed to load media from ; skipping send.” @@ -71,4 +71,4 @@ This document defines how `warelay` should handle sending and replying with imag ## Open Decisions (confirm before coding) - TTL for temp media (proposal: 2 minutes, cleanup at start + interval). - One-file-per-send vs. batching: default to one-file-per-send; multi-attach not supported. -- Should `warelay send --provider twilio --media` implicitly start the media server (even if webhook not running), or require `warelay up/webhook` already active? (Proposal: auto-start lightweight server on demand, auto-stop after media is fetched or TTL.) +- Should `warelay send --provider twilio --media` implicitly start the media server (even if webhook not running), or require `warelay webhook` already active? (Proposal: auto-start lightweight server on demand, auto-stop after media is fetched or TTL.) diff --git a/src/cli/program.test.ts b/src/cli/program.test.ts index 092ab5a72..b31f531d2 100644 --- a/src/cli/program.test.ts +++ b/src/cli/program.test.ts @@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; const sendCommand = vi.fn(); const statusCommand = vi.fn(); -const upCommand = vi.fn().mockResolvedValue({ server: undefined }); const webhookCommand = vi.fn().mockResolvedValue(undefined); const ensureTwilioEnv = vi.fn(); const loginWeb = vi.fn(); @@ -24,7 +23,6 @@ const runtime = { vi.mock("../commands/send.js", () => ({ sendCommand })); vi.mock("../commands/status.js", () => ({ statusCommand })); -vi.mock("../commands/up.js", () => ({ upCommand })); vi.mock("../commands/webhook.js", () => ({ webhookCommand })); vi.mock("../env.js", () => ({ ensureTwilioEnv })); vi.mock("../runtime.js", () => ({ defaultRuntime: runtime })); diff --git a/src/cli/program.ts b/src/cli/program.ts index 3d029a6e6..e5a85aaa5 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -1,7 +1,6 @@ import { Command } from "commander"; 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 { ensureTwilioEnv } from "../env.js"; import { danger, info, setVerbose, setYes, warn } from "../globals.js"; @@ -221,44 +220,6 @@ Examples: } }); - program - .command("up") - .description( - "Alias: webhook --ingress tailscale (Funnel + Twilio callback)", - ) - .option("-p, --port ", "Port to listen on", "42873") - .option("--path ", "Webhook path", "/webhook/whatsapp") - .option("--verbose", "Verbose logging during setup/webhook", false) - .option("-y, --yes", "Auto-confirm prompts when possible", false) - .option( - "--dry-run", - "Print planned actions without touching network", - false, - ) - // istanbul ignore next - .action(async (opts) => { - setVerbose(Boolean(opts.verbose)); - setYes(Boolean(opts.yes)); - const deps = createDefaultDeps(); - try { - const { server } = await upCommand(opts, deps, defaultRuntime); - if (!server) { - defaultRuntime.log(info("Up dry-run complete; no server started.")); - return; - } - process.on("SIGINT", () => { - server.close(() => { - console.log("\n👋 Webhook stopped"); - defaultRuntime.exit(0); - }); - }); - await deps.waitForever(); - } catch (err) { - defaultRuntime.error(String(err)); - defaultRuntime.exit(1); - } - }); - program .command("relay:tmux") .description(