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
@@ -11,7 +11,7 @@ import { buildStatusMessage } from "../status.js";
11
11
  import { isAbortTrigger, setAbortMemory } from "./abort.js";
12
12
  import { stripMentions } from "./mentions.js";
13
13
  export function buildCommandContext(params) {
14
- const { ctx, cfg, sessionKey, isGroup, triggerBodyNormalized } = params;
14
+ const { ctx, cfg, sessionKey, isGroup, triggerBodyNormalized, commandAuthorized, } = params;
15
15
  const surface = (ctx.Surface ?? "").trim().toLowerCase();
16
16
  const isWhatsAppSurface = surface === "whatsapp" ||
17
17
  (ctx.From ?? "").startsWith("whatsapp:") ||
@@ -21,35 +21,35 @@ export function buildCommandContext(params) {
21
21
  : undefined;
22
22
  const from = (ctx.From ?? "").replace(/^whatsapp:/, "");
23
23
  const to = (ctx.To ?? "").replace(/^whatsapp:/, "");
24
- const defaultAllowFrom = isWhatsAppSurface &&
25
- (!configuredAllowFrom || configuredAllowFrom.length === 0) &&
26
- to
27
- ? [to]
28
- : undefined;
29
- const allowFrom = configuredAllowFrom && configuredAllowFrom.length > 0
30
- ? configuredAllowFrom
31
- : defaultAllowFrom;
24
+ const allowFromList = configuredAllowFrom?.filter((entry) => entry?.trim()) ?? [];
25
+ const allowAll = !isWhatsAppSurface ||
26
+ allowFromList.length === 0 ||
27
+ allowFromList.some((entry) => entry.trim() === "*");
32
28
  const abortKey = sessionKey ?? (from || undefined) ?? (to || undefined);
33
29
  const rawBodyNormalized = triggerBodyNormalized;
34
30
  const commandBodyNormalized = isGroup
35
31
  ? stripMentions(rawBodyNormalized, ctx, cfg)
36
32
  : rawBodyNormalized;
37
33
  const senderE164 = normalizeE164(ctx.SenderE164 ?? "");
38
- const ownerCandidates = isWhatsAppSurface
39
- ? (allowFrom ?? []).filter((entry) => entry && entry !== "*")
34
+ const ownerCandidates = isWhatsAppSurface && !allowAll
35
+ ? allowFromList.filter((entry) => entry !== "*")
40
36
  : [];
41
- if (isWhatsAppSurface && ownerCandidates.length === 0 && to) {
37
+ if (isWhatsAppSurface && !allowAll && ownerCandidates.length === 0 && to) {
42
38
  ownerCandidates.push(to);
43
39
  }
44
40
  const ownerList = ownerCandidates
45
41
  .map((entry) => normalizeE164(entry))
46
42
  .filter((entry) => Boolean(entry));
47
- const isOwnerSender = Boolean(senderE164) && ownerList.includes(senderE164 ?? "");
43
+ const isOwner = !isWhatsAppSurface ||
44
+ allowAll ||
45
+ ownerList.length === 0 ||
46
+ (senderE164 ? ownerList.includes(senderE164) : false);
47
+ const isAuthorizedSender = commandAuthorized && isOwner;
48
48
  return {
49
49
  surface,
50
50
  isWhatsAppSurface,
51
51
  ownerList,
52
- isOwnerSender,
52
+ isAuthorizedSender,
53
53
  senderE164: senderE164 || undefined,
54
54
  abortKey,
55
55
  rawBodyNormalized,
@@ -59,7 +59,7 @@ export function buildCommandContext(params) {
59
59
  };
60
60
  }
61
61
  export async function handleCommands(params) {
62
- const { cfg, command, sessionEntry, sessionStore, sessionKey, storePath, sessionScope, workspaceDir, defaultGroupActivation, resolvedThinkLevel, resolvedVerboseLevel, resolveDefaultThinkingLevel, model, contextTokens, isGroup, } = params;
62
+ const { cfg, command, directives, sessionEntry, sessionStore, sessionKey, storePath, sessionScope, workspaceDir, defaultGroupActivation, resolvedThinkLevel, resolvedVerboseLevel, resolveDefaultThinkingLevel, model, contextTokens, isGroup, } = params;
63
63
  const activationCommand = parseActivationCommand(command.commandBodyNormalized);
64
64
  const sendPolicyCommand = parseSendPolicyCommand(command.commandBodyNormalized);
65
65
  if (activationCommand.hasCommand) {
@@ -69,8 +69,17 @@ export async function handleCommands(params) {
69
69
  reply: { text: "⚙️ Group activation only applies to group chats." },
70
70
  };
71
71
  }
72
- if (!command.isOwnerSender) {
73
- logVerbose(`Ignoring /activation from non-owner in group: ${command.senderE164 || "<unknown>"}`);
72
+ const activationOwnerList = command.ownerList;
73
+ const activationSenderE164 = command.senderE164
74
+ ? normalizeE164(command.senderE164)
75
+ : "";
76
+ const isActivationOwner = !command.isWhatsAppSurface || activationOwnerList.length === 0
77
+ ? command.isAuthorizedSender
78
+ : Boolean(activationSenderE164) &&
79
+ activationOwnerList.includes(activationSenderE164);
80
+ if (!command.isAuthorizedSender ||
81
+ (command.isWhatsAppSurface && !isActivationOwner)) {
82
+ logVerbose(`Ignoring /activation from unauthorized sender in group: ${command.senderE164 || "<unknown>"}`);
74
83
  return { shouldContinue: false };
75
84
  }
76
85
  if (!activationCommand.mode) {
@@ -94,8 +103,8 @@ export async function handleCommands(params) {
94
103
  };
95
104
  }
96
105
  if (sendPolicyCommand.hasCommand) {
97
- if (!command.isOwnerSender) {
98
- logVerbose(`Ignoring /send from non-owner: ${command.senderE164 || "<unknown>"}`);
106
+ if (!command.isAuthorizedSender) {
107
+ logVerbose(`Ignoring /send from unauthorized sender: ${command.senderE164 || "<unknown>"}`);
99
108
  return { shouldContinue: false };
100
109
  }
101
110
  if (!sendPolicyCommand.mode) {
@@ -130,8 +139,8 @@ export async function handleCommands(params) {
130
139
  if (command.commandBodyNormalized === "/restart" ||
131
140
  command.commandBodyNormalized === "restart" ||
132
141
  command.commandBodyNormalized.startsWith("/restart ")) {
133
- if (isGroup && !command.isOwnerSender) {
134
- logVerbose(`Ignoring /restart from non-owner in group: ${command.senderE164 || "<unknown>"}`);
142
+ if (!command.isAuthorizedSender) {
143
+ logVerbose(`Ignoring /restart from unauthorized sender: ${command.senderE164 || "<unknown>"}`);
135
144
  return { shouldContinue: false };
136
145
  }
137
146
  const restartMethod = triggerClawdbotRestart();
@@ -142,11 +151,13 @@ export async function handleCommands(params) {
142
151
  },
143
152
  };
144
153
  }
145
- if (command.commandBodyNormalized === "/status" ||
154
+ const statusRequested = directives.hasStatusDirective ||
155
+ command.commandBodyNormalized === "/status" ||
146
156
  command.commandBodyNormalized === "status" ||
147
- command.commandBodyNormalized.startsWith("/status ")) {
148
- if (isGroup && !command.isOwnerSender) {
149
- logVerbose(`Ignoring /status from non-owner in group: ${command.senderE164 || "<unknown>"}`);
157
+ command.commandBodyNormalized.startsWith("/status ");
158
+ if (statusRequested) {
159
+ if (!command.isAuthorizedSender) {
160
+ logVerbose(`Ignoring /status from unauthorized sender: ${command.senderE164 || "<unknown>"}`);
150
161
  return { shouldContinue: false };
151
162
  }
152
163
  const webLinked = await webAuthExists();
@@ -4,7 +4,7 @@ import { buildModelAliasIndex, modelKey, resolveConfiguredModelRef, resolveModel
4
4
  import { saveSessionStore } from "../../config/sessions.js";
5
5
  import { enqueueSystemEvent } from "../../infra/system-events.js";
6
6
  import { extractModelDirective } from "../model.js";
7
- import { extractElevatedDirective, extractThinkDirective, extractVerboseDirective, } from "./directives.js";
7
+ import { extractElevatedDirective, extractStatusDirective, extractThinkDirective, extractVerboseDirective, } from "./directives.js";
8
8
  import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
9
9
  import { resolveModelDirectiveSelection, } from "./model-selection.js";
10
10
  import { extractQueueDirective, } from "./queue.js";
@@ -13,7 +13,8 @@ export function parseInlineDirectives(body) {
13
13
  const { cleaned: thinkCleaned, thinkLevel, rawLevel: rawThinkLevel, hasDirective: hasThinkDirective, } = extractThinkDirective(body);
14
14
  const { cleaned: verboseCleaned, verboseLevel, rawLevel: rawVerboseLevel, hasDirective: hasVerboseDirective, } = extractVerboseDirective(thinkCleaned);
15
15
  const { cleaned: elevatedCleaned, elevatedLevel, rawLevel: rawElevatedLevel, hasDirective: hasElevatedDirective, } = extractElevatedDirective(verboseCleaned);
16
- const { cleaned: modelCleaned, rawModel, hasDirective: hasModelDirective, } = extractModelDirective(elevatedCleaned);
16
+ const { cleaned: statusCleaned, hasDirective: hasStatusDirective } = extractStatusDirective(elevatedCleaned);
17
+ const { cleaned: modelCleaned, rawModel, hasDirective: hasModelDirective, } = extractModelDirective(statusCleaned);
17
18
  const { cleaned: queueCleaned, queueMode, queueReset, rawMode, debounceMs, cap, dropPolicy, rawDebounce, rawCap, rawDrop, hasDirective: hasQueueDirective, hasOptions: hasQueueOptions, } = extractQueueDirective(modelCleaned);
18
19
  return {
19
20
  cleaned: queueCleaned,
@@ -26,6 +27,7 @@ export function parseInlineDirectives(body) {
26
27
  hasElevatedDirective,
27
28
  elevatedLevel,
28
29
  rawElevatedLevel,
30
+ hasStatusDirective,
29
31
  hasModelDirective,
30
32
  rawModelDirective: rawModel,
31
33
  hasQueueDirective,
@@ -45,3 +45,15 @@ export function extractElevatedDirective(body) {
45
45
  hasDirective: !!match,
46
46
  };
47
47
  }
48
+ export function extractStatusDirective(body) {
49
+ if (!body)
50
+ return { cleaned: "", hasDirective: false };
51
+ const match = body.match(/(?:^|\s)\/status(?=$|\s|:)\b/i);
52
+ const cleaned = match
53
+ ? body.replace(match[0], "").replace(/\s+/g, " ").trim()
54
+ : body.trim();
55
+ return {
56
+ cleaned,
57
+ hasDirective: !!match,
58
+ };
59
+ }
@@ -4,8 +4,6 @@ import { saveSessionStore } from "../../config/sessions.js";
4
4
  import { buildProviderSummary } from "../../infra/provider-summary.js";
5
5
  import { drainSystemEvents } from "../../infra/system-events.js";
6
6
  export async function prependSystemEvents(params) {
7
- if (!params.isMainSession)
8
- return params.prefixedBodyBase;
9
7
  const compactSystemEvent = (line) => {
10
8
  const trimmed = line.trim();
11
9
  if (!trimmed)
@@ -21,9 +19,9 @@ export async function prependSystemEvents(params) {
21
19
  return trimmed;
22
20
  };
23
21
  const systemLines = [];
24
- const queued = drainSystemEvents();
22
+ const queued = drainSystemEvents(params.sessionKey);
25
23
  systemLines.push(...queued.map(compactSystemEvent).filter((v) => Boolean(v)));
26
- if (params.isNewSession) {
24
+ if (params.isMainSession && params.isNewSession) {
27
25
  const summary = await buildProviderSummary(params.cfg);
28
26
  if (summary.length > 0)
29
27
  systemLines.unshift(...summary);
@@ -7,6 +7,7 @@ import { resolveSessionTranscriptPath } from "../config/sessions.js";
7
7
  import { logVerbose } from "../globals.js";
8
8
  import { clearCommandLane, getQueueSize } from "../process/command-queue.js";
9
9
  import { defaultRuntime } from "../runtime.js";
10
+ import { hasControlCommand } from "./command-detection.js";
10
11
  import { getAbortMemory } from "./reply/abort.js";
11
12
  import { runReplyAgent } from "./reply/agent-runner.js";
12
13
  import { resolveBlockStreamingChunking } from "./reply/block-streaming.js";
@@ -171,9 +172,22 @@ export async function getReplyFromConfig(ctx, opts, configOverride) {
171
172
  }
172
173
  const sessionState = await initSessionState({ ctx, cfg });
173
174
  let { sessionCtx, sessionEntry, sessionStore, sessionKey, sessionId, isNewSession, systemSent, abortedLastRun, storePath, sessionScope, groupResolution, isGroup, triggerBodyNormalized, } = sessionState;
174
- const directives = parseInlineDirectives(sessionCtx.BodyStripped ?? sessionCtx.Body ?? "");
175
- sessionCtx.Body = directives.cleaned;
176
- sessionCtx.BodyStripped = directives.cleaned;
175
+ const rawBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
176
+ const commandAuthorized = ctx.CommandAuthorized ?? true;
177
+ const parsedDirectives = parseInlineDirectives(rawBody);
178
+ const directives = commandAuthorized
179
+ ? parsedDirectives
180
+ : {
181
+ ...parsedDirectives,
182
+ hasThinkDirective: false,
183
+ hasVerboseDirective: false,
184
+ hasStatusDirective: false,
185
+ hasModelDirective: false,
186
+ hasQueueDirective: false,
187
+ queueReset: false,
188
+ };
189
+ sessionCtx.Body = parsedDirectives.cleaned;
190
+ sessionCtx.BodyStripped = parsedDirectives.cleaned;
177
191
  const surfaceKey = sessionCtx.Surface?.trim().toLowerCase() ??
178
192
  ctx.Surface?.trim().toLowerCase() ??
179
193
  "";
@@ -314,6 +328,7 @@ export async function getReplyFromConfig(ctx, opts, configOverride) {
314
328
  sessionKey,
315
329
  isGroup,
316
330
  triggerBodyNormalized,
331
+ commandAuthorized,
317
332
  });
318
333
  const isEmptyConfig = Object.keys(cfg).length === 0;
319
334
  if (command.isWhatsAppSurface &&
@@ -331,6 +346,7 @@ export async function getReplyFromConfig(ctx, opts, configOverride) {
331
346
  ctx,
332
347
  cfg,
333
348
  command,
349
+ directives,
334
350
  sessionEntry,
335
351
  sessionStore,
336
352
  sessionKey,
@@ -353,7 +369,8 @@ export async function getReplyFromConfig(ctx, opts, configOverride) {
353
369
  const isFirstTurnInSession = isNewSession || !systemSent;
354
370
  const isGroupChat = sessionCtx.ChatType === "group";
355
371
  const wasMentioned = ctx.WasMentioned === true;
356
- const shouldEagerType = !isGroupChat || wasMentioned;
372
+ const isHeartbeat = opts?.isHeartbeat === true;
373
+ const shouldEagerType = (!isGroupChat || wasMentioned) && !isHeartbeat;
357
374
  const shouldInjectGroupIntro = Boolean(isGroupChat &&
358
375
  (isFirstTurnInSession || sessionEntry?.groupActivationNeedsSystemIntro));
359
376
  const groupIntro = shouldInjectGroupIntro
@@ -367,6 +384,10 @@ export async function getReplyFromConfig(ctx, opts, configOverride) {
367
384
  const baseBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
368
385
  const rawBodyTrimmed = (ctx.Body ?? "").trim();
369
386
  const baseBodyTrimmedRaw = baseBody.trim();
387
+ if (!commandAuthorized && !baseBodyTrimmedRaw && hasControlCommand(rawBody)) {
388
+ typing.cleanup();
389
+ return undefined;
390
+ }
370
391
  const isBareSessionReset = isNewSession &&
371
392
  baseBodyTrimmedRaw.length === 0 &&
372
393
  rawBodyTrimmed.length > 0;
@@ -396,6 +417,7 @@ export async function getReplyFromConfig(ctx, opts, configOverride) {
396
417
  const isMainSession = !isGroupSession && sessionKey === (sessionCfg?.mainKey ?? "main");
397
418
  prefixedBodyBase = await prependSystemEvents({
398
419
  cfg,
420
+ sessionKey,
399
421
  isMainSession,
400
422
  isNewSession,
401
423
  prefixedBodyBase,
@@ -1,3 +1,4 @@
1
+ import { deriveDefaultBrowserCdpPortRange, deriveDefaultBrowserControlPort, } from "../config/port-defaults.js";
1
2
  import { DEFAULT_CLAWD_BROWSER_COLOR, DEFAULT_CLAWD_BROWSER_CONTROL_URL, DEFAULT_CLAWD_BROWSER_ENABLED, DEFAULT_CLAWD_BROWSER_PROFILE_NAME, } from "./constants.js";
2
3
  import { CDP_PORT_RANGE_START } from "./profiles.js";
3
4
  function isLoopbackHost(host) {
@@ -43,11 +44,11 @@ export function parseHttpUrl(raw, label) {
43
44
  * Ensure the default "clawd" profile exists in the profiles map.
44
45
  * Auto-creates it with the legacy CDP port (from browser.cdpUrl) or first port if missing.
45
46
  */
46
- function ensureDefaultProfile(profiles, defaultColor, legacyCdpPort) {
47
+ function ensureDefaultProfile(profiles, defaultColor, legacyCdpPort, derivedDefaultCdpPort) {
47
48
  const result = { ...profiles };
48
49
  if (!result[DEFAULT_CLAWD_BROWSER_PROFILE_NAME]) {
49
50
  result[DEFAULT_CLAWD_BROWSER_PROFILE_NAME] = {
50
- cdpPort: legacyCdpPort ?? CDP_PORT_RANGE_START,
51
+ cdpPort: legacyCdpPort ?? derivedDefaultCdpPort ?? CDP_PORT_RANGE_START,
51
52
  color: defaultColor,
52
53
  };
53
54
  }
@@ -55,9 +56,26 @@ function ensureDefaultProfile(profiles, defaultColor, legacyCdpPort) {
55
56
  }
56
57
  export function resolveBrowserConfig(cfg) {
57
58
  const enabled = cfg?.enabled ?? DEFAULT_CLAWD_BROWSER_ENABLED;
58
- const controlInfo = parseHttpUrl(cfg?.controlUrl ?? DEFAULT_CLAWD_BROWSER_CONTROL_URL, "browser.controlUrl");
59
+ const envControlUrl = process.env.CLAWDBOT_BROWSER_CONTROL_URL?.trim();
60
+ const derivedControlPort = (() => {
61
+ const raw = process.env.CLAWDBOT_GATEWAY_PORT?.trim();
62
+ if (!raw)
63
+ return null;
64
+ const gatewayPort = Number.parseInt(raw, 10);
65
+ if (!Number.isFinite(gatewayPort) || gatewayPort <= 0)
66
+ return null;
67
+ return deriveDefaultBrowserControlPort(gatewayPort);
68
+ })();
69
+ const derivedControlUrl = derivedControlPort
70
+ ? `http://127.0.0.1:${derivedControlPort}`
71
+ : null;
72
+ const controlInfo = parseHttpUrl(cfg?.controlUrl ??
73
+ envControlUrl ??
74
+ derivedControlUrl ??
75
+ DEFAULT_CLAWD_BROWSER_CONTROL_URL, "browser.controlUrl");
59
76
  const controlPort = controlInfo.port;
60
77
  const defaultColor = normalizeHexColor(cfg?.color);
78
+ const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort);
61
79
  const rawCdpUrl = (cfg?.cdpUrl ?? "").trim();
62
80
  let cdpInfo;
63
81
  if (rawCdpUrl) {
@@ -83,7 +101,7 @@ export function resolveBrowserConfig(cfg) {
83
101
  const defaultProfile = cfg?.defaultProfile ?? DEFAULT_CLAWD_BROWSER_PROFILE_NAME;
84
102
  // Use legacy cdpUrl port for backward compatibility when no profiles configured
85
103
  const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined;
86
- const profiles = ensureDefaultProfile(cfg?.profiles, defaultColor, legacyCdpPort);
104
+ const profiles = ensureDefaultProfile(cfg?.profiles, defaultColor, legacyCdpPort, derivedCdpRange.start);
87
105
  const cdpProtocol = cdpInfo.parsed.protocol === "https:" ? "https" : "http";
88
106
  return {
89
107
  enabled,
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { loadConfig, writeConfigFile } from "../config/config.js";
4
+ import { deriveDefaultBrowserCdpPortRange } from "../config/port-defaults.js";
4
5
  import { resolveClawdUserDataDir } from "./chrome.js";
5
6
  import { parseHttpUrl, resolveProfile } from "./config.js";
6
7
  import { allocateCdpPort, allocateColor, getUsedColors, getUsedPorts, isValidProfileName, } from "./profiles.js";
@@ -37,7 +38,8 @@ export function createBrowserProfilesService(ctx) {
37
38
  }
38
39
  else {
39
40
  const usedPorts = getUsedPorts(resolvedProfiles);
40
- const cdpPort = allocateCdpPort(usedPorts);
41
+ const range = deriveDefaultBrowserCdpPortRange(state.resolved.controlPort);
42
+ const cdpPort = allocateCdpPort(usedPorts, range);
41
43
  if (cdpPort === null) {
42
44
  throw new Error("no available CDP ports in range");
43
45
  }
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * CDP port allocation for browser profiles.
3
3
  *
4
- * Port range: 18800-18899 (100 profiles max)
4
+ * Default port range: 18800-18899 (100 profiles max)
5
5
  * Ports are allocated once at profile creation and persisted in config.
6
+ * Multi-instance: callers may pass an explicit range to avoid collisions.
6
7
  *
7
8
  * Reserved ports (do not use for CDP):
8
9
  * 18789 - Gateway WebSocket
@@ -18,8 +19,18 @@ export function isValidProfileName(name) {
18
19
  return false;
19
20
  return PROFILE_NAME_REGEX.test(name);
20
21
  }
21
- export function allocateCdpPort(usedPorts) {
22
- for (let port = CDP_PORT_RANGE_START; port <= CDP_PORT_RANGE_END; port++) {
22
+ export function allocateCdpPort(usedPorts, range) {
23
+ const start = range?.start ?? CDP_PORT_RANGE_START;
24
+ const end = range?.end ?? CDP_PORT_RANGE_END;
25
+ if (!Number.isFinite(start) ||
26
+ !Number.isFinite(end) ||
27
+ start <= 0 ||
28
+ end <= 0) {
29
+ return null;
30
+ }
31
+ if (start > end)
32
+ return null;
33
+ for (let port = start; port <= end; port++) {
23
34
  if (!usedPorts.has(port))
24
35
  return port;
25
36
  }
@@ -1 +1,3 @@
1
+ 68f18193053997f3dee16de6b0be0bcd97dc70ff8200c77f687479e8b19b78e1
2
+ ||||||| Stash base
1
3
  7daf1cbf58ef395b74c2690c439ac7b3cb536e8eb124baf72ad41da4f542204d
@@ -113,7 +113,7 @@ export function registerGatewayCli(program) {
113
113
  program
114
114
  .command("gateway-daemon")
115
115
  .description("Run the WebSocket Gateway as a long-lived daemon")
116
- .option("--port <port>", "Port for the gateway WebSocket", "18789")
116
+ .option("--port <port>", "Port for the gateway WebSocket")
117
117
  .option("--bind <mode>", 'Bind mode ("loopback"|"tailnet"|"lan"|"auto"). Defaults to config gateway.bind (or loopback).')
118
118
  .option("--token <token>", "Shared token required in connect.params.auth.token (default: CLAWDBOT_GATEWAY_TOKEN env if set)")
119
119
  .option("--auth <mode>", 'Gateway auth mode ("token"|"password")')
@@ -222,7 +222,7 @@ export function registerGatewayCli(program) {
222
222
  const gateway = program
223
223
  .command("gateway")
224
224
  .description("Run the WebSocket Gateway")
225
- .option("--port <port>", "Port for the gateway WebSocket", "18789")
225
+ .option("--port <port>", "Port for the gateway WebSocket")
226
226
  .option("--bind <mode>", 'Bind mode ("loopback"|"tailnet"|"lan"|"auto"). Defaults to config gateway.bind (or loopback).')
227
227
  .option("--token <token>", "Shared token required in connect.params.auth.token (default: CLAWDBOT_GATEWAY_TOKEN env if set)")
228
228
  .option("--auth <mode>", 'Gateway auth mode ("token"|"password")')
@@ -0,0 +1,81 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ function takeValue(raw, next) {
4
+ if (raw.includes("=")) {
5
+ const [, value] = raw.split("=", 2);
6
+ const trimmed = (value ?? "").trim();
7
+ return { value: trimmed || null, consumedNext: false };
8
+ }
9
+ const trimmed = (next ?? "").trim();
10
+ return { value: trimmed || null, consumedNext: Boolean(next) };
11
+ }
12
+ function isValidProfileName(value) {
13
+ if (!value)
14
+ return false;
15
+ // Keep it path-safe + shell-friendly.
16
+ return /^[a-z0-9][a-z0-9_-]{0,63}$/i.test(value);
17
+ }
18
+ export function parseCliProfileArgs(argv) {
19
+ if (argv.length < 2)
20
+ return { ok: true, profile: null, argv };
21
+ const out = argv.slice(0, 2);
22
+ let profile = null;
23
+ let sawDev = false;
24
+ const args = argv.slice(2);
25
+ for (let i = 0; i < args.length; i += 1) {
26
+ const arg = args[i];
27
+ if (arg === undefined)
28
+ continue;
29
+ if (arg === "--dev") {
30
+ if (profile && profile !== "dev") {
31
+ return { ok: false, error: "Cannot combine --dev with --profile" };
32
+ }
33
+ sawDev = true;
34
+ profile = "dev";
35
+ continue;
36
+ }
37
+ if (arg === "--profile" || arg.startsWith("--profile=")) {
38
+ if (sawDev) {
39
+ return { ok: false, error: "Cannot combine --dev with --profile" };
40
+ }
41
+ const next = args[i + 1];
42
+ const { value, consumedNext } = takeValue(arg, next);
43
+ if (consumedNext)
44
+ i += 1;
45
+ if (!value)
46
+ return { ok: false, error: "--profile requires a value" };
47
+ if (!isValidProfileName(value)) {
48
+ return {
49
+ ok: false,
50
+ error: 'Invalid --profile (use letters, numbers, "_", "-" only)',
51
+ };
52
+ }
53
+ profile = value;
54
+ continue;
55
+ }
56
+ out.push(arg);
57
+ }
58
+ return { ok: true, profile, argv: out };
59
+ }
60
+ function resolveProfileStateDir(profile, homedir) {
61
+ const suffix = profile.toLowerCase() === "default" ? "" : `-${profile}`;
62
+ return path.join(homedir(), `.clawdbot${suffix}`);
63
+ }
64
+ export function applyCliProfileEnv(params) {
65
+ const env = params.env ?? process.env;
66
+ const homedir = params.homedir ?? os.homedir;
67
+ const profile = params.profile.trim();
68
+ if (!profile)
69
+ return;
70
+ // Convenience only: fill defaults, never override explicit env values.
71
+ env.CLAWDBOT_PROFILE = profile;
72
+ const stateDir = env.CLAWDBOT_STATE_DIR?.trim() || resolveProfileStateDir(profile, homedir);
73
+ if (!env.CLAWDBOT_STATE_DIR?.trim())
74
+ env.CLAWDBOT_STATE_DIR = stateDir;
75
+ if (!env.CLAWDBOT_CONFIG_PATH?.trim()) {
76
+ env.CLAWDBOT_CONFIG_PATH = path.join(stateDir, "clawdbot.json");
77
+ }
78
+ if (profile === "dev" && !env.CLAWDBOT_GATEWAY_PORT?.trim()) {
79
+ env.CLAWDBOT_GATEWAY_PORT = "19001";
80
+ }
81
+ }
@@ -31,7 +31,12 @@ export function buildProgram() {
31
31
  const program = new Command();
32
32
  const PROGRAM_VERSION = VERSION;
33
33
  const TAGLINE = "Send, receive, and auto-reply on WhatsApp (web) and Telegram (bot).";
34
- program.name("clawdbot").description("").version(PROGRAM_VERSION);
34
+ program
35
+ .name("clawdbot")
36
+ .description("")
37
+ .version(PROGRAM_VERSION)
38
+ .option("--dev", "Dev profile: isolate state under ~/.clawdbot-dev, default gateway port 19001, and shift derived ports (bridge/browser/canvas)")
39
+ .option("--profile <name>", "Use a named profile (isolates CLAWDBOT_STATE_DIR/CLAWDBOT_CONFIG_PATH under ~/.clawdbot-<name>)");
35
40
  const formatIntroLine = (version, rich = true) => {
36
41
  const base = `📡 clawdbot ${version} — ${TAGLINE}`;
37
42
  return rich && chalk.level > 0
@@ -82,6 +87,10 @@ export function buildProgram() {
82
87
  "Send via your web session and print JSON result.",
83
88
  ],
84
89
  ["clawdbot gateway --port 18789", "Run the WebSocket Gateway locally."],
90
+ [
91
+ "clawdbot --dev gateway",
92
+ "Run a dev Gateway (isolated state/config) on ws://127.0.0.1:19001.",
93
+ ],
85
94
  [
86
95
  "clawdbot gateway --force",
87
96
  "Kill anything bound to the default gateway port, then start it.",
@@ -0,0 +1,33 @@
1
+ import process from "node:process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { loadDotEnv } from "../infra/dotenv.js";
4
+ import { normalizeEnv } from "../infra/env.js";
5
+ import { isMainModule } from "../infra/is-main.js";
6
+ import { ensureClawdbotCliOnPath } from "../infra/path-env.js";
7
+ import { assertSupportedRuntime } from "../infra/runtime-guard.js";
8
+ import { enableConsoleCapture } from "../logging.js";
9
+ export async function runCli(argv = process.argv) {
10
+ loadDotEnv({ quiet: true });
11
+ normalizeEnv();
12
+ ensureClawdbotCliOnPath();
13
+ // Capture all console output into structured logs while keeping stdout/stderr behavior.
14
+ enableConsoleCapture();
15
+ // Enforce the minimum supported runtime before doing any work.
16
+ assertSupportedRuntime();
17
+ const { buildProgram } = await import("./program.js");
18
+ const program = buildProgram();
19
+ // Global error handlers to prevent silent crashes from unhandled rejections/exceptions.
20
+ // These log the error and exit gracefully instead of crashing without trace.
21
+ process.on("unhandledRejection", (reason, _promise) => {
22
+ console.error("[clawdbot] Unhandled promise rejection:", reason instanceof Error ? (reason.stack ?? reason.message) : reason);
23
+ process.exit(1);
24
+ });
25
+ process.on("uncaughtException", (error) => {
26
+ console.error("[clawdbot] Uncaught exception:", error.stack ?? error.message);
27
+ process.exit(1);
28
+ });
29
+ await program.parseAsync(argv);
30
+ }
31
+ export function isCliMainModule() {
32
+ return isMainModule({ currentFile: fileURLToPath(import.meta.url) });
33
+ }
@@ -5,6 +5,7 @@ import { CONFIG_PATH_CLAWDBOT, readConfigFileSnapshot, resolveGatewayPort, write
5
5
  import { GATEWAY_LAUNCH_AGENT_LABEL } from "../daemon/constants.js";
6
6
  import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
7
7
  import { resolveGatewayService } from "../daemon/service.js";
8
+ import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
8
9
  import { defaultRuntime } from "../runtime.js";
9
10
  import { resolveUserPath, sleep } from "../utils.js";
10
11
  import { createClackPrompter } from "../wizard/clack-prompter.js";
@@ -434,6 +435,10 @@ export async function runConfigureWizard(opts, runtime = defaultRuntime) {
434
435
  runtime.error(`Health check failed: ${String(err)}`);
435
436
  }
436
437
  }
438
+ const controlUiAssets = await ensureControlUiAssetsBuilt(runtime);
439
+ if (!controlUiAssets.ok && controlUiAssets.message) {
440
+ runtime.error(controlUiAssets.message);
441
+ }
437
442
  note((() => {
438
443
  const bind = nextConfig.gateway?.bind ?? "loopback";
439
444
  const links = resolveControlUiLinks({
@@ -270,7 +270,7 @@ export async function setupProviders(cfg, runtime, prompter, options) {
270
270
  if (!whatsappLinked) {
271
271
  await prompter.note([
272
272
  "Scan the QR with WhatsApp on your phone.",
273
- "Credentials are stored under ~/.clawdbot/credentials/ for future runs.",
273
+ `Credentials are stored under ${resolveWebAuthDir()}/ for future runs.`,
274
274
  ].join("\n"), "WhatsApp linking");
275
275
  }
276
276
  const wantsLink = await prompter.confirm({
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import JSON5 from "json5";
4
4
  import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace, } from "../agents/workspace.js";
5
5
  import { CONFIG_PATH_CLAWDBOT } from "../config/config.js";
6
+ import { applyModelAliasDefaults } from "../config/defaults.js";
6
7
  import { resolveSessionTranscriptsDir } from "../config/sessions.js";
7
8
  import { defaultRuntime } from "../runtime.js";
8
9
  async function readConfigFileRaw() {
@@ -20,7 +21,9 @@ async function readConfigFileRaw() {
20
21
  }
21
22
  async function writeConfigFile(cfg) {
22
23
  await fs.mkdir(path.dirname(CONFIG_PATH_CLAWDBOT), { recursive: true });
23
- const json = JSON.stringify(cfg, null, 2).trimEnd().concat("\n");
24
+ const json = JSON.stringify(applyModelAliasDefaults(cfg), null, 2)
25
+ .trimEnd()
26
+ .concat("\n");
24
27
  await fs.writeFile(CONFIG_PATH_CLAWDBOT, json, "utf-8");
25
28
  }
26
29
  export async function setupCommand(opts, runtime = defaultRuntime) {