clawdbot 2026.1.4 → 2026.1.5

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +26 -1
  3. package/dist/agents/pi-embedded-runner.js +2 -0
  4. package/dist/agents/pi-embedded-subscribe.js +18 -3
  5. package/dist/agents/pi-tools.js +45 -6
  6. package/dist/agents/tools/browser-tool.js +38 -89
  7. package/dist/agents/tools/cron-tool.js +8 -8
  8. package/dist/agents/workspace.js +8 -1
  9. package/dist/auto-reply/command-detection.js +26 -0
  10. package/dist/auto-reply/reply/agent-runner.js +15 -8
  11. package/dist/auto-reply/reply/commands.js +36 -25
  12. package/dist/auto-reply/reply/directive-handling.js +4 -2
  13. package/dist/auto-reply/reply/directives.js +12 -0
  14. package/dist/auto-reply/reply/session-updates.js +2 -4
  15. package/dist/auto-reply/reply.js +26 -4
  16. package/dist/browser/config.js +22 -4
  17. package/dist/browser/profiles-service.js +3 -1
  18. package/dist/browser/profiles.js +14 -3
  19. package/dist/canvas-host/a2ui/.bundle.hash +2 -0
  20. package/dist/cli/gateway-cli.js +2 -2
  21. package/dist/cli/profile.js +81 -0
  22. package/dist/cli/program.js +10 -1
  23. package/dist/cli/run-main.js +33 -0
  24. package/dist/commands/configure.js +5 -0
  25. package/dist/commands/onboard-providers.js +1 -1
  26. package/dist/commands/setup.js +4 -1
  27. package/dist/config/defaults.js +56 -0
  28. package/dist/config/io.js +47 -6
  29. package/dist/config/paths.js +2 -2
  30. package/dist/config/port-defaults.js +32 -0
  31. package/dist/config/sessions.js +3 -2
  32. package/dist/config/validation.js +2 -2
  33. package/dist/config/zod-schema.js +16 -0
  34. package/dist/discord/monitor.js +75 -266
  35. package/dist/entry.js +16 -0
  36. package/dist/gateway/call.js +8 -1
  37. package/dist/gateway/server-methods/chat.js +1 -1
  38. package/dist/gateway/server.js +14 -3
  39. package/dist/index.js +2 -2
  40. package/dist/infra/control-ui-assets.js +118 -0
  41. package/dist/infra/dotenv.js +15 -0
  42. package/dist/infra/shell-env.js +79 -0
  43. package/dist/infra/system-events.js +50 -23
  44. package/dist/macos/relay.js +8 -2
  45. package/dist/telegram/bot.js +24 -1
  46. package/dist/utils.js +8 -2
  47. package/dist/web/auto-reply.js +18 -21
  48. package/dist/web/inbound.js +5 -1
  49. package/dist/web/qr-image.js +4 -4
  50. package/dist/web/session.js +2 -3
  51. package/docs/agent.md +0 -2
  52. package/docs/assets/markdown.css +4 -1
  53. package/docs/audio.md +0 -2
  54. package/docs/clawd.md +0 -2
  55. package/docs/configuration.md +62 -3
  56. package/docs/docs.json +9 -1
  57. package/docs/faq.md +32 -7
  58. package/docs/gateway.md +28 -0
  59. package/docs/images.md +0 -2
  60. package/docs/index.md +2 -4
  61. package/docs/mac/icon.md +1 -1
  62. package/docs/nix.md +57 -11
  63. package/docs/onboarding.md +0 -2
  64. package/docs/refactor/webagent-session.md +0 -2
  65. package/docs/research/memory.md +1 -1
  66. package/docs/skills.md +0 -2
  67. package/docs/templates/AGENTS.md +2 -2
  68. package/docs/tools.md +15 -0
  69. package/docs/whatsapp.md +2 -0
  70. package/package.json +8 -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 main-session prompt/heartbeat. We intentionally avoid
