diff --git a/AGENTS.md b/AGENTS.md index 492d73102..34d2649e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ ## Build, Test, and Development Commands - 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) - Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format) - Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage` @@ -30,26 +30,26 @@ ## Security & Configuration Tips - 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. -- Media hosting relies on Tailscale Funnel when using Twilio; use `warelay webhook --ingress tailscale` or `--serve-media` for local hosting. +- 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 `clawdis webhook --ingress tailscale` or `--serve-media` for local hosting. ## 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. -- 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 -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 -# WRONG - will send "Hello\!" with backslash -warelay send --provider web --to "+1234" --message 'Hello!' +# WRONG - will send "Hello\\!" with backslash +clawdis send --provider web --to "+1234" --message 'Hello!' # 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! EOF )" ``` -This is a Claude Code quirk, not a warelay bug. +This is a Claude Code quirk, not a clawdis bug. diff --git a/package.json b/package.json index 2118ca5e4..edd38e2ab 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "main": "dist/index.js", "bin": { + "clawdis": "bin/warelay.js", "warelay": "bin/warelay.js", "warely": "bin/warelay.js", "wa": "bin/warelay.js" @@ -16,6 +17,8 @@ "warelay": "tsx src/index.ts", "warely": "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:fix": "biome check --write --unsafe src && biome format --write src", "format": "biome format src", diff --git a/src/agents/gemini.ts b/src/agents/gemini.ts index ce0d68165..a81e4d8a3 100644 --- a/src/agents/gemini.ts +++ b/src/agents/gemini.ts @@ -4,7 +4,7 @@ import type { AgentParseResult, AgentSpec } from "./types.js"; const GEMINI_BIN = "gemini"; 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 // keep parsing minimal and let MEDIA token stripping happen later in the pipeline. diff --git a/src/auto-reply/claude.ts b/src/auto-reply/claude.ts index bd7841dad..016a1df62 100644 --- a/src/auto-reply/claude.ts +++ b/src/auto-reply/claude.ts @@ -4,7 +4,7 @@ import { z } from "zod"; // Preferred binary name for Claude CLI invocations. export const CLAUDE_BIN = "claude"; 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 { // Best-effort walker to find the primary text field in Claude JSON outputs. diff --git a/src/auto-reply/command-reply.ts b/src/auto-reply/command-reply.ts index 6eecb945a..706d4ee9c 100644 --- a/src/auto-reply/command-reply.ts +++ b/src/auto-reply/command-reply.ts @@ -384,7 +384,7 @@ export async function runCommandReply( } const shouldApplyAgent = agent.isInvocation(argv); - const finalArgv = shouldApplyAgent + let finalArgv = shouldApplyAgent ? agent.buildArgs({ argv, bodyIndex, @@ -397,6 +397,22 @@ export async function runCommandReply( }) : 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( `Running command auto-reply: ${finalArgv.join(" ")}${reply.cwd ? ` (cwd: ${reply.cwd})` : ""}`, ); @@ -582,7 +598,11 @@ export async function runCommandReply( flushPendingTool(); 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, { @@ -603,6 +623,23 @@ export async function runCommandReply( 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 parserProvided = !!parsed; @@ -697,6 +734,7 @@ export async function runCommandReply( text: `(command produced no output${meta ? `; ${meta}` : ""})`, }); verboseLog("No text/media produced; injecting fallback notice to user"); + logFailure(); } verboseLog( @@ -709,6 +747,7 @@ export async function runCommandReply( "command auto-reply finished", ); if ((code ?? 0) !== 0) { + logFailure(); console.error( `Command auto-reply exited with code ${code ?? "unknown"} (signal: ${signal ?? "none"})`, ); diff --git a/src/auto-reply/opencode.ts b/src/auto-reply/opencode.ts index 859b81d60..fe6f260ec 100644 --- a/src/auto-reply/opencode.ts +++ b/src/auto-reply/opencode.ts @@ -4,7 +4,7 @@ export const OPENCODE_BIN = "opencode"; 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 = { text?: string; diff --git a/src/auto-reply/transcription.ts b/src/auto-reply/transcription.ts index 6a93f1746..85232de27 100644 --- a/src/auto-reply/transcription.ts +++ b/src/auto-reply/transcription.ts @@ -32,7 +32,7 @@ export async function transcribeInboundAudio( const buffer = Buffer.from(arrayBuf); tmpPath = path.join( os.tmpdir(), - `warelay-audio-${crypto.randomUUID()}.ogg`, + `clawdis-audio-${crypto.randomUUID()}.ogg`, ); await fs.writeFile(tmpPath, buffer); mediaPath = tmpPath; diff --git a/src/cli/program.ts b/src/cli/program.ts index 111ec46ca..afd968797 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -40,14 +40,14 @@ export function buildProgram() { "Send, receive, and auto-reply on WhatsApp—Twilio-backed or QR-linked."; program - .name("warelay") + .name("clawdis") .description("WhatsApp relay CLI (Twilio or WhatsApp Web session)") .version(PROGRAM_VERSION); const formatIntroLine = (version: string, rich = true) => { - const base = `📡 warelay ${version} — ${TAGLINE}`; + const base = `📡 clawdis ${version} — ${TAGLINE}`; 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; }; @@ -76,27 +76,27 @@ export function buildProgram() { program.addHelpText("beforeAll", `\n${formatIntroLine(PROGRAM_VERSION)}\n`); const examples = [ [ - "warelay login --verbose", + "clawdis login --verbose", "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.", ], [ - "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.", ], [ - "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.", ], [ - "warelay status --limit 10 --lookback 60 --json", + "clawdis status --limit 10 --lookback 60 --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.", ], ] as const; @@ -167,10 +167,10 @@ export function buildProgram() { "after", ` Examples: - warelay send --to +15551234567 --message "Hi" # wait 20s for delivery (default) - warelay send --to +15551234567 --message "Hi" --wait 0 # fire-and-forget - warelay 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 20s for delivery (default) + clawdis send --to +15551234567 --message "Hi" --wait 0 # fire-and-forget + clawdis send --to +15551234567 --message "Hi" --dry-run # print payload only + clawdis send --to +15551234567 --message "Hi" --wait 60 --poll 3`, ) .action(async (opts) => { setVerbose(Boolean(opts.verbose)); @@ -218,10 +218,10 @@ Examples: "after", ` Examples: - warelay agent --to +15551234567 --message "status update" - warelay agent --session-id 1234 --message "Summarize inbox" --thinking medium - warelay agent --to +15551234567 --message "Trace logs" --verbose on --json - warelay agent --to +15551234567 --message "Summon reply" --deliver --provider web + clawdis agent --to +15551234567 --message "status update" + clawdis agent --session-id 1234 --message "Summarize inbox" --thinking medium + clawdis agent --to +15551234567 --message "Trace logs" --verbose on --json + clawdis agent --to +15551234567 --message "Summon reply" --deliver --provider web `, ) .action(async (opts) => { @@ -265,12 +265,12 @@ Examples: "after", ` Examples: - warelay heartbeat # uses web session + first allowFrom contact - warelay heartbeat --verbose # prints detailed heartbeat logs - warelay heartbeat --to +1555123 # override destination - warelay heartbeat --session-id --to +1555123 # resume a specific session - warelay heartbeat --message "Ping" --provider twilio - warelay heartbeat --all # send to every active session recipient or allowFrom entry`, + clawdis heartbeat # uses web session + first allowFrom contact + clawdis heartbeat --verbose # prints detailed heartbeat logs + clawdis heartbeat --to +1555123 # override destination + clawdis heartbeat --session-id --to +1555123 # resume a specific session + clawdis heartbeat --message "Ping" --provider twilio + clawdis heartbeat --all # send to every active session recipient or allowFrom entry`, ) .action(async (opts) => { setVerbose(Boolean(opts.verbose)); @@ -379,10 +379,10 @@ Examples: "after", ` Examples: - warelay relay # auto: web if logged-in, else twilio poll - warelay relay --provider web # force personal web session - warelay relay --provider twilio # force twilio poll - warelay relay --provider twilio --interval 2 --lookback 30 + clawdis relay # auto: web if logged-in, else twilio poll + clawdis relay --provider web # force personal web session + clawdis relay --provider twilio # force twilio poll + clawdis relay --provider twilio --interval 2 --lookback 30 # Troubleshooting: docs/refactor/web-relay-troubleshooting.md `, ) @@ -502,7 +502,7 @@ Examples: } catch (err) { defaultRuntime.error( 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); @@ -535,7 +535,7 @@ Examples: if (provider !== "web") { defaultRuntime.error( 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); @@ -565,7 +565,7 @@ Examples: } catch (err) { defaultRuntime.error( 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); @@ -583,9 +583,9 @@ Examples: "after", ` Examples: - warelay status # last 20 msgs in past 4h - warelay status --limit 5 --lookback 30 # last 5 msgs in past 30m - warelay status --json --limit 50 # machine-readable output`, + clawdis status # last 20 msgs in past 4h + clawdis status --limit 5 --lookback 30 # last 5 msgs in past 30m + clawdis status --json --limit 50 # machine-readable output`, ) .action(async (opts) => { setVerbose(Boolean(opts.verbose)); @@ -618,10 +618,10 @@ Examples: "after", ` Examples: - warelay webhook # ingress=tailscale (funnel + Twilio update) - warelay webhook --ingress none # local-only server (no funnel / no Twilio update) - warelay webhook --port 45000 # pick a high, less-colliding port - warelay webhook --reply "Got it!" # static auto-reply; otherwise use config file`, + clawdis webhook # ingress=tailscale (funnel + Twilio update) + clawdis webhook --ingress none # local-only server (no funnel / no Twilio update) + clawdis webhook --port 45000 # pick a high, less-colliding port + clawdis webhook --reply "Got it!" # static auto-reply; otherwise use config file`, ) // istanbul ignore next .action(async (opts) => { @@ -652,20 +652,20 @@ Examples: program .command("relay:tmux") .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 () => { try { const shouldAttach = Boolean(process.stdout.isTTY); const session = await spawnRelayTmux( - "pnpm warelay relay --verbose", + "pnpm clawdis relay --verbose", shouldAttach, ); defaultRuntime.log( info( shouldAttach - ? `tmux session started and attached: ${session} (pane running "pnpm warelay relay --verbose")` - : `tmux session started: ${session} (pane running "pnpm warelay relay --verbose"); attach manually with "tmux attach -t ${session}"`, + ? `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) { @@ -679,24 +679,24 @@ Examples: program .command("relay:tmux:attach") .description( - "Attach to the existing warelay-relay tmux session (no restart)", + "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 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); return; } - await spawnRelayTmux("pnpm warelay relay --verbose", true, false); - defaultRuntime.log(info("Attached to warelay-relay session.")); + 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 warelay-relay: ${String(err)}`), + danger(`Failed to attach to clawdis-relay: ${String(err)}`), ); defaultRuntime.exit(1); } @@ -705,20 +705,20 @@ Examples: program .command("relay:heartbeat:tmux") .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 () => { try { const shouldAttach = Boolean(process.stdout.isTTY); const session = await spawnRelayTmux( - "pnpm warelay relay --verbose --heartbeat-now", + "pnpm clawdis relay --verbose --heartbeat-now", shouldAttach, ); defaultRuntime.log( info( shouldAttach - ? `tmux session started and attached: ${session} (pane running "pnpm warelay 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 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) { diff --git a/src/cli/relay_tmux.ts b/src/cli/relay_tmux.ts index cf78cb1af..73bbde624 100644 --- a/src/cli/relay_tmux.ts +++ b/src/cli/relay_tmux.ts @@ -1,9 +1,9 @@ import { spawn } from "node:child_process"; -const SESSION = "warelay-relay"; +const SESSION = "clawdis-relay"; export async function spawnRelayTmux( - cmd = "pnpm warelay relay --verbose", + cmd = "pnpm clawdis relay --verbose", attach = true, restart = true, ) { diff --git a/src/commands/agent.ts b/src/commands/agent.ts index ba185687b..c4ef6ebab 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -57,7 +57,7 @@ function assertCommandConfig(cfg: WarelayConfig) { const reply = cfg.inbound?.reply; if (!reply || reply.mode !== "command" || !reply.command?.length) { 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< diff --git a/src/config/config.ts b/src/config/config.ts index d75f5f282..e9ef7440c 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -53,7 +53,7 @@ export type WarelayConfig = { logging?: LoggingConfig; inbound?: { 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., "🦞") timestampPrefix?: boolean | string; // true/false or IANA timezone string (default: true with UTC) transcribeAudio?: { @@ -118,7 +118,7 @@ const ReplySchema = z .object({ mode: z.union([z.literal("text"), z.literal("command")]), text: z.string().optional(), - command: z.array(z.string()).optional(), + command: z.array(z.string()).optional(), heartbeatCommand: z.array(z.string()).optional(), thinkingDefault: z .union([ @@ -147,8 +147,8 @@ const ReplySchema = z heartbeatIdleMinutes: z.number().int().positive().optional(), store: z.string().optional(), sessionArgNew: z.array(z.string()).optional(), - sessionArgResume: z.array(z.string()).optional(), - sessionArgBeforeBody: z.boolean().optional(), + sessionArgResume: z.array(z.string()).optional(), + sessionArgBeforeBody: z.boolean().optional(), sendSystemOnce: z.boolean().optional(), sessionIntro: z.string().optional(), typingIntervalSeconds: z.number().int().positive().optional(), diff --git a/src/index.ts b/src/index.ts index 00e2d0487..35143c4b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -125,7 +125,7 @@ if (isMain) { // These log the error and exit gracefully instead of crashing without trace. process.on("unhandledRejection", (reason, _promise) => { console.error( - "[warelay] Unhandled promise rejection:", + "[clawdis] Unhandled promise rejection:", reason instanceof Error ? (reason.stack ?? reason.message) : reason, ); process.exit(1); @@ -133,7 +133,7 @@ if (isMain) { process.on("uncaughtException", (error) => { console.error( - "[warelay] Uncaught exception:", + "[clawdis] Uncaught exception:", error.stack ?? error.message, ); process.exit(1); diff --git a/src/infra/ports.ts b/src/infra/ports.ts index 35fce8177..93a1d8b58 100644 --- a/src/infra/ports.ts +++ b/src/infra/ports.ts @@ -79,10 +79,10 @@ export async function handlePortError( if (details) { runtime.error(info("Port listener 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( 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.", ), ); } diff --git a/src/infra/tailscale.ts b/src/infra/tailscale.ts index ebf100e40..70b8de6d7 100644 --- a/src/infra/tailscale.ts +++ b/src/infra/tailscale.ts @@ -150,7 +150,7 @@ export async function ensureFunnel( ); runtime.error( 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()) { diff --git a/src/logger.test.ts b/src/logger.test.ts index 4fa8f38b5..f3850fb2f 100644 --- a/src/logger.test.ts +++ b/src/logger.test.ts @@ -72,10 +72,10 @@ describe("logger helpers", () => { resetLogger(); setLoggerOverride({}); // force defaults regardless of user config 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 - 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.writeFileSync(oldPath, "old"); fs.utimesSync(oldPath, new Date(0), new Date(0)); @@ -92,7 +92,7 @@ describe("logger helpers", () => { }); 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) { diff --git a/src/logging.ts b/src/logging.ts index d9158231b..00ab56695 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -7,10 +7,10 @@ import pino, { type Bindings, type LevelWithSilent, type Logger } from "pino"; import { loadConfig, type WarelayConfig } from "./config/config.js"; import { isVerbose } from "./globals.js"; -export const DEFAULT_LOG_DIR = path.join(os.tmpdir(), "warelay"); -export const DEFAULT_LOG_FILE = path.join(DEFAULT_LOG_DIR, "warelay.log"); // legacy single-file path +export const DEFAULT_LOG_DIR = path.join(os.tmpdir(), "clawdis"); +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 MAX_LOG_AGE_MS = 24 * 60 * 60 * 1000; // 24h diff --git a/src/media/host.ts b/src/media/host.ts index 83d1d8e74..e082859ad 100644 --- a/src/media/host.ts +++ b/src/media/host.ts @@ -36,7 +36,7 @@ export async function ensureMediaHosted( if (needsServerStart && !opts.startServer) { await fs.rm(saved.path).catch(() => {}); 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) { diff --git a/src/process/exec.ts b/src/process/exec.ts index 45a7744ae..673635879 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -46,6 +46,7 @@ export type SpawnResult = { export type CommandOptions = { timeoutMs: number; cwd?: string; + input?: string; }; export async function runCommandWithTimeout( @@ -56,12 +57,12 @@ export async function runCommandWithTimeout( typeof optionsOrTimeout === "number" ? { timeoutMs: optionsOrTimeout } : optionsOrTimeout; - const { timeoutMs, cwd } = options; + const { timeoutMs, cwd, input } = options; // Spawn with inherited stdin (TTY) so tools like `claude` don't hang. return await new Promise((resolve, reject) => { const child = spawn(argv[0], argv.slice(1), { - stdio: ["inherit", "pipe", "pipe"], + stdio: [input ? "pipe" : "inherit", "pipe", "pipe"], cwd, }); let stdout = ""; @@ -71,6 +72,11 @@ export async function runCommandWithTimeout( child.kill("SIGKILL"); }, timeoutMs); + if (input && child.stdin) { + child.stdin.write(input); + child.stdin.end(); + } + child.stdout?.on("data", (d) => { stdout += d.toString(); }); diff --git a/src/twilio/webhook.ts b/src/twilio/webhook.ts index e82970d10..8198831b6 100644 --- a/src/twilio/webhook.ts +++ b/src/twilio/webhook.ts @@ -120,7 +120,7 @@ export async function startWebhook( app.use((_req, res) => { 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. diff --git a/src/version.ts b/src/version.ts index c43b54bac..5ad0d68ba 100644 --- a/src/version.ts +++ b/src/version.ts @@ -3,5 +3,5 @@ import { createRequire } from "node:module"; const require = createRequire(import.meta.url); 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"; diff --git a/src/web/auto-reply.test.ts b/src/web/auto-reply.test.ts index 8a5390757..86f0abe74 100644 --- a/src/web/auto-reply.test.ts +++ b/src/web/auto-reply.test.ts @@ -631,8 +631,8 @@ describe("web auto-reply", () => { expect(resolver).toHaveBeenCalledTimes(1); const args = resolver.mock.calls[0][0]; - expect(args.Body).toContain("[Jan 1 00:00] [warelay] first"); - expect(args.Body).toContain("[Jan 1 01:00] [warelay] second"); + expect(args.Body).toContain("[Jan 1 00:00] [clawdis] first"); + expect(args.Body).toContain("[Jan 1 01:00] [clawdis] second"); // Max listeners bumped to avoid warnings in multi-instance test runs expect(process.getMaxListeners?.()).toBeGreaterThanOrEqual(50); diff --git a/src/web/auto-reply.ts b/src/web/auto-reply.ts index 2eb4d5dc7..11f3ad1e5 100644 --- a/src/web/auto-reply.ts +++ b/src/web/auto-reply.ts @@ -690,7 +690,7 @@ export async function monitorWebProvider( let messagePrefix = cfg.inbound?.messagePrefix; if (messagePrefix === undefined) { const hasAllowFrom = (cfg.inbound?.allowFrom?.length ?? 0) > 0; - messagePrefix = hasAllowFrom ? "" : "[warelay]"; + messagePrefix = hasAllowFrom ? "" : "[clawdis]"; } const prefixStr = messagePrefix ? `${messagePrefix} ` : ""; 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) if ("sendMessage" in listener && "sendComposingTo" in listener) { startIpcServer(async (to, message, mediaUrl) => { @@ -1300,7 +1300,7 @@ export async function monitorWebProvider( if (loggedOut) { runtime.error( 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(); diff --git a/src/web/ipc.ts b/src/web/ipc.ts index 0d3943532..bc982a545 100644 --- a/src/web/ipc.ts +++ b/src/web/ipc.ts @@ -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 - * `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. * * This prevents Signal session ratchet corruption from multiple connections. diff --git a/src/web/login.ts b/src/web/login.ts index 33d7437d7..a507ac401 100644 --- a/src/web/login.ts +++ b/src/web/login.ts @@ -55,7 +55,7 @@ export async function loginWeb( await fs.rm(WA_WEB_AUTH_DIR, { recursive: true, force: true }); console.error( 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."); diff --git a/src/web/session.ts b/src/web/session.ts index 38c185792..67c32cd95 100644 --- a/src/web/session.ts +++ b/src/web/session.ts @@ -48,7 +48,7 @@ export async function createWaSocket(printQr: boolean, verbose: boolean) { version, logger, printQRInTerminal: false, - browser: ["warelay", "cli", VERSION], + browser: ["clawdis", "cli", VERSION], syncFullHistory: false, markOnlineOnConnect: false, }); @@ -69,7 +69,7 @@ export async function createWaSocket(printQr: boolean, verbose: boolean) { const status = getStatusCode(lastDisconnect?.error); if (status === DisconnectReason.loggedOut) { console.error( - danger("WhatsApp session logged out. Run: warelay login"), + danger("WhatsApp session logged out. Run: clawdis login"), ); } } diff --git a/src/web/test-helpers.ts b/src/web/test-helpers.ts index 923fa19f3..b4aa076c6 100644 --- a/src/web/test-helpers.ts +++ b/src/web/test-helpers.ts @@ -4,7 +4,7 @@ import type { MockBaileysSocket } 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 -const CONFIG_KEY = Symbol.for("warelay:testConfigMock"); +const CONFIG_KEY = Symbol.for("clawdis:testConfigMock"); const DEFAULT_CONFIG = { inbound: { allowFrom: ["*"], // Allow all in tests by default @@ -50,7 +50,7 @@ vi.mock("../media/store.js", () => ({ vi.mock("@whiskeysockets/baileys", () => { const created = createMockBaileys(); (globalThis as Record)[ - Symbol.for("warelay:lastSocket") + Symbol.for("clawdis:lastSocket") ] = created.lastSocket; return created.mod; }); @@ -72,7 +72,7 @@ export const baileys = (await import( export function resetBaileysMocks() { const recreated = createMockBaileys(); (globalThis as Record)[ - Symbol.for("warelay:lastSocket") + Symbol.for("clawdis:lastSocket") ] = recreated.lastSocket; baileys.makeWASocket.mockImplementation(recreated.mod.makeWASocket); baileys.useMultiFileAuthState.mockImplementation( @@ -88,7 +88,7 @@ export function resetBaileysMocks() { export function getLastSocket(): MockBaileysSocket { const getter = (globalThis as Record)[ - Symbol.for("warelay:lastSocket") + Symbol.for("clawdis:lastSocket") ]; if (typeof getter === "function") return (getter as () => MockBaileysSocket)();