From 17fa2f40533ce577eae62359bd7821492acbe92b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Dec 2025 12:43:13 +0100 Subject: [PATCH] refactor(cli): drop tmux helpers and update help copy --- CHANGELOG.md | 3 +- README.md | 8 +-- docs/clawd.md | 7 ++- docs/heartbeat.md | 5 +- docs/tmux.md | 17 ------ src/cli/program.test.ts | 12 ----- src/cli/program.ts | 103 ++++--------------------------------- src/cli/relay_tmux.test.ts | 47 ----------------- src/cli/relay_tmux.ts | 50 ------------------ 9 files changed, 23 insertions(+), 229 deletions(-) delete mode 100644 docs/tmux.md delete mode 100644 src/cli/relay_tmux.test.ts delete mode 100644 src/cli/relay_tmux.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index caf8e5cd4..5567c0a4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ First Clawdis release after the Warelay rebrand. This is a semver-major because - Pi/Tau only: `inbound.reply.agent.kind` accepts only `"pi"`, and the agent CLI/CLI flags for Claude/Codex/Gemini were removed. The Pi CLI runs in RPC mode with a persistent worker. - WhatsApp Web is the only transport; Twilio support and related CLI flags/tests were removed. - Direct chats now collapse into a single `main` session by default (no config needed); groups stay isolated as `group:`. +- Relay background helpers were removed; run `clawdis relay --verbose` under your supervisor of choice if you want it detached. ### macOS companion app - **Clawdis.app menu bar companion**: packaged, signed bundle with relay start/stop, launchd toggle, project-root and pnpm/node auto-resolution, live log shortcut, restart button, and status/recipient table plus badges/dimming for attention and paused states. @@ -147,7 +148,7 @@ First Clawdis release after the Warelay rebrand. This is a semver-major because ### Changes - Heartbeat interval default 10m for command mode; prompt `HEARTBEAT /think:high`; skips don’t refresh session; session `heartbeatIdleMinutes` support. -- Heartbeat tooling: `--session-id`, `--heartbeat-now`, relay helpers `relay:heartbeat` and `relay:heartbeat:tmux`. +- Heartbeat tooling: `--session-id`, `--heartbeat-now`, and a relay helper `relay:heartbeat` for immediate startup probes. - Prompt structure: `sessionIntro` plus per-message `/think:high`; session idle up to 7 days. - Thinking directives: `/think:`; Pi uses `--thinking`; others append cue; `/think:off` no-op. - Robustness: Baileys/WebSocket guards; global unhandled error handlers; WhatsApp LID mapping; hosted media MIME-sniffing and cleanup. diff --git a/README.md b/README.md index 5e9e6f6f3..aa260168f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🦞 CLAWDIS β€” WhatsApp Gateway for AI Agents +# 🦞 CLAWDIS β€” WhatsApp & Telegram Gateway for AI Agents

CLAWDIS @@ -14,12 +14,13 @@ MIT License

