clawdbot 2026.1.4 → 2026.1.5-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -1
- package/README.md +26 -1
- package/dist/agents/pi-embedded-runner.js +2 -0
- package/dist/agents/pi-embedded-subscribe.js +18 -3
- package/dist/agents/pi-tools.js +45 -6
- package/dist/agents/tools/browser-tool.js +38 -89
- package/dist/agents/tools/cron-tool.js +8 -8
- package/dist/agents/workspace.js +8 -1
- package/dist/auto-reply/command-detection.js +26 -0
- package/dist/auto-reply/reply/agent-runner.js +15 -8
- package/dist/auto-reply/reply/commands.js +36 -25
- package/dist/auto-reply/reply/directive-handling.js +4 -2
- package/dist/auto-reply/reply/directives.js +12 -0
- package/dist/auto-reply/reply/session-updates.js +2 -4
- package/dist/auto-reply/reply.js +26 -4
- package/dist/browser/config.js +22 -4
- package/dist/browser/profiles-service.js +3 -1
- package/dist/browser/profiles.js +14 -3
- package/dist/canvas-host/a2ui/.bundle.hash +2 -0
- package/dist/cli/gateway-cli.js +2 -2
- package/dist/cli/profile.js +81 -0
- package/dist/cli/program.js +10 -1
- package/dist/cli/run-main.js +33 -0
- package/dist/commands/configure.js +5 -0
- package/dist/commands/onboard-providers.js +1 -1
- package/dist/commands/setup.js +4 -1
- package/dist/config/defaults.js +56 -0
- package/dist/config/io.js +47 -6
- package/dist/config/paths.js +2 -2
- package/dist/config/port-defaults.js +32 -0
- package/dist/config/sessions.js +3 -2
- package/dist/config/validation.js +2 -2
- package/dist/config/zod-schema.js +16 -0
- package/dist/discord/monitor.js +75 -266
- package/dist/entry.js +16 -0
- package/dist/gateway/call.js +8 -1
- package/dist/gateway/server-methods/chat.js +1 -1
- package/dist/gateway/server.js +14 -3
- package/dist/index.js +2 -2
- package/dist/infra/control-ui-assets.js +118 -0
- package/dist/infra/dotenv.js +15 -0
- package/dist/infra/shell-env.js +79 -0
- package/dist/infra/system-events.js +50 -23
- package/dist/macos/relay.js +8 -2
- package/dist/sessions/send-policy.js +68 -0
- package/dist/telegram/bot.js +24 -1
- package/dist/utils.js +8 -2
- package/dist/web/auto-reply.js +18 -21
- package/dist/web/inbound.js +5 -1
- package/dist/web/qr-image.js +4 -4
- package/dist/web/session.js +2 -3
- package/docs/agent.md +0 -2
- package/docs/assets/markdown.css +4 -1
- package/docs/audio.md +0 -2
- package/docs/clawd.md +0 -2
- package/docs/configuration.md +62 -3
- package/docs/docs.json +9 -1
- package/docs/faq.md +32 -7
- package/docs/gateway.md +28 -0
- package/docs/images.md +0 -2
- package/docs/index.md +2 -4
- package/docs/mac/icon.md +1 -1
- package/docs/nix.md +57 -11
- package/docs/onboarding.md +0 -2
- package/docs/refactor/webagent-session.md +0 -2
- package/docs/research/memory.md +1 -1
- package/docs/skills.md +0 -2
- package/docs/templates/AGENTS.md +2 -2
- package/docs/tools.md +15 -0
- package/docs/whatsapp.md +2 -0
- package/package.json +9 -8
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
import { resolveConfigDir } from "../utils.js";
|
|
5
|
+
export function loadDotEnv(opts) {
|
|
6
|
+
const quiet = opts?.quiet ?? true;
|
|
7
|
+
// Load from process CWD first (dotenv default).
|
|
8
|
+
dotenv.config({ quiet });
|
|
9
|
+
// Then load global fallback: ~/.clawdbot/.env (or CLAWDBOT_STATE_DIR/.env),
|
|
10
|
+
// without overriding any env vars already present.
|
|
11
|
+
const globalEnvPath = path.join(resolveConfigDir(process.env), ".env");
|
|
12
|
+
if (!fs.existsSync(globalEnvPath))
|
|
13
|
+
return;
|
|
14
|
+
dotenv.config({ quiet, path: globalEnvPath, override: false });
|
|
15
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
3
|
+
const DEFAULT_MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
4
|
+
function isTruthy(raw) {
|
|
5
|
+
if (!raw)
|
|
6
|
+
return false;
|
|
7
|
+
const value = raw.trim().toLowerCase();
|
|
8
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
9
|
+
}
|
|
10
|
+
function resolveShell(env) {
|
|
11
|
+
const shell = env.SHELL?.trim();
|
|
12
|
+
return shell && shell.length > 0 ? shell : "/bin/sh";
|
|
13
|
+
}
|
|
14
|
+
export function loadShellEnvFallback(opts) {
|
|
15
|
+
const logger = opts.logger ?? console;
|
|
16
|
+
const exec = opts.exec ?? execFileSync;
|
|
17
|
+
if (!opts.enabled)
|
|
18
|
+
return { ok: true, applied: [], skippedReason: "disabled" };
|
|
19
|
+
const hasAnyKey = opts.expectedKeys.some((key) => Boolean(opts.env[key]?.trim()));
|
|
20
|
+
if (hasAnyKey) {
|
|
21
|
+
return { ok: true, applied: [], skippedReason: "already-has-keys" };
|
|
22
|
+
}
|
|
23
|
+
const timeoutMs = typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
|
24
|
+
? Math.max(0, opts.timeoutMs)
|
|
25
|
+
: DEFAULT_TIMEOUT_MS;
|
|
26
|
+
const shell = resolveShell(opts.env);
|
|
27
|
+
let stdout;
|
|
28
|
+
try {
|
|
29
|
+
stdout = exec(shell, ["-l", "-c", "env -0"], {
|
|
30
|
+
encoding: "buffer",
|
|
31
|
+
timeout: timeoutMs,
|
|
32
|
+
maxBuffer: DEFAULT_MAX_BUFFER_BYTES,
|
|
33
|
+
env: opts.env,
|
|
34
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39
|
+
logger.warn(`[clawdbot] shell env fallback failed: ${msg}`);
|
|
40
|
+
return { ok: false, error: msg, applied: [] };
|
|
41
|
+
}
|
|
42
|
+
const shellEnv = new Map();
|
|
43
|
+
const parts = stdout.toString("utf8").split("\0");
|
|
44
|
+
for (const part of parts) {
|
|
45
|
+
if (!part)
|
|
46
|
+
continue;
|
|
47
|
+
const eq = part.indexOf("=");
|
|
48
|
+
if (eq <= 0)
|
|
49
|
+
continue;
|
|
50
|
+
const key = part.slice(0, eq);
|
|
51
|
+
const value = part.slice(eq + 1);
|
|
52
|
+
if (!key)
|
|
53
|
+
continue;
|
|
54
|
+
shellEnv.set(key, value);
|
|
55
|
+
}
|
|
56
|
+
const applied = [];
|
|
57
|
+
for (const key of opts.expectedKeys) {
|
|
58
|
+
if (opts.env[key]?.trim())
|
|
59
|
+
continue;
|
|
60
|
+
const value = shellEnv.get(key);
|
|
61
|
+
if (!value?.trim())
|
|
62
|
+
continue;
|
|
63
|
+
opts.env[key] = value;
|
|
64
|
+
applied.push(key);
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, applied };
|
|
67
|
+
}
|
|
68
|
+
export function shouldEnableShellEnvFallback(env) {
|
|
69
|
+
return isTruthy(env.CLAWDBOT_LOAD_SHELL_ENV);
|
|
70
|
+
}
|
|
71
|
+
export function resolveShellEnvFallbackTimeoutMs(env) {
|
|
72
|
+
const raw = env.CLAWDBOT_SHELL_ENV_TIMEOUT_MS?.trim();
|
|
73
|
+
if (!raw)
|
|
74
|
+
return DEFAULT_TIMEOUT_MS;
|
|
75
|
+
const parsed = Number.parseInt(raw, 10);
|
|
76
|
+
if (!Number.isFinite(parsed))
|
|
77
|
+
return DEFAULT_TIMEOUT_MS;
|
|
78
|
+
return Math.max(0, parsed);
|
|
79
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// Lightweight in-memory queue for human-readable system events that should be
|
|
2
|
-
// prefixed to the next
|
|
3
|
-
//
|
|
2
|
+
// prefixed to the next prompt. We intentionally avoid persistence to keep
|
|
3
|
+
// events ephemeral. Events are session-scoped; callers that don't specify a
|
|
4
|
+
// session key default to "main".
|
|
5
|
+
const DEFAULT_SESSION_KEY = "main";
|
|
4
6
|
const MAX_EVENTS = 20;
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
const queues = new Map();
|
|
8
|
+
function normalizeSessionKey(key) {
|
|
9
|
+
const trimmed = typeof key === "string" ? key.trim() : "";
|
|
10
|
+
return trimmed || DEFAULT_SESSION_KEY;
|
|
11
|
+
}
|
|
8
12
|
function normalizeContextKey(key) {
|
|
9
13
|
if (!key)
|
|
10
14
|
return null;
|
|
@@ -13,32 +17,55 @@ function normalizeContextKey(key) {
|
|
|
13
17
|
return null;
|
|
14
18
|
return trimmed.toLowerCase();
|
|
15
19
|
}
|
|
16
|
-
export function isSystemEventContextChanged(contextKey) {
|
|
20
|
+
export function isSystemEventContextChanged(contextKey, sessionKey) {
|
|
21
|
+
const key = normalizeSessionKey(sessionKey);
|
|
22
|
+
const existing = queues.get(key);
|
|
17
23
|
const normalized = normalizeContextKey(contextKey);
|
|
18
|
-
return normalized !== lastContextKey;
|
|
24
|
+
return normalized !== (existing?.lastContextKey ?? null);
|
|
19
25
|
}
|
|
20
26
|
export function enqueueSystemEvent(text, options) {
|
|
27
|
+
const key = normalizeSessionKey(options?.sessionKey);
|
|
28
|
+
const entry = queues.get(key) ??
|
|
29
|
+
(() => {
|
|
30
|
+
const created = {
|
|
31
|
+
queue: [],
|
|
32
|
+
lastText: null,
|
|
33
|
+
lastContextKey: null,
|
|
34
|
+
};
|
|
35
|
+
queues.set(key, created);
|
|
36
|
+
return created;
|
|
37
|
+
})();
|
|
21
38
|
const cleaned = text.trim();
|
|
22
39
|
if (!cleaned)
|
|
23
40
|
return;
|
|
24
|
-
lastContextKey = normalizeContextKey(options?.contextKey);
|
|
25
|
-
if (lastText === cleaned)
|
|
41
|
+
entry.lastContextKey = normalizeContextKey(options?.contextKey);
|
|
42
|
+
if (entry.lastText === cleaned)
|
|
26
43
|
return; // skip consecutive duplicates
|
|
27
|
-
lastText = cleaned;
|
|
28
|
-
queue.push({ text: cleaned, ts: Date.now() });
|
|
29
|
-
if (queue.length > MAX_EVENTS)
|
|
30
|
-
queue.shift();
|
|
31
|
-
}
|
|
32
|
-
export function drainSystemEvents() {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
entry.lastText = cleaned;
|
|
45
|
+
entry.queue.push({ text: cleaned, ts: Date.now() });
|
|
46
|
+
if (entry.queue.length > MAX_EVENTS)
|
|
47
|
+
entry.queue.shift();
|
|
48
|
+
}
|
|
49
|
+
export function drainSystemEvents(sessionKey) {
|
|
50
|
+
const key = normalizeSessionKey(sessionKey);
|
|
51
|
+
const entry = queues.get(key);
|
|
52
|
+
if (!entry || entry.queue.length === 0)
|
|
53
|
+
return [];
|
|
54
|
+
const out = entry.queue.map((e) => e.text);
|
|
55
|
+
entry.queue.length = 0;
|
|
56
|
+
entry.lastText = null;
|
|
57
|
+
entry.lastContextKey = null;
|
|
58
|
+
queues.delete(key);
|
|
37
59
|
return out;
|
|
38
60
|
}
|
|
39
|
-
export function peekSystemEvents() {
|
|
40
|
-
|
|
61
|
+
export function peekSystemEvents(sessionKey) {
|
|
62
|
+
const key = normalizeSessionKey(sessionKey);
|
|
63
|
+
return queues.get(key)?.queue.map((e) => e.text) ?? [];
|
|
64
|
+
}
|
|
65
|
+
export function hasSystemEvents(sessionKey) {
|
|
66
|
+
const key = normalizeSessionKey(sessionKey);
|
|
67
|
+
return (queues.get(key)?.queue.length ?? 0) > 0;
|
|
41
68
|
}
|
|
42
|
-
export function
|
|
43
|
-
|
|
69
|
+
export function resetSystemEventsForTest() {
|
|
70
|
+
queues.clear();
|
|
44
71
|
}
|
package/dist/macos/relay.js
CHANGED
|
@@ -22,9 +22,15 @@ async function main() {
|
|
|
22
22
|
console.log(BUNDLED_VERSION);
|
|
23
23
|
process.exit(0);
|
|
24
24
|
}
|
|
25
|
+
if (process.env.CLAWDBOT_SMOKE_QR === "1") {
|
|
26
|
+
const { renderQrPngBase64 } = await import("../web/qr-image.js");
|
|
27
|
+
await renderQrPngBase64("clawdbot-smoke");
|
|
28
|
+
console.log("smoke: qr ok");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
25
31
|
await patchBunLongForProtobuf();
|
|
26
|
-
const {
|
|
27
|
-
|
|
32
|
+
const { loadDotEnv } = await import("../infra/dotenv.js");
|
|
33
|
+
loadDotEnv({ quiet: true });
|
|
28
34
|
const { ensureClawdbotCliOnPath } = await import("../infra/path-env.js");
|
|
29
35
|
ensureClawdbotCliOnPath();
|
|
30
36
|
const { enableConsoleCapture } = await import("../logging.js");
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function normalizeSendPolicy(raw) {
|
|
2
|
+
const value = raw?.trim().toLowerCase();
|
|
3
|
+
if (value === "allow")
|
|
4
|
+
return "allow";
|
|
5
|
+
if (value === "deny")
|
|
6
|
+
return "deny";
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
function normalizeMatchValue(raw) {
|
|
10
|
+
const value = raw?.trim().toLowerCase();
|
|
11
|
+
return value ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
function deriveSurfaceFromKey(key) {
|
|
14
|
+
if (!key)
|
|
15
|
+
return undefined;
|
|
16
|
+
const parts = key.split(":").filter(Boolean);
|
|
17
|
+
if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
|
|
18
|
+
return normalizeMatchValue(parts[0]);
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function deriveChatTypeFromKey(key) {
|
|
23
|
+
if (!key)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (key.startsWith("group:") || key.includes(":group:"))
|
|
26
|
+
return "group";
|
|
27
|
+
if (key.includes(":channel:"))
|
|
28
|
+
return "room";
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
export function resolveSendPolicy(params) {
|
|
32
|
+
const override = normalizeSendPolicy(params.entry?.sendPolicy);
|
|
33
|
+
if (override)
|
|
34
|
+
return override;
|
|
35
|
+
const policy = params.cfg.session?.sendPolicy;
|
|
36
|
+
if (!policy)
|
|
37
|
+
return "allow";
|
|
38
|
+
const surface = normalizeMatchValue(params.surface) ??
|
|
39
|
+
normalizeMatchValue(params.entry?.surface) ??
|
|
40
|
+
normalizeMatchValue(params.entry?.lastChannel) ??
|
|
41
|
+
deriveSurfaceFromKey(params.sessionKey);
|
|
42
|
+
const chatType = normalizeMatchValue(params.chatType ?? params.entry?.chatType) ??
|
|
43
|
+
normalizeMatchValue(deriveChatTypeFromKey(params.sessionKey));
|
|
44
|
+
const sessionKey = params.sessionKey ?? "";
|
|
45
|
+
let allowedMatch = false;
|
|
46
|
+
for (const rule of policy.rules ?? []) {
|
|
47
|
+
if (!rule)
|
|
48
|
+
continue;
|
|
49
|
+
const action = normalizeSendPolicy(rule.action) ?? "allow";
|
|
50
|
+
const match = rule.match ?? {};
|
|
51
|
+
const matchSurface = normalizeMatchValue(match.surface);
|
|
52
|
+
const matchChatType = normalizeMatchValue(match.chatType);
|
|
53
|
+
const matchPrefix = normalizeMatchValue(match.keyPrefix);
|
|
54
|
+
if (matchSurface && matchSurface !== surface)
|
|
55
|
+
continue;
|
|
56
|
+
if (matchChatType && matchChatType !== chatType)
|
|
57
|
+
continue;
|
|
58
|
+
if (matchPrefix && !sessionKey.startsWith(matchPrefix))
|
|
59
|
+
continue;
|
|
60
|
+
if (action === "deny")
|
|
61
|
+
return "deny";
|
|
62
|
+
allowedMatch = true;
|
|
63
|
+
}
|
|
64
|
+
if (allowedMatch)
|
|
65
|
+
return "allow";
|
|
66
|
+
const fallback = normalizeSendPolicy(policy.default);
|
|
67
|
+
return fallback ?? "allow";
|
|
68
|
+
}
|
package/dist/telegram/bot.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Buffer } from "node:buffer";
|
|
|
3
3
|
import { apiThrottler } from "@grammyjs/transformer-throttler";
|
|
4
4
|
import { Bot, InputFile, webhookCallback } from "grammy";
|
|
5
5
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
|
6
|
+
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
|
6
7
|
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
|
7
8
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
|
8
9
|
import { loadConfig } from "../config/config.js";
|
|
@@ -76,9 +77,28 @@ export function createTelegramBot(opts) {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
const botUsername = ctx.me?.username?.toLowerCase();
|
|
80
|
+
const allowFromList = Array.isArray(allowFrom)
|
|
81
|
+
? allowFrom.map((entry) => String(entry).trim()).filter(Boolean)
|
|
82
|
+
: [];
|
|
83
|
+
const senderId = msg.from?.id ? String(msg.from.id) : "";
|
|
84
|
+
const senderUsername = msg.from?.username ?? "";
|
|
85
|
+
const commandAuthorized = allowFromList.length === 0 ||
|
|
86
|
+
allowFromList.includes("*") ||
|
|
87
|
+
(senderId && allowFromList.includes(senderId)) ||
|
|
88
|
+
(senderId && allowFromList.includes(`telegram:${senderId}`)) ||
|
|
89
|
+
(senderUsername &&
|
|
90
|
+
allowFromList.some((entry) => entry.toLowerCase() === senderUsername.toLowerCase() ||
|
|
91
|
+
entry.toLowerCase() === `@${senderUsername.toLowerCase()}`));
|
|
79
92
|
const wasMentioned = Boolean(botUsername) && hasBotMention(msg, botUsername);
|
|
93
|
+
const hasAnyMention = (msg.entities ?? msg.caption_entities ?? []).some((ent) => ent.type === "mention");
|
|
94
|
+
const shouldBypassMention = isGroup &&
|
|
95
|
+
resolveGroupRequireMention(chatId) &&
|
|
96
|
+
!wasMentioned &&
|
|
97
|
+
!hasAnyMention &&
|
|
98
|
+
commandAuthorized &&
|
|
99
|
+
hasControlCommand(msg.text ?? msg.caption ?? "");
|
|
80
100
|
if (isGroup && resolveGroupRequireMention(chatId) && botUsername) {
|
|
81
|
-
if (!wasMentioned) {
|
|
101
|
+
if (!wasMentioned && !shouldBypassMention) {
|
|
82
102
|
logger.info({ chatId, reason: "no-mention" }, "skipping group message");
|
|
83
103
|
return;
|
|
84
104
|
}
|
|
@@ -109,6 +129,8 @@ export function createTelegramBot(opts) {
|
|
|
109
129
|
ChatType: isGroup ? "group" : "direct",
|
|
110
130
|
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
|
|
111
131
|
SenderName: buildSenderName(msg),
|
|
132
|
+
SenderId: senderId || undefined,
|
|
133
|
+
SenderUsername: senderUsername || undefined,
|
|
112
134
|
Surface: "telegram",
|
|
113
135
|
MessageSid: String(msg.message_id),
|
|
114
136
|
ReplyToId: replyTarget?.id,
|
|
@@ -119,6 +141,7 @@ export function createTelegramBot(opts) {
|
|
|
119
141
|
MediaPath: media?.path,
|
|
120
142
|
MediaType: media?.contentType,
|
|
121
143
|
MediaUrl: media?.path,
|
|
144
|
+
CommandAuthorized: commandAuthorized,
|
|
122
145
|
};
|
|
123
146
|
if (replyTarget && shouldLogVerbose()) {
|
|
124
147
|
const preview = replyTarget.body.replace(/\s+/g, " ").slice(0, 120);
|
package/dist/utils.js
CHANGED
|
@@ -94,6 +94,12 @@ export function resolveUserPath(input) {
|
|
|
94
94
|
}
|
|
95
95
|
return path.resolve(trimmed);
|
|
96
96
|
}
|
|
97
|
+
export function resolveConfigDir(env = process.env, homedir = os.homedir) {
|
|
98
|
+
const override = env.CLAWDBOT_STATE_DIR?.trim();
|
|
99
|
+
if (override)
|
|
100
|
+
return resolveUserPath(override);
|
|
101
|
+
return path.join(homedir(), ".clawdbot");
|
|
102
|
+
}
|
|
97
103
|
export function resolveHomeDir() {
|
|
98
104
|
const envHome = process.env.HOME?.trim();
|
|
99
105
|
if (envHome)
|
|
@@ -129,5 +135,5 @@ export function shortenHomeInString(input) {
|
|
|
129
135
|
return input;
|
|
130
136
|
return input.split(home).join("~");
|
|
131
137
|
}
|
|
132
|
-
//
|
|
133
|
-
export const CONFIG_DIR =
|
|
138
|
+
// Configuration root; can be overridden via CLAWDBOT_STATE_DIR.
|
|
139
|
+
export const CONFIG_DIR = resolveConfigDir();
|
package/dist/web/auto-reply.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
|
2
|
+
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
|
2
3
|
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
|
3
4
|
import { normalizeGroupActivation, parseActivationCommand, } from "../auto-reply/group-activation.js";
|
|
4
5
|
import { HEARTBEAT_PROMPT, stripHeartbeatToken, } from "../auto-reply/heartbeat.js";
|
|
@@ -592,32 +593,24 @@ export async function monitorWebProvider(verbose, listenerFactory = monitorWebIn
|
|
|
592
593
|
const defaultActivation = requireMention === false ? "always" : "mention";
|
|
593
594
|
return (normalizeGroupActivation(entry?.groupActivation) ?? defaultActivation);
|
|
594
595
|
};
|
|
595
|
-
const
|
|
596
|
+
const resolveCommandAllowFrom = () => {
|
|
596
597
|
const allowFrom = mentionConfig.allowFrom;
|
|
597
|
-
const raw = Array.isArray(allowFrom) && allowFrom.length > 0
|
|
598
|
-
? allowFrom
|
|
599
|
-
: selfE164
|
|
600
|
-
? [selfE164]
|
|
601
|
-
: [];
|
|
598
|
+
const raw = Array.isArray(allowFrom) && allowFrom.length > 0 ? allowFrom : [];
|
|
602
599
|
return raw
|
|
603
600
|
.filter((entry) => Boolean(entry && entry !== "*"))
|
|
604
601
|
.map((entry) => normalizeE164(entry))
|
|
605
602
|
.filter((entry) => Boolean(entry));
|
|
606
603
|
};
|
|
607
|
-
const
|
|
604
|
+
const isCommandAuthorized = (msg) => {
|
|
605
|
+
const allowFrom = resolveCommandAllowFrom();
|
|
606
|
+
if (allowFrom.length === 0)
|
|
607
|
+
return true;
|
|
608
|
+
if (mentionConfig.allowFrom?.includes("*"))
|
|
609
|
+
return true;
|
|
608
610
|
const sender = normalizeE164(msg.senderE164 ?? "");
|
|
609
611
|
if (!sender)
|
|
610
612
|
return false;
|
|
611
|
-
|
|
612
|
-
return owners.includes(sender);
|
|
613
|
-
};
|
|
614
|
-
const isStatusCommand = (body) => {
|
|
615
|
-
const trimmed = body.trim().toLowerCase();
|
|
616
|
-
if (!trimmed)
|
|
617
|
-
return false;
|
|
618
|
-
return (trimmed === "/status" ||
|
|
619
|
-
trimmed === "status" ||
|
|
620
|
-
trimmed.startsWith("/status "));
|
|
613
|
+
return allowFrom.includes(sender);
|
|
621
614
|
};
|
|
622
615
|
const stripMentionsForCommand = (text, selfE164) => {
|
|
623
616
|
let result = text;
|
|
@@ -888,6 +881,7 @@ export async function monitorWebProvider(verbose, listenerFactory = monitorWebIn
|
|
|
888
881
|
SenderName: msg.senderName,
|
|
889
882
|
SenderE164: msg.senderE164,
|
|
890
883
|
WasMentioned: msg.wasMentioned,
|
|
884
|
+
CommandAuthorized: isCommandAuthorized(msg),
|
|
891
885
|
Surface: "whatsapp",
|
|
892
886
|
}, {
|
|
893
887
|
onReplyStart: msg.sendComposing,
|
|
@@ -992,10 +986,13 @@ export async function monitorWebProvider(verbose, listenerFactory = monitorWebIn
|
|
|
992
986
|
noteGroupMember(conversationId, msg.senderE164, msg.senderName);
|
|
993
987
|
const commandBody = stripMentionsForCommand(msg.body, msg.selfE164);
|
|
994
988
|
const activationCommand = parseActivationCommand(commandBody);
|
|
995
|
-
const
|
|
996
|
-
const statusCommand =
|
|
997
|
-
const
|
|
998
|
-
|
|
989
|
+
const commandAuthorized = isCommandAuthorized(msg);
|
|
990
|
+
const statusCommand = hasControlCommand(commandBody);
|
|
991
|
+
const hasAnyMention = (msg.mentionedJids?.length ?? 0) > 0;
|
|
992
|
+
const shouldBypassMention = commandAuthorized &&
|
|
993
|
+
(activationCommand.hasCommand || statusCommand) &&
|
|
994
|
+
!hasAnyMention;
|
|
995
|
+
if (activationCommand.hasCommand && !commandAuthorized) {
|
|
999
996
|
logVerbose(`Ignoring /activation from non-owner in group ${conversationId}`);
|
|
1000
997
|
return;
|
|
1001
998
|
}
|
package/dist/web/inbound.js
CHANGED
|
@@ -58,7 +58,7 @@ export async function monitorWebInbox(options) {
|
|
|
58
58
|
}
|
|
59
59
|
};
|
|
60
60
|
const handleMessagesUpsert = async (upsert) => {
|
|
61
|
-
if (upsert.type !== "notify")
|
|
61
|
+
if (upsert.type !== "notify" && upsert.type !== "append")
|
|
62
62
|
return;
|
|
63
63
|
for (const msg of upsert.messages ?? []) {
|
|
64
64
|
const id = msg.key?.id ?? undefined;
|
|
@@ -129,6 +129,10 @@ export async function monitorWebInbox(options) {
|
|
|
129
129
|
// Self-chat mode: never auto-send read receipts (blue ticks) on behalf of the owner.
|
|
130
130
|
logVerbose(`Self-chat mode: skipping read receipt for ${id}`);
|
|
131
131
|
}
|
|
132
|
+
// If this is history/offline catch-up, we marked it as read above,
|
|
133
|
+
// but we skip triggering the auto-reply logic to avoid spamming old context.
|
|
134
|
+
if (upsert.type === "append")
|
|
135
|
+
continue;
|
|
132
136
|
let body = extractText(msg.message ?? undefined);
|
|
133
137
|
if (!body) {
|
|
134
138
|
body = extractMediaPlaceholder(msg.message ?? undefined);
|
package/dist/web/qr-image.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
1
|
import { deflateSync } from "node:zlib";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
2
|
+
import QRCodeModule from "qrcode-terminal/vendor/QRCode/index.js";
|
|
3
|
+
import QRErrorCorrectLevelModule from "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel";
|
|
4
|
+
const QRCode = QRCodeModule;
|
|
5
|
+
const QRErrorCorrectLevel = QRErrorCorrectLevelModule;
|
|
6
6
|
function createQrMatrix(input) {
|
|
7
7
|
const qr = new QRCode(-1, QRErrorCorrectLevel.L);
|
|
8
8
|
qr.addData(input);
|
package/dist/web/session.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
-
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { DisconnectReason, fetchLatestBaileysVersion, makeCacheableSignalKeyStore, makeWASocket, useMultiFileAuthState, } from "@whiskeysockets/baileys";
|
|
7
6
|
import qrcode from "qrcode-terminal";
|
|
@@ -9,10 +8,10 @@ import { resolveDefaultSessionStorePath } from "../config/sessions.js";
|
|
|
9
8
|
import { danger, info, success } from "../globals.js";
|
|
10
9
|
import { getChildLogger, toPinoLikeLogger } from "../logging.js";
|
|
11
10
|
import { defaultRuntime } from "../runtime.js";
|
|
12
|
-
import { CONFIG_DIR, ensureDir, jidToE164 } from "../utils.js";
|
|
11
|
+
import { CONFIG_DIR, ensureDir, jidToE164, resolveConfigDir, } from "../utils.js";
|
|
13
12
|
import { VERSION } from "../version.js";
|
|
14
13
|
export function resolveWebAuthDir() {
|
|
15
|
-
return path.join(
|
|
14
|
+
return path.join(resolveConfigDir(), "credentials");
|
|
16
15
|
}
|
|
17
16
|
function resolveWebCredsPath() {
|
|
18
17
|
return path.join(resolveWebAuthDir(), "creds.json");
|
package/docs/agent.md
CHANGED
|
@@ -3,7 +3,6 @@ summary: "Agent runtime (embedded p-mono), workspace contract, and session boots
|
|
|
3
3
|
read_when:
|
|
4
4
|
- Changing agent runtime, workspace bootstrap, or session behavior
|
|
5
5
|
---
|
|
6
|
-
<!-- {% raw %} -->
|
|
7
6
|
# Agent Runtime 🤖
|
|
8
7
|
|
|
9
8
|
CLAWDBOT runs a single embedded agent runtime derived from **p-mono** (internal name: **p**).
|
|
@@ -101,4 +100,3 @@ At minimum, set:
|
|
|
101
100
|
---
|
|
102
101
|
|
|
103
102
|
*Next: [Group Chats](./group-messages.md)* 🦞
|
|
104
|
-
<!-- {% endraw %} -->
|
package/docs/assets/markdown.css
CHANGED
package/docs/audio.md
CHANGED
|
@@ -3,7 +3,6 @@ summary: "How inbound audio/voice notes are downloaded, transcribed, and injecte
|
|
|
3
3
|
read_when:
|
|
4
4
|
- Changing audio transcription or media handling
|
|
5
5
|
---
|
|
6
|
-
<!-- {% raw %} -->
|
|
7
6
|
# Audio / Voice Notes — 2025-12-05
|
|
8
7
|
|
|
9
8
|
## What works
|
|
@@ -47,4 +46,3 @@ Requires `OPENAI_API_KEY` in env and `openai` CLI installed:
|
|
|
47
46
|
## Gotchas
|
|
48
47
|
- Ensure your CLI exits 0 and prints plain text; JSON needs to be massaged via `jq -r .text`.
|
|
49
48
|
- Keep timeouts reasonable (`timeoutSeconds`, default 45s) to avoid blocking the reply queue.
|
|
50
|
-
<!-- {% endraw %} -->
|
package/docs/clawd.md
CHANGED
|
@@ -4,7 +4,6 @@ read_when:
|
|
|
4
4
|
- Onboarding a new assistant instance
|
|
5
5
|
- Reviewing safety/permission implications
|
|
6
6
|
---
|
|
7
|
-
<!-- {% raw %} -->
|
|
8
7
|
# Building a personal assistant with CLAWDBOT (Clawd-style)
|
|
9
8
|
|
|
10
9
|
CLAWDBOT is a WhatsApp + Telegram + Discord gateway for **Pi** agents. This guide is the “personal assistant” setup: one dedicated WhatsApp number that behaves like your always-on agent.
|
|
@@ -196,4 +195,3 @@ Logs live under `/tmp/clawdbot/` (default: `clawdbot-YYYY-MM-DD.log`).
|
|
|
196
195
|
- Cron + wakeups: [Cron + wakeups](./cron.md)
|
|
197
196
|
- macOS menu bar companion: [Clawdbot macOS app](./clawdbot-mac.md)
|
|
198
197
|
- Security: [Security](./security.md)
|
|
199
|
-
<!-- {% endraw %} -->
|
package/docs/configuration.md
CHANGED
|
@@ -3,7 +3,6 @@ summary: "All configuration options for ~/.clawdbot/clawdbot.json with examples"
|
|
|
3
3
|
read_when:
|
|
4
4
|
- Adding or modifying config fields
|
|
5
5
|
---
|
|
6
|
-
<!-- {% raw %} -->
|
|
7
6
|
# Configuration 🔧
|
|
8
7
|
|
|
9
8
|
CLAWDBOT reads an optional **JSON5** config from `~/.clawdbot/clawdbot.json` (comments + trailing commas allowed).
|
|
@@ -60,6 +59,36 @@ To prevent the bot from responding to WhatsApp @-mentions in groups (only respon
|
|
|
60
59
|
|
|
61
60
|
## Common options
|
|
62
61
|
|
|
62
|
+
### Env vars + `.env`
|
|
63
|
+
|
|
64
|
+
CLAWDBOT reads env vars from the parent process (shell, launchd/systemd, CI, etc.).
|
|
65
|
+
|
|
66
|
+
Additionally, it loads:
|
|
67
|
+
- `.env` from the current working directory (if present)
|
|
68
|
+
- a global fallback `.env` from `~/.clawdbot/.env` (aka `$CLAWDBOT_STATE_DIR/.env`)
|
|
69
|
+
|
|
70
|
+
Neither `.env` file overrides existing env vars.
|
|
71
|
+
|
|
72
|
+
### `env.shellEnv` (optional)
|
|
73
|
+
|
|
74
|
+
Opt-in convenience: if enabled and none of the expected keys are set yet, CLAWDBOT runs your login shell and imports only the missing expected keys (never overrides).
|
|
75
|
+
This effectively sources your shell profile.
|
|
76
|
+
|
|
77
|
+
```json5
|
|
78
|
+
{
|
|
79
|
+
env: {
|
|
80
|
+
shellEnv: {
|
|
81
|
+
enabled: true,
|
|
82
|
+
timeoutMs: 15000
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Env var equivalent:
|
|
89
|
+
- `CLAWDBOT_LOAD_SHELL_ENV=1`
|
|
90
|
+
- `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`
|
|
91
|
+
|
|
63
92
|
### `identity`
|
|
64
93
|
|
|
65
94
|
Optional agent identity used for defaults and UX. This is written by the macOS onboarding assistant.
|
|
@@ -434,6 +463,17 @@ Controls the embedded agent runtime (model/thinking/verbose/timeouts).
|
|
|
434
463
|
`imageModel` selects an image-capable model for the `image` tool.
|
|
435
464
|
`imageModelFallbacks` lists ordered fallback image models for the `image` tool.
|
|
436
465
|
|
|
466
|
+
Clawdbot also ships a few built-in `modelAliases` shorthands (when an `agent` section exists):
|
|
467
|
+
|
|
468
|
+
- `opus` -> `anthropic/claude-opus-4-5`
|
|
469
|
+
- `sonnet` -> `anthropic/claude-sonnet-4-5`
|
|
470
|
+
- `gpt` -> `openai/gpt-5.2`
|
|
471
|
+
- `gpt-mini` -> `openai/gpt-5-mini`
|
|
472
|
+
- `gemini` -> `google/gemini-3-pro-preview`
|
|
473
|
+
- `gemini-flash` -> `google/gemini-3-flash-preview`
|
|
474
|
+
|
|
475
|
+
If you configure the same alias name (case-insensitive) yourself, your value wins (defaults never override).
|
|
476
|
+
|
|
437
477
|
```json5
|
|
438
478
|
{
|
|
439
479
|
agent: {
|
|
@@ -508,6 +548,20 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
|
|
|
508
548
|
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
|
|
509
549
|
- `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000)
|
|
510
550
|
|
|
551
|
+
`agent.tools` configures a global tool allow/deny policy (deny wins).
|
|
552
|
+
This is applied even when the Docker sandbox is **off**.
|
|
553
|
+
|
|
554
|
+
Example (disable browser/canvas everywhere):
|
|
555
|
+
```json5
|
|
556
|
+
{
|
|
557
|
+
agent: {
|
|
558
|
+
tools: {
|
|
559
|
+
deny: ["browser", "canvas"]
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
511
565
|
`agent.elevated` controls elevated (host) bash access:
|
|
512
566
|
- `enabled`: allow elevated mode (default true)
|
|
513
567
|
- `allowFrom`: per-surface allowlists (empty = disabled)
|
|
@@ -962,10 +1016,16 @@ Requires full Gateway restart:
|
|
|
962
1016
|
|
|
963
1017
|
To run multiple gateways on one host, isolate per-instance state + config and use unique ports:
|
|
964
1018
|
- `CLAWDBOT_CONFIG_PATH` (per-instance config)
|
|
965
|
-
- `CLAWDBOT_STATE_DIR` (sessions/creds
|
|
1019
|
+
- `CLAWDBOT_STATE_DIR` (sessions/creds)
|
|
966
1020
|
- `agent.workspace` (memories)
|
|
967
1021
|
- `gateway.port` (unique per instance)
|
|
968
1022
|
|
|
1023
|
+
Convenience flags (CLI):
|
|
1024
|
+
- `clawdbot --dev …` → uses `~/.clawdbot-dev` + shifts ports from base `19001`
|
|
1025
|
+
- `clawdbot --profile <name> …` → uses `~/.clawdbot-<name>` (port via config/env/flags)
|
|
1026
|
+
|
|
1027
|
+
See `docs/gateway.md` for the derived port mapping (gateway/bridge/browser/canvas).
|
|
1028
|
+
|
|
969
1029
|
Example:
|
|
970
1030
|
```bash
|
|
971
1031
|
CLAWDBOT_CONFIG_PATH=~/.clawdbot/a.json \
|
|
@@ -1174,4 +1234,3 @@ Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See [Cron + wa
|
|
|
1174
1234
|
---
|
|
1175
1235
|
|
|
1176
1236
|
*Next: [Agent Runtime](./agent.md)* 🦞
|
|
1177
|
-
<!-- {% endraw %} -->
|
package/docs/docs.json
CHANGED
|
@@ -27,11 +27,19 @@
|
|
|
27
27
|
"pages": [
|
|
28
28
|
"index",
|
|
29
29
|
"onboarding",
|
|
30
|
-
"wizard",
|
|
31
30
|
"clawd",
|
|
32
31
|
"faq"
|
|
33
32
|
]
|
|
34
33
|
},
|
|
34
|
+
{
|
|
35
|
+
"group": "Installation",
|
|
36
|
+
"pages": [
|
|
37
|
+
"wizard",
|
|
38
|
+
"nix",
|
|
39
|
+
"docker",
|
|
40
|
+
"setup"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
35
43
|
{
|
|
36
44
|
"group": "Core Concepts",
|
|
37
45
|
"pages": [
|