diff --git a/docs/AGENTS.default.md b/docs/AGENTS.default.md index d7e49804b..dcb91ccbc 100644 --- a/docs/AGENTS.default.md +++ b/docs/AGENTS.default.md @@ -6,6 +6,37 @@ read_when: --- # AGENTS.md — Clawdis Personal Assistant (default) +## First run (recommended) + +1) Create a dedicated workspace for your assistant (where it can read/write files): + +```bash +mkdir -p ~/clawd +``` + +2) Copy this template to your workspace root as `AGENTS.md`: + +```bash +cp docs/AGENTS.default.md ~/clawd/AGENTS.md +``` + +3) Point CLAWDIS at that workspace so Pi runs with the right context: + +```json5 +{ + inbound: { + reply: { + cwd: "~/clawd" + } + } +} +``` + +## Safety defaults +- Don’t dump directories or secrets into chat. +- Don’t run destructive commands unless explicitly asked. +- Don’t send partial/streaming replies to external messaging surfaces (only final replies). + ## What Clawdis Does - Runs WhatsApp gateway + Pi coding agent so the assistant can read/write chats, fetch context, and run tools via the host Mac. - macOS app manages permissions (screen recording, notifications, microphone) and exposes a CLI helper `clawdis-mac` for scripts. diff --git a/docs/agents.md b/docs/agents.md index 1a0c4f67f..0b4d7291e 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -17,6 +17,8 @@ If you don’t configure `inbound.reply`, CLAWDIS uses the bundled Pi binary in This is usually enough for a personal assistant setup; add `inbound.allowFrom` to restrict who can trigger it. +If you keep an `AGENTS.md` (and optional “memory” files) for the agent, set `inbound.reply.cwd` to that workspace so Pi runs with the right context. + ## Custom agent command (still Pi) To override the agent command, configure `inbound.reply.mode: "command"`: diff --git a/docs/clawd.md b/docs/clawd.md index 6babbad42..bea9b823a 100644 --- a/docs/clawd.md +++ b/docs/clawd.md @@ -88,6 +88,29 @@ clawdis webchat Now message the assistant number from your allowlisted phone. +## Give the agent a workspace (AGENTS.md) + +Pi (the bundled coding agent) will read operating instructions and “memory” from the current working directory. For a good first-run experience, create a dedicated workspace and drop an `AGENTS.md` there. + +From the CLAWDIS repo: + +```bash +mkdir -p ~/clawd +cp docs/AGENTS.default.md ~/clawd/AGENTS.md +``` + +Then set `inbound.reply.cwd` to that directory (supports `~`): + +```json5 +{ + inbound: { + reply: { + cwd: "~/clawd" + } + } +} +``` + ## The config that turns it into “an assistant” CLAWDIS defaults to a good Pi setup even without `inbound.reply`, but you’ll usually want to tune: @@ -110,6 +133,8 @@ Example: mode: "command", // Pi is bundled; CLAWDIS forces --mode rpc for Pi runs. command: ["pi", "--mode", "rpc", "{{BodyStripped}}"], + // Run the agent from your dedicated workspace (AGENTS.md, memory files, etc). + cwd: "~/clawd", timeoutSeconds: 1800, bodyPrefix: "/think:high ", session: { diff --git a/docs/configuration.md b/docs/configuration.md index 95f013762..fbc33d3d2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -81,6 +81,9 @@ Example command-mode config: mode: "command", // Example: run the bundled agent (Pi) in RPC mode command: ["pi", "--mode", "rpc", "{{BodyStripped}}"], + // Optional: run the agent from a specific working directory (supports ~). + // Useful when you keep an AGENTS.md + memory files in a dedicated workspace. + cwd: "~/clawd", timeoutSeconds: 1800, heartbeatMinutes: 30, // Optional: override the command used for heartbeat runs @@ -103,6 +106,10 @@ Example command-mode config: } ``` +Notes: +- `inbound.reply.cwd` sets the working directory for the reply command (and Pi RPC). It supports `~` and is resolved to an absolute path. +- If you don’t set it, the agent runs from the Gateway’s current directory (often not what you want for a “personal assistant” workspace). + ### `browser` (clawd-managed Chrome) Clawdis can start a **dedicated, isolated** Chrome/Chromium instance for clawd and expose a small loopback control server. diff --git a/docs/index.md b/docs/index.md index 1fc132a20..2e3060b60 100644 --- a/docs/index.md +++ b/docs/index.md @@ -116,6 +116,7 @@ Example: - Start here: - [Configuration](./configuration.md) - [Clawd personal assistant setup](./clawd.md) + - [AGENTS.md template (default)](./AGENTS.default.md) - [Gateway runbook](./gateway.md) - [Discovery + transports](./discovery.md) - [Remote access](./remote.md) diff --git a/src/auto-reply/command-reply.ts b/src/auto-reply/command-reply.ts index dd517ae15..a647eb2be 100644 --- a/src/auto-reply/command-reply.ts +++ b/src/auto-reply/command-reply.ts @@ -16,6 +16,7 @@ import { getChildLogger } from "../logging.js"; import { splitMediaFromOutput } from "../media/parse.js"; import { enqueueCommand } from "../process/command-queue.js"; import { runPiRpc } from "../process/tau-rpc.js"; +import { resolveUserPath } from "../utils.js"; import { applyTemplate, type TemplateContext } from "./templating.js"; import { formatToolAggregate, @@ -357,6 +358,11 @@ export async function runCommandReply( onPartialReply, } = params; + const resolvedCwd = + typeof reply.cwd === "string" && reply.cwd.trim() + ? resolveUserPath(reply.cwd) + : undefined; + if (!reply.command?.length) { throw new Error("reply.command is required for mode=command"); } @@ -499,14 +505,14 @@ export async function runCommandReply( } logVerbose( - `Running command auto-reply: ${rpcArgv.join(" ")}${reply.cwd ? ` (cwd: ${reply.cwd})` : ""}`, + `Running command auto-reply: ${rpcArgv.join(" ")}${resolvedCwd ? ` (cwd: ${resolvedCwd})` : ""}`, ); logger.info( { agent: agentKind, sessionId: templatingCtx.SessionId, newSession: isNewSession, - cwd: reply.cwd, + cwd: resolvedCwd, command: rpcArgv.slice(0, -1), // omit body to reduce noise }, "command auto-reply start", @@ -593,7 +599,7 @@ export async function runCommandReply( const rpcResult = await runPiRpc({ argv: rpcArgvForRun, - cwd: reply.cwd, + cwd: resolvedCwd, prompt: body, timeoutMs, onEvent: (line: string) => { @@ -758,7 +764,7 @@ export async function runCommandReply( signal: signalUsed, killed: killedUsed, argv: finalArgv, - cwd: reply.cwd, + cwd: resolvedCwd, stdout: truncate(rawStdout), stderr: truncate(stderrUsed), }, @@ -888,7 +894,7 @@ export async function runCommandReply( const elapsed = Date.now() - started; verboseLog(`Command auto-reply finished in ${elapsed}ms`); logger.info( - { durationMs: elapsed, agent: agentKind, cwd: reply.cwd }, + { durationMs: elapsed, agent: agentKind, cwd: resolvedCwd }, "command auto-reply finished", ); if ((codeUsed ?? 0) !== 0) { @@ -995,7 +1001,7 @@ export async function runCommandReply( } catch (err) { const elapsed = Date.now() - started; logger.info( - { durationMs: elapsed, agent: agentKind, cwd: reply.cwd }, + { durationMs: elapsed, agent: agentKind, cwd: resolvedCwd }, "command auto-reply failed", ); const anyErr = err as { killed?: boolean; signal?: string }; @@ -1010,7 +1016,7 @@ export async function runCommandReply( ); const baseMsg = "Command timed out after " + - `${timeoutSeconds}s${reply.cwd ? ` (cwd: ${reply.cwd})` : ""}. Try a shorter prompt or split the request.`; + `${timeoutSeconds}s${resolvedCwd ? ` (cwd: ${resolvedCwd})` : ""}. Try a shorter prompt or split the request.`; const partial = extractRpcAssistantText(errorObj.stdout ?? "") || extractAssistantTextLoosely(errorObj.stdout ?? "") || diff --git a/src/utils.test.ts b/src/utils.test.ts index a2bc0d975..deacb1c5e 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -9,6 +9,7 @@ import { jidToE164, normalizeE164, normalizePath, + resolveUserPath, sleep, toWhatsappJid, withWhatsAppPrefix, @@ -85,3 +86,19 @@ describe("jidToE164", () => { spy.mockRestore(); }); }); + +describe("resolveUserPath", () => { + it("expands ~ to home dir", () => { + expect(resolveUserPath("~")).toBe(path.resolve(os.homedir())); + }); + + it("expands ~/ to home dir", () => { + expect(resolveUserPath("~/clawd")).toBe( + path.resolve(os.homedir(), "clawd"), + ); + }); + + it("resolves relative paths", () => { + expect(resolveUserPath("tmp/dir")).toBe(path.resolve("tmp/dir")); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index cdc69590c..39793ddbf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -71,5 +71,14 @@ export function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } +export function resolveUserPath(input: string): string { + const trimmed = input.trim(); + if (!trimmed) return trimmed; + if (trimmed.startsWith("~")) { + return path.resolve(trimmed.replace("~", os.homedir())); + } + return path.resolve(trimmed); +} + // Fixed configuration root; legacy ~/.clawdis is no longer used. export const CONFIG_DIR = path.join(os.homedir(), ".clawdis");