-**CLAWDIS** (formerly Warelay) is a WhatsApp-to-AI gateway. Send a message, get an AI response. It's like having a genius lobster in your pocket 24/7. +**CLAWDIS** (formerly Warelay) is a WhatsApp- and Telegram-to-AI gateway. Send a message, get an AI response. It's like having a genius lobster in your pocket 24/7. ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ WhatsApp β”‚ ───▢ β”‚ CLAWDIS β”‚ ───▢ β”‚ AI Agent β”‚ -β”‚ (You) β”‚ ◀─── β”‚ πŸ¦žβ±οΈπŸ’™ β”‚ ◀─── β”‚ (Pi/Tau) β”‚ +β”‚ Telegram β”‚ ───▢ β”‚ πŸ¦žβ±οΈπŸ’™ β”‚ ◀─── β”‚ (Pi/Tau) β”‚ +β”‚ (You) β”‚ ◀─── β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` @@ -32,6 +33,7 @@ Because every space lobster needs a time-and-space machine. The Doctor has a TAR ## Features - πŸ“± **WhatsApp Integration** β€” Personal WhatsApp Web (Baileys) +- ✈️ **Telegram (Bot API)** β€” DMs and groups via grammY - πŸ€– **AI Agent Gateway** β€” Pi/Tau only (Pi CLI in RPC mode) - πŸ’¬ **Session Management** β€” Per-sender conversation context - πŸ”” **Heartbeats** β€” Periodic check-ins for proactive AI diff --git a/docs/clawd.md b/docs/clawd.md index cfb23a6f3..7acf708f3 100644 --- a/docs/clawd.md +++ b/docs/clawd.md @@ -240,13 +240,12 @@ Include `MEDIA:/path/to/file.png` in Claude's output to attach images. clawdis h # Foreground (see all logs) clawdis relay --provider web --verbose -# Background in tmux (recommended) -clawdis relay:tmux - # With immediate heartbeat on startup -clawdis relay:heartbeat:tmux +clawdis relay:heartbeat ``` +For backgrounding, run the relay under your preferred supervisor (e.g., launchd/systemd) and point it at the same `clawdis relay --provider web --verbose` command. + ## Tips for a Great Personal Assistant 1. **Give it a home** - A dedicated folder (`~/clawd`) lets your AI build persistent memory diff --git a/docs/heartbeat.md b/docs/heartbeat.md index 86f854148..c0815b5bd 100644 --- a/docs/heartbeat.md +++ b/docs/heartbeat.md @@ -39,7 +39,6 @@ Goal: add a simple heartbeat poll for command-based auto-replies (Pi/Tau) that o - Expose CLI triggers: - `clawdis heartbeat` (web provider, defaults to first `allowFrom`; optional `--to` override) - `--session-id ` forces resuming a specific session for that heartbeat - - `clawdis relay:heartbeat` to run the relay loop with an immediate heartbeat (no tmux) - - `clawdis relay:heartbeat:tmux` to run the same in tmux (detached, attachable) - - Relay supports `--heartbeat-now` to fire once at startup (including the tmux helper). + - `clawdis relay:heartbeat` to run the relay loop with an immediate heartbeat + - Relay supports `--heartbeat-now` to fire once at startup. - When multiple sessions are active or `allowFrom` is only `"*"`, require `--to ` or `--all` for manual heartbeats to avoid ambiguous targets. diff --git a/docs/tmux.md b/docs/tmux.md deleted file mode 100644 index a6340b727..000000000 --- a/docs/tmux.md +++ /dev/null @@ -1,17 +0,0 @@ -# tmux helpers (relay backgrounding) - -## Why we ship tmux helpers -- Run the relay detached so your shell can close, while keeping an interactive pane you can reattach to. -- Provide a consistent start/attach workflow without adding a daemon mode or external process manager. -- Keep the relay code itself tmux-agnostic; tmux is only a launcher concern. - -## Commands -- `clawdis relay:tmux` β€” restarts the `clawdis-relay` session running `pnpm clawdis relay --verbose`, then attaches (skips attach when stdout isn’t a TTY). -- `clawdis relay:tmux:attach` β€” attach to the existing session without restarting it. -- `clawdis relay:heartbeat:tmux` β€” same as `relay:tmux` but adds `--heartbeat-now` so Pi is pinged immediately on startup. - -All helpers use the fixed session name `clawdis-relay`. - -## Logs -- The relay always writes to the configured file logger (defaults to `/tmp/clawdis/clawdis.log`); on start it prints the active log path and level. -- tmux is just for interactive viewing; you can also tail the log file or use another supervisor if you prefer. diff --git a/src/cli/program.test.ts b/src/cli/program.test.ts index 30b1f2732..27210c354 100644 --- a/src/cli/program.test.ts +++ b/src/cli/program.test.ts @@ -6,7 +6,6 @@ const loginWeb = vi.fn(); const monitorWebProvider = vi.fn(); const logWebSelfId = vi.fn(); const waitForever = vi.fn(); -const spawnRelayTmux = vi.fn().mockResolvedValue("clawdis-relay"); const monitorTelegramProvider = vi.fn(); const runtime = { @@ -31,7 +30,6 @@ vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({ waitForever }), logWebSelfId, })); -vi.mock("./relay_tmux.js", () => ({ spawnRelayTmux })); const { buildProgram } = await import("./program.js"); @@ -80,16 +78,6 @@ describe("cli program", () => { runtime.exit = originalExit; }); - it("runs relay heartbeat tmux helper", async () => { - const program = buildProgram(); - await program.parseAsync(["relay:heartbeat:tmux"], { from: "user" }); - const shouldAttach = Boolean(process.stdout.isTTY); - expect(spawnRelayTmux).toHaveBeenCalledWith( - "pnpm clawdis relay --verbose --heartbeat-now", - shouldAttach, - ); - }); - it("runs telegram relay when token set", async () => { const program = buildProgram(); const prev = process.env.TELEGRAM_BOT_TOKEN; diff --git a/src/cli/program.ts b/src/cli/program.ts index c4f5d235f..0ecb148d4 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -25,17 +25,16 @@ import { resolveReconnectPolicy, } from "../web/reconnect.js"; import { createDefaultDeps, logWebSelfId } from "./deps.js"; -import { spawnRelayTmux } from "./relay_tmux.js"; export function buildProgram() { const program = new Command(); const PROGRAM_VERSION = VERSION; const TAGLINE = - "Send, receive, and auto-reply on WhatsAppβ€”Baileys (web) only."; + "Send, receive, and auto-reply on WhatsApp (web) and Telegram (bot)."; program .name("clawdis") - .description("WhatsApp relay CLI (WhatsApp Web session only)") + .description("Messaging relay CLI for WhatsApp Web and Telegram Bot API") .version(PROGRAM_VERSION); const formatIntroLine = (version: string, rich = true) => { @@ -91,7 +90,11 @@ export function buildProgram() { ], [ 'clawdis agent --to +15555550123 --message "Run summary" --deliver', - "Talk directly to the agent using the same session handling; optionally send the reply.", + "Talk directly to the agent using the same session handling; optionally send the WhatsApp reply.", + ], + [ + 'clawdis send --provider telegram --to @mychat --message "Hi"', + "Send via your Telegram bot.", ], ] as const; @@ -176,7 +179,7 @@ Examples: program .command("agent") .description( - "Talk directly to the configured agent (no WhatsApp send, reuses sessions)", + "Talk directly to the configured agent (no chat send; optional WhatsApp delivery)", ) .requiredOption("-m, --message ", "Message body for the agent") .option( @@ -312,7 +315,7 @@ Examples: program .command("heartbeat") - .description("Trigger a heartbeat or manual send once (web only, no tmux)") + .description("Trigger a heartbeat or manual send once (web provider only)") .option("--to ", "Override target E.164; defaults to allowFrom[0]") .option( "--session-id ", @@ -396,7 +399,7 @@ Examples: program .command("relay") - .description("Auto-reply to inbound messages (web only)") + .description("Auto-reply to inbound WhatsApp messages (web provider)") .option( "--web-heartbeat ", "Heartbeat interval for web relay health logs (seconds)", @@ -528,9 +531,7 @@ Examples: program .command("relay:heartbeat") - .description( - "Run relay with an immediate heartbeat (no tmux); requires web provider", - ) + .description("Run relay with an immediate heartbeat; requires web provider") .option("--verbose", "Verbose logging", false) .action(async (opts) => { setVerbose(Boolean(opts.verbose)); @@ -737,88 +738,6 @@ Shows token usage per session when the agent reports it; set inbound.reply.agent ); }); - program - .command("relay:tmux") - .description( - "Run relay --verbose inside tmux (session clawdis-relay), restarting if already running, then attach", - ) - .action(async () => { - try { - const shouldAttach = Boolean(process.stdout.isTTY); - const session = await spawnRelayTmux( - "pnpm clawdis relay --verbose", - shouldAttach, - ); - defaultRuntime.log( - info( - shouldAttach - ? `tmux session started and attached: ${session} (pane running "pnpm clawdis relay --verbose")` - : `tmux session started: ${session} (pane running "pnpm clawdis relay --verbose"); attach manually with "tmux attach -t ${session}"`, - ), - ); - } catch (err) { - defaultRuntime.error( - danger(`Failed to start relay tmux session: ${String(err)}`), - ); - defaultRuntime.exit(1); - } - }); - - program - .command("relay:tmux:attach") - .description( - "Attach to the existing clawdis-relay tmux session (no restart)", - ) - .action(async () => { - try { - if (!process.stdout.isTTY) { - defaultRuntime.error( - danger( - "Cannot attach: stdout is not a TTY. Run this in a terminal or use 'tmux attach -t clawdis-relay' manually.", - ), - ); - defaultRuntime.exit(1); - return; - } - await spawnRelayTmux("pnpm clawdis relay --verbose", true, false); - defaultRuntime.log(info("Attached to clawdis-relay session.")); - } catch (err) { - defaultRuntime.error( - danger(`Failed to attach to clawdis-relay: ${String(err)}`), - ); - defaultRuntime.exit(1); - } - }); - - program - .command("relay:heartbeat:tmux") - .description( - "Run relay --verbose with an immediate heartbeat inside tmux (session clawdis-relay), then attach", - ) - .action(async () => { - try { - const shouldAttach = Boolean(process.stdout.isTTY); - const session = await spawnRelayTmux( - "pnpm clawdis relay --verbose --heartbeat-now", - shouldAttach, - ); - defaultRuntime.log( - info( - shouldAttach - ? `tmux session started and attached: ${session} (pane running "pnpm clawdis relay --verbose --heartbeat-now")` - : `tmux session started: ${session} (pane running "pnpm clawdis relay --verbose --heartbeat-now"); attach manually with "tmux attach -t ${session}"`, - ), - ); - } catch (err) { - defaultRuntime.error( - danger( - `Failed to start relay tmux session with heartbeat: ${String(err)}`, - ), - ); - defaultRuntime.exit(1); - } - }); - program .command("webchat") .description("Start or query the loopback-only web chat server") diff --git a/src/cli/relay_tmux.test.ts b/src/cli/relay_tmux.test.ts deleted file mode 100644 index 4a94da499..000000000 --- a/src/cli/relay_tmux.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { EventEmitter } from "node:events"; - -import { beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("node:child_process", () => { - const spawn = vi.fn((_cmd: string, _args: string[]) => { - const proc = new EventEmitter() as EventEmitter & { - kill: ReturnType; - }; - queueMicrotask(() => { - proc.emit("exit", 0); - }); - proc.kill = vi.fn(); - return proc; - }); - return { spawn }; -}); - -const { spawnRelayTmux } = await import("./relay_tmux.js"); -const { spawn } = await import("node:child_process"); - -describe("spawnRelayTmux", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("kills old session, starts new one, and attaches", async () => { - const session = await spawnRelayTmux("echo hi", true, true); - expect(session).toBe("clawdis-relay"); - const spawnMock = spawn as unknown as vi.Mock; - expect(spawnMock.mock.calls.length).toBe(3); - const calls = spawnMock.mock.calls as Array<[string, string[], unknown]>; - expect(calls[0][0]).toBe("tmux"); // kill-session - expect(calls[1][2]?.cmd ?? "").not.toBeUndefined(); // new session - expect(calls[2][1][0]).toBe("attach-session"); - }); - - it("can skip attach", async () => { - await spawnRelayTmux("echo hi", false, true); - const spawnMock = spawn as unknown as vi.Mock; - const hasAttach = spawnMock.mock.calls.some( - (c) => - Array.isArray(c[1]) && (c[1] as string[]).includes("attach-session"), - ); - expect(hasAttach).toBe(false); - }); -}); diff --git a/src/cli/relay_tmux.ts b/src/cli/relay_tmux.ts deleted file mode 100644 index 73bbde624..000000000 --- a/src/cli/relay_tmux.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { spawn } from "node:child_process"; - -const SESSION = "clawdis-relay"; - -export async function spawnRelayTmux( - cmd = "pnpm clawdis relay --verbose", - attach = true, - restart = true, -) { - if (restart) { - await killSession(SESSION); - await new Promise((resolve, reject) => { - const child = spawn("tmux", ["new", "-d", "-s", SESSION, cmd], { - stdio: "inherit", - shell: false, - }); - child.on("error", reject); - child.on("exit", (code) => { - if (code === 0) resolve(); - else reject(new Error(`tmux exited with code ${code}`)); - }); - }); - } - - if (attach) { - await new Promise((resolve, reject) => { - const child = spawn("tmux", ["attach-session", "-t", SESSION], { - stdio: "inherit", - shell: false, - }); - child.on("error", reject); - child.on("exit", (code) => { - if (code === 0) resolve(); - else reject(new Error(`tmux attach exited with code ${code}`)); - }); - }); - } - - return SESSION; -} - -async function killSession(name: string) { - await new Promise((resolve) => { - const child = spawn("tmux", ["kill-session", "-t", name], { - stdio: "ignore", - }); - child.on("exit", () => resolve()); - child.on("error", () => resolve()); - }); -}