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
- 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.

View File

@@ -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",

View File

@@ -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.

View File

@@ -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.

View File

@@ -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"})`,
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 <uuid> --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 <uuid> --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) {

View File

@@ -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,
) {

View File

@@ -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<

View File

@@ -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(),

View File

@@ -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);

View File

@@ -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.",
),
);
}

View File

@@ -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()) {

View File

@@ -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) {

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 { 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

View File

@@ -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) {

View File

@@ -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();
});

View File

@@ -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.

View File

@@ -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";

View File

@@ -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);

View File

@@ -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();

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
* `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.

View File

@@ -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.");

View File

@@ -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"),
);
}
}

View File

@@ -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<PropertyKey, unknown>)[
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<PropertyKey, unknown>)[
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<PropertyKey, unknown>)[
Symbol.for("warelay:lastSocket")
Symbol.for("clawdis:lastSocket")
];
if (typeof getter === "function")
return (getter as () => MockBaileysSocket)();