Switch to clawdis RPC mode and complete rebrand

This commit is contained in:
Peter Steinberger
2025-12-05 17:22:53 +00:00
parent 20cb709ae3
commit b3e50cbb33
26 changed files with 151 additions and 103 deletions

View File

@@ -7,7 +7,7 @@
## Build, Test, and Development Commands ## Build, Test, and Development Commands
- Install deps: `pnpm install` - Install deps: `pnpm install`
- Run CLI in dev: `pnpm warelay ...` (tsx entry) or `pnpm dev` for `src/index.ts`. - Run CLI in dev: `pnpm clawdis ...` (tsx entry) or `pnpm dev` for `src/index.ts`.
- Type-check/build: `pnpm build` (tsc) - Type-check/build: `pnpm build` (tsc)
- Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format) - Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format)
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage` - Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
@@ -30,26 +30,26 @@
## Security & Configuration Tips ## Security & Configuration Tips
- Environment: copy `.env.example`; set Twilio creds and WhatsApp sender (`TWILIO_WHATSAPP_FROM`). - Environment: copy `.env.example`; set Twilio creds and WhatsApp sender (`TWILIO_WHATSAPP_FROM`).
- Web provider stores creds at `~/.clawdis/credentials/` (legacy fallback: `~/.warelay/credentials/`); rerun `warelay login` if logged out. - Web provider stores creds at `~/.clawdis/credentials/` (legacy fallback: `~/.warelay/credentials/`); rerun `clawdis login` if logged out.
- Media hosting relies on Tailscale Funnel when using Twilio; use `warelay webhook --ingress tailscale` or `--serve-media` for local hosting. - Media hosting relies on Tailscale Funnel when using Twilio; use `clawdis webhook --ingress tailscale` or `--serve-media` for local hosting.
## Agent-Specific Notes ## Agent-Specific Notes
- Relay is managed by launchctl (new label `com.steipete.clawdis`). After code changes restart with `launchctl kickstart -k gui/$UID/com.steipete.clawdis` and verify via `launchctl list | grep clawdis`. Legacy label `com.steipete.warelay` still exists for rollback; prefer the new one. Use tmux only if you spin up a temporary relay yourself and clean it up afterward. - Relay is managed by launchctl (label `com.steipete.clawdis`). After code changes restart with `launchctl kickstart -k gui/$UID/com.steipete.clawdis` and verify via `launchctl list | grep clawdis`. Legacy label `com.steipete.warelay` still exists for rollback; prefer the new one. Use tmux only if you spin up a temporary relay yourself and clean it up afterward.
- Also read the shared guardrails at `~/Projects/oracle/AGENTS.md` and `~/Projects/agent-scripts/AGENTS.MD` before making changes; align with any cross-repo rules noted there. - Also read the shared guardrails at `~/Projects/oracle/AGENTS.md` and `~/Projects/agent-scripts/AGENTS.MD` before making changes; align with any cross-repo rules noted there.
- When asked to open a “session” file, open the Pi/Tau session logs under `~/.pi/agent/sessions/warelay/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`. - When asked to open a “session” file, open the Pi/Tau session logs under `~/.tau/agent/sessions/clawdis/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`.
## Exclamation Mark Escaping Workaround ## Exclamation Mark Escaping Workaround
The Claude Code Bash tool escapes `!` to `\!` in command arguments. When using `warelay send` with messages containing exclamation marks, use heredoc syntax: The Claude Code Bash tool escapes `!` to `\\!` in command arguments. When using `clawdis send` with messages containing exclamation marks, use heredoc syntax:
```bash ```bash
# WRONG - will send "Hello\!" with backslash # WRONG - will send "Hello\\!" with backslash
warelay send --provider web --to "+1234" --message 'Hello!' clawdis send --provider web --to "+1234" --message 'Hello!'
# CORRECT - use heredoc to avoid escaping # CORRECT - use heredoc to avoid escaping
warelay send --provider web --to "+1234" --message "$(cat <<'EOF' clawdis send --provider web --to "+1234" --message "$(cat <<'EOF'
Hello! Hello!
EOF EOF
)" )"
``` ```
This is a Claude Code quirk, not a warelay bug. This is a Claude Code quirk, not a clawdis bug.

View File

@@ -5,6 +5,7 @@
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {
"clawdis": "bin/warelay.js",
"warelay": "bin/warelay.js", "warelay": "bin/warelay.js",
"warely": "bin/warelay.js", "warely": "bin/warelay.js",
"wa": "bin/warelay.js" "wa": "bin/warelay.js"
@@ -16,6 +17,8 @@
"warelay": "tsx src/index.ts", "warelay": "tsx src/index.ts",
"warely": "tsx src/index.ts", "warely": "tsx src/index.ts",
"wa": "tsx src/index.ts", "wa": "tsx src/index.ts",
"clawdis": "tsx src/index.ts",
"clawdis:rpc": "tsx src/index.ts agent --mode rpc --json",
"lint": "biome check src", "lint": "biome check src",
"lint:fix": "biome check --write --unsafe src && biome format --write src", "lint:fix": "biome check --write --unsafe src && biome format --write src",
"format": "biome format src", "format": "biome format src",

View File

@@ -4,7 +4,7 @@ import type { AgentParseResult, AgentSpec } from "./types.js";
const GEMINI_BIN = "gemini"; const GEMINI_BIN = "gemini";
export const GEMINI_IDENTITY_PREFIX = export const GEMINI_IDENTITY_PREFIX =
"You are Gemini responding for warelay. Keep WhatsApp replies concise (<1500 chars). If the prompt contains media paths or a Transcript block, use them. If this was a heartbeat probe and nothing needs attention, reply with exactly HEARTBEAT_OK."; "You are Gemini responding for clawdis. Keep WhatsApp replies concise (<1500 chars). If the prompt contains media paths or a Transcript block, use them. If this was a heartbeat probe and nothing needs attention, reply with exactly HEARTBEAT_OK.";
// Gemini CLI currently prints plain text; --output json is flaky across versions, so we // Gemini CLI currently prints plain text; --output json is flaky across versions, so we
// keep parsing minimal and let MEDIA token stripping happen later in the pipeline. // keep parsing minimal and let MEDIA token stripping happen later in the pipeline.

View File

@@ -4,7 +4,7 @@ import { z } from "zod";
// Preferred binary name for Claude CLI invocations. // Preferred binary name for Claude CLI invocations.
export const CLAUDE_BIN = "claude"; export const CLAUDE_BIN = "claude";
export const CLAUDE_IDENTITY_PREFIX = export const CLAUDE_IDENTITY_PREFIX =
"You are Clawd (Claude) running on the user's Mac via warelay. Keep WhatsApp replies under ~1500 characters. Your scratchpad is ~/clawd; this is your folder and you can add what you like in markdown files and/or images. You can send media by including MEDIA:/path/to/file.jpg on its own line (no spaces in path). Media limits: images ≤6MB, audio/video ≤16MB, documents ≤100MB. The prompt may include a media path and an optional Transcript: section—use them when present. If a prompt is a heartbeat poll and nothing needs attention, reply with exactly HEARTBEAT_OK and nothing else; for any alert, do not include HEARTBEAT_OK."; "You are Clawd (Claude) running on the user's Mac via clawdis. Keep WhatsApp replies under ~1500 characters. Your scratchpad is ~/clawd; this is your folder and you can add what you like in markdown files and/or images. You can send media by including MEDIA:/path/to/file.jpg on its own line (no spaces in path). Media limits: images ≤6MB, audio/video ≤16MB, documents ≤100MB. The prompt may include a media path and an optional Transcript: section—use them when present. If a prompt is a heartbeat poll and nothing needs attention, reply with exactly HEARTBEAT_OK and nothing else; for any alert, do not include HEARTBEAT_OK.";
function extractClaudeText(payload: unknown): string | undefined { function extractClaudeText(payload: unknown): string | undefined {
// Best-effort walker to find the primary text field in Claude JSON outputs. // Best-effort walker to find the primary text field in Claude JSON outputs.

View File

@@ -384,7 +384,7 @@ export async function runCommandReply(
} }
const shouldApplyAgent = agent.isInvocation(argv); const shouldApplyAgent = agent.isInvocation(argv);
const finalArgv = shouldApplyAgent let finalArgv = shouldApplyAgent
? agent.buildArgs({ ? agent.buildArgs({
argv, argv,
bodyIndex, bodyIndex,
@@ -397,6 +397,22 @@ export async function runCommandReply(
}) })
: argv; : argv;
// For pi/tau: prefer RPC mode so auto-compaction and streaming events run server-side.
let rpcInput: string | undefined;
if (agentKind === "pi") {
const bodyArg = finalArgv[bodyIndex] ?? templatingCtx.Body ?? "";
rpcInput = JSON.stringify({ type: "prompt", message: bodyArg }) + "\n";
// Remove body argument (RPC expects stdin JSON instead of positional message)
finalArgv = finalArgv.filter((_, idx) => idx !== bodyIndex);
// Force --mode rpc
const modeIdx = finalArgv.findIndex((v) => v === "--mode");
if (modeIdx >= 0 && finalArgv[modeIdx + 1]) {
finalArgv[modeIdx + 1] = "rpc";
} else {
finalArgv.push("--mode", "rpc");
}
}
logVerbose( logVerbose(
`Running command auto-reply: ${finalArgv.join(" ")}${reply.cwd ? ` (cwd: ${reply.cwd})` : ""}`, `Running command auto-reply: ${finalArgv.join(" ")}${reply.cwd ? ` (cwd: ${reply.cwd})` : ""}`,
); );
@@ -582,7 +598,11 @@ export async function runCommandReply(
flushPendingTool(); flushPendingTool();
return rpcResult; return rpcResult;
} }
return await commandRunner(finalArgv, { timeoutMs, cwd: reply.cwd }); return await commandRunner(finalArgv, {
timeoutMs,
cwd: reply.cwd,
input: rpcInput,
});
}; };
const { stdout, stderr, code, signal, killed } = await enqueue(run, { const { stdout, stderr, code, signal, killed } = await enqueue(run, {
@@ -603,6 +623,23 @@ export async function runCommandReply(
logVerbose(`Command auto-reply stderr: ${stderr.trim()}`); logVerbose(`Command auto-reply stderr: ${stderr.trim()}`);
} }
const logFailure = () => {
const truncate = (s?: string) =>
s ? (s.length > 4000 ? `${s.slice(0, 4000)}` : s) : undefined;
logger.warn(
{
code,
signal,
killed,
argv: finalArgv,
cwd: reply.cwd,
stdout: truncate(rawStdout),
stderr: truncate(stderr),
},
"command auto-reply failed",
);
};
const parsed = trimmed ? agent.parseOutput(trimmed) : undefined; const parsed = trimmed ? agent.parseOutput(trimmed) : undefined;
const parserProvided = !!parsed; const parserProvided = !!parsed;
@@ -697,6 +734,7 @@ export async function runCommandReply(
text: `(command produced no output${meta ? `; ${meta}` : ""})`, text: `(command produced no output${meta ? `; ${meta}` : ""})`,
}); });
verboseLog("No text/media produced; injecting fallback notice to user"); verboseLog("No text/media produced; injecting fallback notice to user");
logFailure();
} }
verboseLog( verboseLog(
@@ -709,6 +747,7 @@ export async function runCommandReply(
"command auto-reply finished", "command auto-reply finished",
); );
if ((code ?? 0) !== 0) { if ((code ?? 0) !== 0) {
logFailure();
console.error( console.error(
`Command auto-reply exited with code ${code ?? "unknown"} (signal: ${signal ?? "none"})`, `Command auto-reply exited with code ${code ?? "unknown"} (signal: ${signal ?? "none"})`,
); );

View File

@@ -4,7 +4,7 @@
export const OPENCODE_BIN = "opencode"; export const OPENCODE_BIN = "opencode";
export const OPENCODE_IDENTITY_PREFIX = export const OPENCODE_IDENTITY_PREFIX =
"You are Openclawd running on the user's Mac via warelay. Your scratchpad is /Users/steipete/openclawd; this is your folder and you can add what you like in markdown files and/or images. You don't need to be concise, but WhatsApp replies must stay under ~1500 characters. Media you can send: images ≤6MB, audio/video ≤16MB, documents ≤100MB. The prompt may include a media path and an optional Transcript: section—use them when present. If a prompt is a heartbeat poll and nothing needs attention, reply with exactly HEARTBEAT_OK and nothing else; for any alert, do not include HEARTBEAT_OK."; "You are Openclawd running on the user's Mac via clawdis. Your scratchpad is /Users/steipete/openclawd; this is your folder and you can add what you like in markdown files and/or images. You don't need to be concise, but WhatsApp replies must stay under ~1500 characters. Media you can send: images ≤6MB, audio/video ≤16MB, documents ≤100MB. The prompt may include a media path and an optional Transcript: section—use them when present. If a prompt is a heartbeat poll and nothing needs attention, reply with exactly HEARTBEAT_OK and nothing else; for any alert, do not include HEARTBEAT_OK.";
export type OpencodeJsonParseResult = { export type OpencodeJsonParseResult = {
text?: string; text?: string;

View File

@@ -32,7 +32,7 @@ export async function transcribeInboundAudio(
const buffer = Buffer.from(arrayBuf); const buffer = Buffer.from(arrayBuf);
tmpPath = path.join( tmpPath = path.join(
os.tmpdir(), os.tmpdir(),
`warelay-audio-${crypto.randomUUID()}.ogg`, `clawdis-audio-${crypto.randomUUID()}.ogg`,
); );
await fs.writeFile(tmpPath, buffer); await fs.writeFile(tmpPath, buffer);
mediaPath = tmpPath; mediaPath = tmpPath;

View File

@@ -40,14 +40,14 @@ export function buildProgram() {
"Send, receive, and auto-reply on WhatsApp—Twilio-backed or QR-linked."; "Send, receive, and auto-reply on WhatsApp—Twilio-backed or QR-linked.";
program program
.name("warelay") .name("clawdis")
.description("WhatsApp relay CLI (Twilio or WhatsApp Web session)") .description("WhatsApp relay CLI (Twilio or WhatsApp Web session)")
.version(PROGRAM_VERSION); .version(PROGRAM_VERSION);
const formatIntroLine = (version: string, rich = true) => { const formatIntroLine = (version: string, rich = true) => {
const base = `📡 warelay ${version}${TAGLINE}`; const base = `📡 clawdis ${version}${TAGLINE}`;
return rich && chalk.level > 0 return rich && chalk.level > 0
? `${chalk.bold.cyan("📡 warelay")} ${chalk.white(version)} ${chalk.gray("—")} ${chalk.green(TAGLINE)}` ? `${chalk.bold.cyan("📡 clawdis")} ${chalk.white(version)} ${chalk.gray("—")} ${chalk.green(TAGLINE)}`
: base; : base;
}; };
@@ -76,27 +76,27 @@ export function buildProgram() {
program.addHelpText("beforeAll", `\n${formatIntroLine(PROGRAM_VERSION)}\n`); program.addHelpText("beforeAll", `\n${formatIntroLine(PROGRAM_VERSION)}\n`);
const examples = [ const examples = [
[ [
"warelay login --verbose", "clawdis login --verbose",
"Link personal WhatsApp Web and show QR + connection logs.", "Link personal WhatsApp Web and show QR + connection logs.",
], ],
[ [
'warelay send --to +15551234567 --message "Hi" --provider web --json', 'clawdis send --to +15551234567 --message "Hi" --provider web --json',
"Send via your web session and print JSON result.", "Send via your web session and print JSON result.",
], ],
[ [
"warelay relay --provider auto --interval 5 --lookback 15 --verbose", "clawdis relay --provider auto --interval 5 --lookback 15 --verbose",
"Auto-reply loop: prefer Web when logged in, otherwise Twilio polling.", "Auto-reply loop: prefer Web when logged in, otherwise Twilio polling.",
], ],
[ [
"warelay webhook --ingress tailscale --port 42873 --path /webhook/whatsapp --verbose", "clawdis webhook --ingress tailscale --port 42873 --path /webhook/whatsapp --verbose",
"Start webhook + Tailscale Funnel and update Twilio callbacks.", "Start webhook + Tailscale Funnel and update Twilio callbacks.",
], ],
[ [
"warelay status --limit 10 --lookback 60 --json", "clawdis status --limit 10 --lookback 60 --json",
"Show last 10 messages from the past hour as JSON.", "Show last 10 messages from the past hour as JSON.",
], ],
[ [
'warelay agent --to +15551234567 --message "Run summary" --thinking high', 'clawdis agent --to +15551234567 --message "Run summary" --thinking high',
"Talk directly to the agent using the same session handling, no WhatsApp send.", "Talk directly to the agent using the same session handling, no WhatsApp send.",
], ],
] as const; ] as const;
@@ -167,10 +167,10 @@ export function buildProgram() {
"after", "after",
` `
Examples: Examples:
warelay send --to +15551234567 --message "Hi" # wait 20s for delivery (default) clawdis send --to +15551234567 --message "Hi" # wait 20s for delivery (default)
warelay send --to +15551234567 --message "Hi" --wait 0 # fire-and-forget clawdis send --to +15551234567 --message "Hi" --wait 0 # fire-and-forget
warelay send --to +15551234567 --message "Hi" --dry-run # print payload only clawdis send --to +15551234567 --message "Hi" --dry-run # print payload only
warelay send --to +15551234567 --message "Hi" --wait 60 --poll 3`, clawdis send --to +15551234567 --message "Hi" --wait 60 --poll 3`,
) )
.action(async (opts) => { .action(async (opts) => {
setVerbose(Boolean(opts.verbose)); setVerbose(Boolean(opts.verbose));
@@ -218,10 +218,10 @@ Examples:
"after", "after",
` `
Examples: Examples:
warelay agent --to +15551234567 --message "status update" clawdis agent --to +15551234567 --message "status update"
warelay agent --session-id 1234 --message "Summarize inbox" --thinking medium clawdis agent --session-id 1234 --message "Summarize inbox" --thinking medium
warelay agent --to +15551234567 --message "Trace logs" --verbose on --json clawdis agent --to +15551234567 --message "Trace logs" --verbose on --json
warelay agent --to +15551234567 --message "Summon reply" --deliver --provider web clawdis agent --to +15551234567 --message "Summon reply" --deliver --provider web
`, `,
) )
.action(async (opts) => { .action(async (opts) => {
@@ -265,12 +265,12 @@ Examples:
"after", "after",
` `
Examples: Examples:
warelay heartbeat # uses web session + first allowFrom contact clawdis heartbeat # uses web session + first allowFrom contact
warelay heartbeat --verbose # prints detailed heartbeat logs clawdis heartbeat --verbose # prints detailed heartbeat logs
warelay heartbeat --to +1555123 # override destination clawdis heartbeat --to +1555123 # override destination
warelay heartbeat --session-id <uuid> --to +1555123 # resume a specific session clawdis heartbeat --session-id <uuid> --to +1555123 # resume a specific session
warelay heartbeat --message "Ping" --provider twilio clawdis heartbeat --message "Ping" --provider twilio
warelay heartbeat --all # send to every active session recipient or allowFrom entry`, clawdis heartbeat --all # send to every active session recipient or allowFrom entry`,
) )
.action(async (opts) => { .action(async (opts) => {
setVerbose(Boolean(opts.verbose)); setVerbose(Boolean(opts.verbose));
@@ -379,10 +379,10 @@ Examples:
"after", "after",
` `
Examples: Examples:
warelay relay # auto: web if logged-in, else twilio poll clawdis relay # auto: web if logged-in, else twilio poll
warelay relay --provider web # force personal web session clawdis relay --provider web # force personal web session
warelay relay --provider twilio # force twilio poll clawdis relay --provider twilio # force twilio poll
warelay relay --provider twilio --interval 2 --lookback 30 clawdis relay --provider twilio --interval 2 --lookback 30
# Troubleshooting: docs/refactor/web-relay-troubleshooting.md # Troubleshooting: docs/refactor/web-relay-troubleshooting.md
`, `,
) )
@@ -502,7 +502,7 @@ Examples:
} catch (err) { } catch (err) {
defaultRuntime.error( defaultRuntime.error(
danger( danger(
`Web relay failed: ${String(err)}. Not falling back; re-link with 'warelay login --provider web'.`, `Web relay failed: ${String(err)}. Not falling back; re-link with 'clawdis login --provider web'.`,
), ),
); );
defaultRuntime.exit(1); defaultRuntime.exit(1);
@@ -535,7 +535,7 @@ Examples:
if (provider !== "web") { if (provider !== "web") {
defaultRuntime.error( defaultRuntime.error(
danger( danger(
"Heartbeat relay is only supported for the web provider. Link with `warelay login --verbose`.", "Heartbeat relay is only supported for the web provider. Link with `clawdis login --verbose`.",
), ),
); );
defaultRuntime.exit(1); defaultRuntime.exit(1);
@@ -565,7 +565,7 @@ Examples:
} catch (err) { } catch (err) {
defaultRuntime.error( defaultRuntime.error(
danger( danger(
`Web relay failed: ${String(err)}. Re-link with 'warelay login --provider web'.`, `Web relay failed: ${String(err)}. Re-link with 'clawdis login --provider web'.`,
), ),
); );
defaultRuntime.exit(1); defaultRuntime.exit(1);
@@ -583,9 +583,9 @@ Examples:
"after", "after",
` `
Examples: Examples:
warelay status # last 20 msgs in past 4h clawdis status # last 20 msgs in past 4h
warelay status --limit 5 --lookback 30 # last 5 msgs in past 30m clawdis status --limit 5 --lookback 30 # last 5 msgs in past 30m
warelay status --json --limit 50 # machine-readable output`, clawdis status --json --limit 50 # machine-readable output`,
) )
.action(async (opts) => { .action(async (opts) => {
setVerbose(Boolean(opts.verbose)); setVerbose(Boolean(opts.verbose));
@@ -618,10 +618,10 @@ Examples:
"after", "after",
` `
Examples: Examples:
warelay webhook # ingress=tailscale (funnel + Twilio update) clawdis webhook # ingress=tailscale (funnel + Twilio update)
warelay webhook --ingress none # local-only server (no funnel / no Twilio update) clawdis webhook --ingress none # local-only server (no funnel / no Twilio update)
warelay webhook --port 45000 # pick a high, less-colliding port clawdis webhook --port 45000 # pick a high, less-colliding port
warelay webhook --reply "Got it!" # static auto-reply; otherwise use config file`, clawdis webhook --reply "Got it!" # static auto-reply; otherwise use config file`,
) )
// istanbul ignore next // istanbul ignore next
.action(async (opts) => { .action(async (opts) => {
@@ -652,20 +652,20 @@ Examples:
program program
.command("relay:tmux") .command("relay:tmux")
.description( .description(
"Run relay --verbose inside tmux (session warelay-relay), restarting if already running, then attach", "Run relay --verbose inside tmux (session clawdis-relay), restarting if already running, then attach",
) )
.action(async () => { .action(async () => {
try { try {
const shouldAttach = Boolean(process.stdout.isTTY); const shouldAttach = Boolean(process.stdout.isTTY);
const session = await spawnRelayTmux( const session = await spawnRelayTmux(
"pnpm warelay relay --verbose", "pnpm clawdis relay --verbose",
shouldAttach, shouldAttach,
); );
defaultRuntime.log( defaultRuntime.log(
info( info(
shouldAttach shouldAttach
? `tmux session started and attached: ${session} (pane running "pnpm warelay relay --verbose")` ? `tmux session started and attached: ${session} (pane running "pnpm clawdis relay --verbose")`
: `tmux session started: ${session} (pane running "pnpm warelay relay --verbose"); attach manually with "tmux attach -t ${session}"`, : `tmux session started: ${session} (pane running "pnpm clawdis relay --verbose"); attach manually with "tmux attach -t ${session}"`,
), ),
); );
} catch (err) { } catch (err) {
@@ -679,24 +679,24 @@ Examples:
program program
.command("relay:tmux:attach") .command("relay:tmux:attach")
.description( .description(
"Attach to the existing warelay-relay tmux session (no restart)", "Attach to the existing clawdis-relay tmux session (no restart)",
) )
.action(async () => { .action(async () => {
try { try {
if (!process.stdout.isTTY) { if (!process.stdout.isTTY) {
defaultRuntime.error( defaultRuntime.error(
danger( danger(
"Cannot attach: stdout is not a TTY. Run this in a terminal or use 'tmux attach -t warelay-relay' manually.", "Cannot attach: stdout is not a TTY. Run this in a terminal or use 'tmux attach -t clawdis-relay' manually.",
), ),
); );
defaultRuntime.exit(1); defaultRuntime.exit(1);
return; return;
} }
await spawnRelayTmux("pnpm warelay relay --verbose", true, false); await spawnRelayTmux("pnpm clawdis relay --verbose", true, false);
defaultRuntime.log(info("Attached to warelay-relay session.")); defaultRuntime.log(info("Attached to clawdis-relay session."));
} catch (err) { } catch (err) {
defaultRuntime.error( defaultRuntime.error(
danger(`Failed to attach to warelay-relay: ${String(err)}`), danger(`Failed to attach to clawdis-relay: ${String(err)}`),
); );
defaultRuntime.exit(1); defaultRuntime.exit(1);
} }
@@ -705,20 +705,20 @@ Examples:
program program
.command("relay:heartbeat:tmux") .command("relay:heartbeat:tmux")
.description( .description(
"Run relay --verbose with an immediate heartbeat inside tmux (session warelay-relay), then attach", "Run relay --verbose with an immediate heartbeat inside tmux (session clawdis-relay), then attach",
) )
.action(async () => { .action(async () => {
try { try {
const shouldAttach = Boolean(process.stdout.isTTY); const shouldAttach = Boolean(process.stdout.isTTY);
const session = await spawnRelayTmux( const session = await spawnRelayTmux(
"pnpm warelay relay --verbose --heartbeat-now", "pnpm clawdis relay --verbose --heartbeat-now",
shouldAttach, shouldAttach,
); );
defaultRuntime.log( defaultRuntime.log(
info( info(
shouldAttach shouldAttach
? `tmux session started and attached: ${session} (pane running "pnpm warelay relay --verbose --heartbeat-now")` ? `tmux session started and attached: ${session} (pane running "pnpm clawdis relay --verbose --heartbeat-now")`
: `tmux session started: ${session} (pane running "pnpm warelay relay --verbose --heartbeat-now"); attach manually with "tmux attach -t ${session}"`, : `tmux session started: ${session} (pane running "pnpm clawdis relay --verbose --heartbeat-now"); attach manually with "tmux attach -t ${session}"`,
), ),
); );
} catch (err) { } catch (err) {

View File

@@ -1,9 +1,9 @@
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
const SESSION = "warelay-relay"; const SESSION = "clawdis-relay";
export async function spawnRelayTmux( export async function spawnRelayTmux(
cmd = "pnpm warelay relay --verbose", cmd = "pnpm clawdis relay --verbose",
attach = true, attach = true,
restart = true, restart = true,
) { ) {

View File

@@ -57,7 +57,7 @@ function assertCommandConfig(cfg: WarelayConfig) {
const reply = cfg.inbound?.reply; const reply = cfg.inbound?.reply;
if (!reply || reply.mode !== "command" || !reply.command?.length) { if (!reply || reply.mode !== "command" || !reply.command?.length) {
throw new Error( throw new Error(
"Configure inbound.reply.mode=command with reply.command before using `warelay agent`.", "Configure inbound.reply.mode=command with reply.command before using `clawdis agent`.",
); );
} }
return reply as NonNullable< return reply as NonNullable<

View File

@@ -53,7 +53,7 @@ export type WarelayConfig = {
logging?: LoggingConfig; logging?: LoggingConfig;
inbound?: { inbound?: {
allowFrom?: string[]; // E.164 numbers allowed to trigger auto-reply (without whatsapp:) allowFrom?: string[]; // E.164 numbers allowed to trigger auto-reply (without whatsapp:)
messagePrefix?: string; // Prefix added to all inbound messages (default: "[warelay]" if no allowFrom, else "") messagePrefix?: string; // Prefix added to all inbound messages (default: "[clawdis]" if no allowFrom, else "")
responsePrefix?: string; // Prefix auto-added to all outbound replies (e.g., "🦞") responsePrefix?: string; // Prefix auto-added to all outbound replies (e.g., "🦞")
timestampPrefix?: boolean | string; // true/false or IANA timezone string (default: true with UTC) timestampPrefix?: boolean | string; // true/false or IANA timezone string (default: true with UTC)
transcribeAudio?: { transcribeAudio?: {
@@ -118,7 +118,7 @@ const ReplySchema = z
.object({ .object({
mode: z.union([z.literal("text"), z.literal("command")]), mode: z.union([z.literal("text"), z.literal("command")]),
text: z.string().optional(), text: z.string().optional(),
command: z.array(z.string()).optional(), command: z.array(z.string()).optional(),
heartbeatCommand: z.array(z.string()).optional(), heartbeatCommand: z.array(z.string()).optional(),
thinkingDefault: z thinkingDefault: z
.union([ .union([
@@ -147,8 +147,8 @@ const ReplySchema = z
heartbeatIdleMinutes: z.number().int().positive().optional(), heartbeatIdleMinutes: z.number().int().positive().optional(),
store: z.string().optional(), store: z.string().optional(),
sessionArgNew: z.array(z.string()).optional(), sessionArgNew: z.array(z.string()).optional(),
sessionArgResume: z.array(z.string()).optional(), sessionArgResume: z.array(z.string()).optional(),
sessionArgBeforeBody: z.boolean().optional(), sessionArgBeforeBody: z.boolean().optional(),
sendSystemOnce: z.boolean().optional(), sendSystemOnce: z.boolean().optional(),
sessionIntro: z.string().optional(), sessionIntro: z.string().optional(),
typingIntervalSeconds: z.number().int().positive().optional(), typingIntervalSeconds: z.number().int().positive().optional(),

View File

@@ -125,7 +125,7 @@ if (isMain) {
// These log the error and exit gracefully instead of crashing without trace. // These log the error and exit gracefully instead of crashing without trace.
process.on("unhandledRejection", (reason, _promise) => { process.on("unhandledRejection", (reason, _promise) => {
console.error( console.error(
"[warelay] Unhandled promise rejection:", "[clawdis] Unhandled promise rejection:",
reason instanceof Error ? (reason.stack ?? reason.message) : reason, reason instanceof Error ? (reason.stack ?? reason.message) : reason,
); );
process.exit(1); process.exit(1);
@@ -133,7 +133,7 @@ if (isMain) {
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
console.error( console.error(
"[warelay] Uncaught exception:", "[clawdis] Uncaught exception:",
error.stack ?? error.message, error.stack ?? error.message,
); );
process.exit(1); process.exit(1);

View File

@@ -79,10 +79,10 @@ export async function handlePortError(
if (details) { if (details) {
runtime.error(info("Port listener details:")); runtime.error(info("Port listener details:"));
runtime.error(details); runtime.error(details);
if (/warelay|src\/index\.ts|dist\/index\.js/.test(details)) { if (/clawdis|src\/index\.ts|dist\/index\.js/.test(details)) {
runtime.error( runtime.error(
warn( warn(
"It looks like another warelay instance is already running. Stop it or pick a different port.", "It looks like another clawdis instance is already running. Stop it or pick a different port.",
), ),
); );
} }

View File

@@ -150,7 +150,7 @@ export async function ensureFunnel(
); );
runtime.error( runtime.error(
info( info(
"Tip: you can fall back to polling (no webhooks needed): `pnpm warelay relay --provider twilio --interval 5 --lookback 10`", "Tip: you can fall back to polling (no webhooks needed): `pnpm clawdis relay --provider twilio --interval 5 --lookback 10`",
), ),
); );
if (isVerbose()) { if (isVerbose()) {

View File

@@ -72,10 +72,10 @@ describe("logger helpers", () => {
resetLogger(); resetLogger();
setLoggerOverride({}); // force defaults regardless of user config setLoggerOverride({}); // force defaults regardless of user config
const today = new Date().toISOString().slice(0, 10); const today = new Date().toISOString().slice(0, 10);
const todayPath = path.join(DEFAULT_LOG_DIR, `warelay-${today}.log`); const todayPath = path.join(DEFAULT_LOG_DIR, `clawdis-${today}.log`);
// create an old file to be pruned // create an old file to be pruned
const oldPath = path.join(DEFAULT_LOG_DIR, "warelay-2000-01-01.log"); const oldPath = path.join(DEFAULT_LOG_DIR, "clawdis-2000-01-01.log");
fs.mkdirSync(DEFAULT_LOG_DIR, { recursive: true }); fs.mkdirSync(DEFAULT_LOG_DIR, { recursive: true });
fs.writeFileSync(oldPath, "old"); fs.writeFileSync(oldPath, "old");
fs.utimesSync(oldPath, new Date(0), new Date(0)); fs.utimesSync(oldPath, new Date(0), new Date(0));
@@ -92,7 +92,7 @@ describe("logger helpers", () => {
}); });
function pathForTest() { function pathForTest() {
return path.join(os.tmpdir(), `warelay-log-${crypto.randomUUID()}.log`); return path.join(os.tmpdir(), `clawdis-log-${crypto.randomUUID()}.log`);
} }
function cleanup(file: string) { function cleanup(file: string) {

View File

@@ -7,10 +7,10 @@ import pino, { type Bindings, type LevelWithSilent, type Logger } from "pino";
import { loadConfig, type WarelayConfig } from "./config/config.js"; import { loadConfig, type WarelayConfig } from "./config/config.js";
import { isVerbose } from "./globals.js"; import { isVerbose } from "./globals.js";
export const DEFAULT_LOG_DIR = path.join(os.tmpdir(), "warelay"); export const DEFAULT_LOG_DIR = path.join(os.tmpdir(), "clawdis");
export const DEFAULT_LOG_FILE = path.join(DEFAULT_LOG_DIR, "warelay.log"); // legacy single-file path export const DEFAULT_LOG_FILE = path.join(DEFAULT_LOG_DIR, "clawdis.log"); // legacy single-file path
const LOG_PREFIX = "warelay"; const LOG_PREFIX = "clawdis";
const LOG_SUFFIX = ".log"; const LOG_SUFFIX = ".log";
const MAX_LOG_AGE_MS = 24 * 60 * 60 * 1000; // 24h const MAX_LOG_AGE_MS = 24 * 60 * 60 * 1000; // 24h

View File

@@ -36,7 +36,7 @@ export async function ensureMediaHosted(
if (needsServerStart && !opts.startServer) { if (needsServerStart && !opts.startServer) {
await fs.rm(saved.path).catch(() => {}); await fs.rm(saved.path).catch(() => {});
throw new Error( throw new Error(
"Media hosting requires the webhook/Funnel server. Start `warelay webhook`/`warelay up` or re-run with --serve-media.", "Media hosting requires the webhook/Funnel server. Start `clawdis webhook`/`clawdis up` or re-run with --serve-media.",
); );
} }
if (needsServerStart && opts.startServer) { if (needsServerStart && opts.startServer) {

View File

@@ -46,6 +46,7 @@ export type SpawnResult = {
export type CommandOptions = { export type CommandOptions = {
timeoutMs: number; timeoutMs: number;
cwd?: string; cwd?: string;
input?: string;
}; };
export async function runCommandWithTimeout( export async function runCommandWithTimeout(
@@ -56,12 +57,12 @@ export async function runCommandWithTimeout(
typeof optionsOrTimeout === "number" typeof optionsOrTimeout === "number"
? { timeoutMs: optionsOrTimeout } ? { timeoutMs: optionsOrTimeout }
: optionsOrTimeout; : optionsOrTimeout;
const { timeoutMs, cwd } = options; const { timeoutMs, cwd, input } = options;
// Spawn with inherited stdin (TTY) so tools like `claude` don't hang. // Spawn with inherited stdin (TTY) so tools like `claude` don't hang.
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const child = spawn(argv[0], argv.slice(1), { const child = spawn(argv[0], argv.slice(1), {
stdio: ["inherit", "pipe", "pipe"], stdio: [input ? "pipe" : "inherit", "pipe", "pipe"],
cwd, cwd,
}); });
let stdout = ""; let stdout = "";
@@ -71,6 +72,11 @@ export async function runCommandWithTimeout(
child.kill("SIGKILL"); child.kill("SIGKILL");
}, timeoutMs); }, timeoutMs);
if (input && child.stdin) {
child.stdin.write(input);
child.stdin.end();
}
child.stdout?.on("data", (d) => { child.stdout?.on("data", (d) => {
stdout += d.toString(); stdout += d.toString();
}); });

View File

@@ -120,7 +120,7 @@ export async function startWebhook(
app.use((_req, res) => { app.use((_req, res) => {
if (verbose) runtime.log(chalk.yellow(`404 ${_req.method} ${_req.url}`)); if (verbose) runtime.log(chalk.yellow(`404 ${_req.method} ${_req.url}`));
res.status(404).send("warelay webhook: not found"); res.status(404).send("clawdis webhook: not found");
}); });
// Start server and resolve once listening; reject on bind error. // Start server and resolve once listening; reject on bind error.

View File

@@ -3,5 +3,5 @@ import { createRequire } from "node:module";
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const pkg = require("../package.json") as { version?: string }; const pkg = require("../package.json") as { version?: string };
// Single source of truth for the current warelay version (reads from package.json). // Single source of truth for the current clawdis version (reads from package.json).
export const VERSION = pkg.version ?? "0.0.0"; export const VERSION = pkg.version ?? "0.0.0";

View File

@@ -631,8 +631,8 @@ describe("web auto-reply", () => {
expect(resolver).toHaveBeenCalledTimes(1); expect(resolver).toHaveBeenCalledTimes(1);
const args = resolver.mock.calls[0][0]; const args = resolver.mock.calls[0][0];
expect(args.Body).toContain("[Jan 1 00:00] [warelay] first"); expect(args.Body).toContain("[Jan 1 00:00] [clawdis] first");
expect(args.Body).toContain("[Jan 1 01:00] [warelay] second"); expect(args.Body).toContain("[Jan 1 01:00] [clawdis] second");
// Max listeners bumped to avoid warnings in multi-instance test runs // Max listeners bumped to avoid warnings in multi-instance test runs
expect(process.getMaxListeners?.()).toBeGreaterThanOrEqual(50); expect(process.getMaxListeners?.()).toBeGreaterThanOrEqual(50);

View File

@@ -690,7 +690,7 @@ export async function monitorWebProvider(
let messagePrefix = cfg.inbound?.messagePrefix; let messagePrefix = cfg.inbound?.messagePrefix;
if (messagePrefix === undefined) { if (messagePrefix === undefined) {
const hasAllowFrom = (cfg.inbound?.allowFrom?.length ?? 0) > 0; const hasAllowFrom = (cfg.inbound?.allowFrom?.length ?? 0) > 0;
messagePrefix = hasAllowFrom ? "" : "[warelay]"; messagePrefix = hasAllowFrom ? "" : "[clawdis]";
} }
const prefixStr = messagePrefix ? `${messagePrefix} ` : ""; const prefixStr = messagePrefix ? `${messagePrefix} ` : "";
const senderLabel = const senderLabel =
@@ -930,7 +930,7 @@ export async function monitorWebProvider(
}, },
}); });
// Start IPC server so `warelay send` can use this connection // Start IPC server so `clawdis send` can use this connection
// instead of creating a new one (which would corrupt Signal session) // instead of creating a new one (which would corrupt Signal session)
if ("sendMessage" in listener && "sendComposingTo" in listener) { if ("sendMessage" in listener && "sendComposingTo" in listener) {
startIpcServer(async (to, message, mediaUrl) => { startIpcServer(async (to, message, mediaUrl) => {
@@ -1300,7 +1300,7 @@ export async function monitorWebProvider(
if (loggedOut) { if (loggedOut) {
runtime.error( runtime.error(
danger( danger(
"WhatsApp session logged out. Run `warelay login --provider web` to relink.", "WhatsApp session logged out. Run `clawdis login --provider web` to relink.",
), ),
); );
await closeListener(); await closeListener();

View File

@@ -1,8 +1,8 @@
/** /**
* IPC server for warelay relay. * IPC server for clawdis relay.
* *
* When the relay is running, it starts a Unix socket server that allows * When the relay is running, it starts a Unix socket server that allows
* `warelay send` and `warelay heartbeat` to send messages through the * `clawdis send` and `clawdis heartbeat` to send messages through the
* existing WhatsApp connection instead of creating new ones. * existing WhatsApp connection instead of creating new ones.
* *
* This prevents Signal session ratchet corruption from multiple connections. * This prevents Signal session ratchet corruption from multiple connections.

View File

@@ -55,7 +55,7 @@ export async function loginWeb(
await fs.rm(WA_WEB_AUTH_DIR, { recursive: true, force: true }); await fs.rm(WA_WEB_AUTH_DIR, { recursive: true, force: true });
console.error( console.error(
danger( danger(
"WhatsApp reported the session is logged out. Cleared cached web session; please rerun warelay login and scan the QR again.", "WhatsApp reported the session is logged out. Cleared cached web session; please rerun clawdis login and scan the QR again.",
), ),
); );
throw new Error("Session logged out; cache cleared. Re-run login."); throw new Error("Session logged out; cache cleared. Re-run login.");

View File

@@ -48,7 +48,7 @@ export async function createWaSocket(printQr: boolean, verbose: boolean) {
version, version,
logger, logger,
printQRInTerminal: false, printQRInTerminal: false,
browser: ["warelay", "cli", VERSION], browser: ["clawdis", "cli", VERSION],
syncFullHistory: false, syncFullHistory: false,
markOnlineOnConnect: false, markOnlineOnConnect: false,
}); });
@@ -69,7 +69,7 @@ export async function createWaSocket(printQr: boolean, verbose: boolean) {
const status = getStatusCode(lastDisconnect?.error); const status = getStatusCode(lastDisconnect?.error);
if (status === DisconnectReason.loggedOut) { if (status === DisconnectReason.loggedOut) {
console.error( console.error(
danger("WhatsApp session logged out. Run: warelay login"), danger("WhatsApp session logged out. Run: clawdis login"),
); );
} }
} }

View File

@@ -4,7 +4,7 @@ import type { MockBaileysSocket } from "../../test/mocks/baileys.js";
import { createMockBaileys } from "../../test/mocks/baileys.js"; import { createMockBaileys } from "../../test/mocks/baileys.js";
// Use globalThis to store the mock config so it survives vi.mock hoisting // Use globalThis to store the mock config so it survives vi.mock hoisting
const CONFIG_KEY = Symbol.for("warelay:testConfigMock"); const CONFIG_KEY = Symbol.for("clawdis:testConfigMock");
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
inbound: { inbound: {
allowFrom: ["*"], // Allow all in tests by default allowFrom: ["*"], // Allow all in tests by default
@@ -50,7 +50,7 @@ vi.mock("../media/store.js", () => ({
vi.mock("@whiskeysockets/baileys", () => { vi.mock("@whiskeysockets/baileys", () => {
const created = createMockBaileys(); const created = createMockBaileys();
(globalThis as Record<PropertyKey, unknown>)[ (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("warelay:lastSocket") Symbol.for("clawdis:lastSocket")
] = created.lastSocket; ] = created.lastSocket;
return created.mod; return created.mod;
}); });
@@ -72,7 +72,7 @@ export const baileys = (await import(
export function resetBaileysMocks() { export function resetBaileysMocks() {
const recreated = createMockBaileys(); const recreated = createMockBaileys();
(globalThis as Record<PropertyKey, unknown>)[ (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("warelay:lastSocket") Symbol.for("clawdis:lastSocket")
] = recreated.lastSocket; ] = recreated.lastSocket;
baileys.makeWASocket.mockImplementation(recreated.mod.makeWASocket); baileys.makeWASocket.mockImplementation(recreated.mod.makeWASocket);
baileys.useMultiFileAuthState.mockImplementation( baileys.useMultiFileAuthState.mockImplementation(
@@ -88,7 +88,7 @@ export function resetBaileysMocks() {
export function getLastSocket(): MockBaileysSocket { export function getLastSocket(): MockBaileysSocket {
const getter = (globalThis as Record<PropertyKey, unknown>)[ const getter = (globalThis as Record<PropertyKey, unknown>)[
Symbol.for("warelay:lastSocket") Symbol.for("clawdis:lastSocket")
]; ];
if (typeof getter === "function") if (typeof getter === "function")
return (getter as () => MockBaileysSocket)(); return (getter as () => MockBaileysSocket)();