fix: expand reply cwd (~) and document AGENTS
parent
5ace7c9c66
commit
507f5623f4
|
|
@ -6,6 +6,37 @@ read_when:
|
||||||
---
|
---
|
||||||
# AGENTS.md — Clawdis Personal Assistant (default)
|
# 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
|
## 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.
|
- 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.
|
- macOS app manages permissions (screen recording, notifications, microphone) and exposes a CLI helper `clawdis-mac` for scripts.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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)
|
## Custom agent command (still Pi)
|
||||||
|
|
||||||
To override the agent command, configure `inbound.reply.mode: "command"`:
|
To override the agent command, configure `inbound.reply.mode: "command"`:
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,29 @@ clawdis webchat
|
||||||
|
|
||||||
Now message the assistant number from your allowlisted phone.
|
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”
|
## 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:
|
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",
|
mode: "command",
|
||||||
// Pi is bundled; CLAWDIS forces --mode rpc for Pi runs.
|
// Pi is bundled; CLAWDIS forces --mode rpc for Pi runs.
|
||||||
command: ["pi", "--mode", "rpc", "{{BodyStripped}}"],
|
command: ["pi", "--mode", "rpc", "{{BodyStripped}}"],
|
||||||
|
// Run the agent from your dedicated workspace (AGENTS.md, memory files, etc).
|
||||||
|
cwd: "~/clawd",
|
||||||
timeoutSeconds: 1800,
|
timeoutSeconds: 1800,
|
||||||
bodyPrefix: "/think:high ",
|
bodyPrefix: "/think:high ",
|
||||||
session: {
|
session: {
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,9 @@ Example command-mode config:
|
||||||
mode: "command",
|
mode: "command",
|
||||||
// Example: run the bundled agent (Pi) in RPC mode
|
// Example: run the bundled agent (Pi) in RPC mode
|
||||||
command: ["pi", "--mode", "rpc", "{{BodyStripped}}"],
|
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,
|
timeoutSeconds: 1800,
|
||||||
heartbeatMinutes: 30,
|
heartbeatMinutes: 30,
|
||||||
// Optional: override the command used for heartbeat runs
|
// 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)
|
### `browser` (clawd-managed Chrome)
|
||||||
|
|
||||||
Clawdis can start a **dedicated, isolated** Chrome/Chromium instance for clawd and expose a small loopback control server.
|
Clawdis can start a **dedicated, isolated** Chrome/Chromium instance for clawd and expose a small loopback control server.
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ Example:
|
||||||
- Start here:
|
- Start here:
|
||||||
- [Configuration](./configuration.md)
|
- [Configuration](./configuration.md)
|
||||||
- [Clawd personal assistant setup](./clawd.md)
|
- [Clawd personal assistant setup](./clawd.md)
|
||||||
|
- [AGENTS.md template (default)](./AGENTS.default.md)
|
||||||
- [Gateway runbook](./gateway.md)
|
- [Gateway runbook](./gateway.md)
|
||||||
- [Discovery + transports](./discovery.md)
|
- [Discovery + transports](./discovery.md)
|
||||||
- [Remote access](./remote.md)
|
- [Remote access](./remote.md)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { getChildLogger } from "../logging.js";
|
||||||
import { splitMediaFromOutput } from "../media/parse.js";
|
import { splitMediaFromOutput } from "../media/parse.js";
|
||||||
import { enqueueCommand } from "../process/command-queue.js";
|
import { enqueueCommand } from "../process/command-queue.js";
|
||||||
import { runPiRpc } from "../process/tau-rpc.js";
|
import { runPiRpc } from "../process/tau-rpc.js";
|
||||||
|
import { resolveUserPath } from "../utils.js";
|
||||||
import { applyTemplate, type TemplateContext } from "./templating.js";
|
import { applyTemplate, type TemplateContext } from "./templating.js";
|
||||||
import {
|
import {
|
||||||
formatToolAggregate,
|
formatToolAggregate,
|
||||||
|
|
@ -357,6 +358,11 @@ export async function runCommandReply(
|
||||||
onPartialReply,
|
onPartialReply,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
|
const resolvedCwd =
|
||||||
|
typeof reply.cwd === "string" && reply.cwd.trim()
|
||||||
|
? resolveUserPath(reply.cwd)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (!reply.command?.length) {
|
if (!reply.command?.length) {
|
||||||
throw new Error("reply.command is required for mode=command");
|
throw new Error("reply.command is required for mode=command");
|
||||||
}
|
}
|
||||||
|
|
@ -499,14 +505,14 @@ export async function runCommandReply(
|
||||||
}
|
}
|
||||||
|
|
||||||
logVerbose(
|
logVerbose(
|
||||||
`Running command auto-reply: ${rpcArgv.join(" ")}${reply.cwd ? ` (cwd: ${reply.cwd})` : ""}`,
|
`Running command auto-reply: ${rpcArgv.join(" ")}${resolvedCwd ? ` (cwd: ${resolvedCwd})` : ""}`,
|
||||||
);
|
);
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
agent: agentKind,
|
agent: agentKind,
|
||||||
sessionId: templatingCtx.SessionId,
|
sessionId: templatingCtx.SessionId,
|
||||||
newSession: isNewSession,
|
newSession: isNewSession,
|
||||||
cwd: reply.cwd,
|
cwd: resolvedCwd,
|
||||||
command: rpcArgv.slice(0, -1), // omit body to reduce noise
|
command: rpcArgv.slice(0, -1), // omit body to reduce noise
|
||||||
},
|
},
|
||||||
"command auto-reply start",
|
"command auto-reply start",
|
||||||
|
|
@ -593,7 +599,7 @@ export async function runCommandReply(
|
||||||
|
|
||||||
const rpcResult = await runPiRpc({
|
const rpcResult = await runPiRpc({
|
||||||
argv: rpcArgvForRun,
|
argv: rpcArgvForRun,
|
||||||
cwd: reply.cwd,
|
cwd: resolvedCwd,
|
||||||
prompt: body,
|
prompt: body,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
onEvent: (line: string) => {
|
onEvent: (line: string) => {
|
||||||
|
|
@ -758,7 +764,7 @@ export async function runCommandReply(
|
||||||
signal: signalUsed,
|
signal: signalUsed,
|
||||||
killed: killedUsed,
|
killed: killedUsed,
|
||||||
argv: finalArgv,
|
argv: finalArgv,
|
||||||
cwd: reply.cwd,
|
cwd: resolvedCwd,
|
||||||
stdout: truncate(rawStdout),
|
stdout: truncate(rawStdout),
|
||||||
stderr: truncate(stderrUsed),
|
stderr: truncate(stderrUsed),
|
||||||
},
|
},
|
||||||
|
|
@ -888,7 +894,7 @@ export async function runCommandReply(
|
||||||
const elapsed = Date.now() - started;
|
const elapsed = Date.now() - started;
|
||||||
verboseLog(`Command auto-reply finished in ${elapsed}ms`);
|
verboseLog(`Command auto-reply finished in ${elapsed}ms`);
|
||||||
logger.info(
|
logger.info(
|
||||||
{ durationMs: elapsed, agent: agentKind, cwd: reply.cwd },
|
{ durationMs: elapsed, agent: agentKind, cwd: resolvedCwd },
|
||||||
"command auto-reply finished",
|
"command auto-reply finished",
|
||||||
);
|
);
|
||||||
if ((codeUsed ?? 0) !== 0) {
|
if ((codeUsed ?? 0) !== 0) {
|
||||||
|
|
@ -995,7 +1001,7 @@ export async function runCommandReply(
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const elapsed = Date.now() - started;
|
const elapsed = Date.now() - started;
|
||||||
logger.info(
|
logger.info(
|
||||||
{ durationMs: elapsed, agent: agentKind, cwd: reply.cwd },
|
{ durationMs: elapsed, agent: agentKind, cwd: resolvedCwd },
|
||||||
"command auto-reply failed",
|
"command auto-reply failed",
|
||||||
);
|
);
|
||||||
const anyErr = err as { killed?: boolean; signal?: string };
|
const anyErr = err as { killed?: boolean; signal?: string };
|
||||||
|
|
@ -1010,7 +1016,7 @@ export async function runCommandReply(
|
||||||
);
|
);
|
||||||
const baseMsg =
|
const baseMsg =
|
||||||
"Command timed out after " +
|
"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 =
|
const partial =
|
||||||
extractRpcAssistantText(errorObj.stdout ?? "") ||
|
extractRpcAssistantText(errorObj.stdout ?? "") ||
|
||||||
extractAssistantTextLoosely(errorObj.stdout ?? "") ||
|
extractAssistantTextLoosely(errorObj.stdout ?? "") ||
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
jidToE164,
|
jidToE164,
|
||||||
normalizeE164,
|
normalizeE164,
|
||||||
normalizePath,
|
normalizePath,
|
||||||
|
resolveUserPath,
|
||||||
sleep,
|
sleep,
|
||||||
toWhatsappJid,
|
toWhatsappJid,
|
||||||
withWhatsAppPrefix,
|
withWhatsAppPrefix,
|
||||||
|
|
@ -85,3 +86,19 @@ describe("jidToE164", () => {
|
||||||
spy.mockRestore();
|
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"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -71,5 +71,14 @@ export function sleep(ms: number) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
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.
|
// Fixed configuration root; legacy ~/.clawdis is no longer used.
|
||||||
export const CONFIG_DIR = path.join(os.homedir(), ".clawdis");
|
export const CONFIG_DIR = path.join(os.homedir(), ".clawdis");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue