Switch to clawdis RPC mode and complete rebrand
This commit is contained in:
20
AGENTS.md
20
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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"})`,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)();
|
||||
|
||||
Reference in New Issue
Block a user