3
- // persistence to keep events ephemeral.
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 queue = [];
6
- let lastText = null;
7
- let lastContextKey = null;
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 out = queue.map((e) => e.text);
34
- queue.length = 0;
35
- lastText = null;
36
- lastContextKey = null;
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
- return queue.map((e) => e.text);
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 hasSystemEvents() {
43
- return queue.length > 0;
69
+ export function resetSystemEventsForTest() {
70
+ queues.clear();
44
71
  }
@@ -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 { default: dotenv } = await import("dotenv");
27
- dotenv.config({ quiet: true });
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");
@@ -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
- // Fixed configuration root; legacy ~/.clawdbot is no longer used.
133
- export const CONFIG_DIR = path.join(os.homedir(), ".clawdbot");
138
+ // Configuration root; can be overridden via CLAWDBOT_STATE_DIR.
139
+ export const CONFIG_DIR = resolveConfigDir();
@@ -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 resolveOwnerList = (selfE164) => {
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 isOwnerSender = (msg) => {
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
- const owners = resolveOwnerList(msg.selfE164 ?? undefined);
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 isOwner = isOwnerSender(msg);
996
- const statusCommand = isStatusCommand(commandBody);
997
- const shouldBypassMention = isOwner && (activationCommand.hasCommand || statusCommand);
998
- if (activationCommand.hasCommand && !isOwner) {
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
  }
@@ -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);
@@ -1,8 +1,8 @@
1
- import { createRequire } from "node:module";
2
1
  import { deflateSync } from "node:zlib";
3
- const require = createRequire(import.meta.url);
4
- const QRCode = require("qrcode-terminal/vendor/QRCode");
5
- const QRErrorCorrectLevel = require("qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel");
2
+ import QRCodeModule from "qrcode-terminal/vendor/QRCode";
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);
@@ -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(os.homedir(), ".clawdbot", "credentials");
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 %} -->
@@ -3,6 +3,10 @@
3
3
  line-height: 1.7;
4
4
  }
5
5
 
6
+ .mdx-content > h1:first-of-type {
7
+ display: none;
8
+ }
9
+
6
10
  .markdown h1,
7
11
  .markdown h2,
8
12
  .markdown h3,
@@ -127,4 +131,3 @@
127
131
  font-family: var(--font-pixel);
128
132
  letter-spacing: 0.06em;
129
133
  }
130
-
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 %} -->
@@ -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/logs)
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": [
package/docs/faq.md CHANGED
@@ -118,6 +118,24 @@ pnpm clawdbot login
118
118
 
119
119
  **If OAuth fails** (headless/container): Do OAuth on a normal machine, then copy `~/.clawdbot/` to your server. The auth is just a JSON file.
120
120
 
121
+ ### How are env vars loaded?
122
+
123
+ CLAWDBOT reads env vars from the parent process (shell, launchd/systemd, CI, etc.). It also loads `.env` files:
124
+ - `.env` in the current working directory
125
+ - global fallback: `~/.clawdbot/.env` (aka `$CLAWDBOT_STATE_DIR/.env`)
126
+
127
+ Neither `.env` file overrides existing env vars.
128
+
129
+ Optional convenience: import missing expected keys from your login shell env (sources your shell profile):
130
+
131
+ ```json5
132
+ {
133
+ env: { shellEnv: { enabled: true, timeoutMs: 15000 } }
134
+ }
135
+ ```
136
+
137
+ Or set `CLAWDBOT_LOAD_SHELL_ENV=1` (timeout: `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`).
138
+
121
139
  ### Does enterprise OAuth work?
122
140
 
123
141
  **Not currently.** Enterprise accounts use SSO which requires a different auth flow that pi-coding-agent doesn't support yet.
@@ -530,23 +548,30 @@ Use `/model` to switch without restarting:
530
548
  /model sonnet
531
549
  /model haiku
532
550
  /model opus
551
+ /model gpt
552
+ /model gpt-mini
553
+ /model gemini
554
+ /model gemini-flash
533
555
  ```
534
556
 
557
+ Clawdbot ships a few default model shorthands (you can override them in config):
558
+ `opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`.
559
+
535
560
  **Setup:** Configure allowed models and aliases in `clawdbot.json`:
536
561
 
537
562
  ```json
538
563
  {
539
564
  "agent": {
540
- "model": "anthropic/claude-opus-4-5-20251022",
565
+ "model": "anthropic/claude-opus-4-5",
541
566
  "allowedModels": [
542
- "anthropic/claude-opus-4-5-20251022",
543
- "anthropic/claude-sonnet-4-5-20251022",
544
- "anthropic/claude-haiku-4-5-20251001"
567
+ "anthropic/claude-opus-4-5",
568
+ "anthropic/claude-sonnet-4-5",
569
+ "anthropic/claude-haiku-4-5"
545
570
  ],
546
571
  "modelAliases": {
547
- "opus": "anthropic/claude-opus-4-5-20251022",
548
- "sonnet": "anthropic/claude-sonnet-4-5-20251022",
549
- "haiku": "anthropic/claude-haiku-4-5-20251001"
572
+ "opus": "anthropic/claude-opus-4-5",
573
+ "sonnet": "anthropic/claude-sonnet-4-5",
574
+ "haiku": "anthropic/claude-haiku-4-5"
550
575
  }
551
576
  }
552
577
  }