refactor(cli): drop tmux helpers and update help copy
This commit is contained in:
@@ -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:<jid>`.
|
||||
- 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:<level>`; 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 🦞 CLAWDIS — WhatsApp Gateway for AI Agents
|
||||
# 🦞 CLAWDIS — WhatsApp & Telegram Gateway for AI Agents
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/whatsapp-clawd.jpg" alt="CLAWDIS" width="400">
|
||||
@@ -14,12 +14,13 @@
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
|
||||
</p>
|
||||
|
||||
**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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <uuid>` 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 <E.164>` or `--all` for manual heartbeats to avoid ambiguous targets.
|
||||
|
||||
17
docs/tmux.md
17
docs/tmux.md
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <text>", "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 <number>", "Override target E.164; defaults to allowFrom[0]")
|
||||
.option(
|
||||
"--session-id <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 <seconds>",
|
||||
"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")
|
||||
|
||||
@@ -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<typeof vi.fn>;
|
||||
};
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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<void>((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<void>((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<void>((resolve) => {
|
||||
const child = spawn("tmux", ["kill-session", "-t", name], {
|
||||
stdio: "ignore",
|
||||
});
|
||||
child.on("exit", () => resolve());
|
||||
child.on("error", () => resolve());
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user