diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fbe6c6e2..7affc760f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ ## [Unreleased] 0.1.3 -_Add notes here for the next release._ +### Features +- Added `cwd` option to command reply config for setting the working directory where commands execute. Essential for Claude Code to have proper project context. + +### Developer notes +- Command auto-replies now pass `{ timeoutMs, cwd }` into the command runner; custom runners/tests that stub `runCommandWithTimeout` should accept the options object as well as the legacy numeric timeout. ## 0.1.2 — 2025-11-25 diff --git a/docs/claude-config.md b/docs/claude-config.md index 91d314009..076e53ad5 100644 --- a/docs/claude-config.md +++ b/docs/claude-config.md @@ -21,6 +21,8 @@ warelay reads `~/.warelay/warelay.json` (JSON5 accepted). Add a command-mode rep allowFrom: ["+15551234567"], reply: { mode: "command", + // Working directory for command execution (useful for Claude Code project context). + cwd: "/Users/you/Projects/my-project", // Prepended before the inbound body; good for system prompts. bodyPrefix: "You are a concise WhatsApp assistant. Keep replies under 1500 characters.\n\n", // Claude CLI argv; the final element is the prompt/body provided by warelay. @@ -38,6 +40,7 @@ warelay reads `~/.warelay/warelay.json` (JSON5 accepted). Add a command-mode rep ``` Notes on this configuration: +- `cwd` sets the working directory where the command runs. This is essential for Claude Code to have the right project context—Claude will see the project's `CLAUDE.md`, have access to project files, and understand the codebase structure. - warelay automatically injects a Claude identity prefix and the correct `--output-format`/`-p` flags when `command[0]` is `claude` and `claudeOutputFormat` is set. - Sessions are stored in `~/.warelay/sessions.json`; `scope: per-sender` keeps separate threads for each contact. - `bodyPrefix` is added before the inbound message body that reaches Claude. The string above mirrors the built-in 1500-character WhatsApp guardrail. diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index 886493703..e6178a603 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -288,11 +288,13 @@ export async function getReplyFromConfig( [CLAUDE_IDENTITY_PREFIX, existingBody].filter(Boolean).join("\n\n"), ]; } - logVerbose(`Running command auto-reply: ${finalArgv.join(" ")}`); + logVerbose( + `Running command auto-reply: ${finalArgv.join(" ")}${reply.cwd ? ` (cwd: ${reply.cwd})` : ""}`, + ); const started = Date.now(); try { const { stdout, stderr, code, signal, killed } = await enqueueCommand( - () => commandRunner(finalArgv, timeoutMs), + () => commandRunner(finalArgv, { timeoutMs, cwd: reply.cwd }), { onWait: (waitMs, queuedAhead) => { if (isVerbose()) { diff --git a/src/config/config.ts b/src/config/config.ts index bf405a40f..4ba46a362 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -26,6 +26,7 @@ export type WarelayConfig = { mode: ReplyMode; text?: string; // for mode=text, can contain {{Body}} command?: string[]; // for mode=command, argv with templates + cwd?: string; // working directory for command execution template?: string; // prepend template string when building command/prompt timeoutSeconds?: number; // optional command timeout; defaults to 600s bodyPrefix?: string; // optional string prepended to Body before templating @@ -43,6 +44,7 @@ const ReplySchema = z mode: z.union([z.literal("text"), z.literal("command")]), text: z.string().optional(), command: z.array(z.string()).optional(), + cwd: z.string().optional(), template: z.string().optional(), timeoutSeconds: z.number().int().positive().optional(), bodyPrefix: z.string().optional(), diff --git a/src/process/exec.ts b/src/process/exec.ts index 75c2cca30..db8fd59c8 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -43,14 +43,26 @@ export type SpawnResult = { killed: boolean; }; +export type CommandOptions = { + timeoutMs: number; + cwd?: string; +}; + export async function runCommandWithTimeout( argv: string[], - timeoutMs: number, + optionsOrTimeout: number | CommandOptions, ): Promise { + const options: CommandOptions = + typeof optionsOrTimeout === "number" + ? { timeoutMs: optionsOrTimeout } + : optionsOrTimeout; + const { timeoutMs, cwd } = 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"], + cwd, }); let stdout = ""; let stderr = "";