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
|
@@ -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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
?
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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.
|
|
98
|
-
logVerbose(`Ignoring /send from
|
|
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 (
|
|
134
|
-
logVerbose(`Ignoring /restart from
|
|
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
|
-
|
|
154
|
+
const statusRequested = directives.hasStatusDirective ||
|
|
155
|
+
command.commandBodyNormalized === "/status" ||
|
|
146
156
|
command.commandBodyNormalized === "status" ||
|
|
147
|
-
command.commandBodyNormalized.startsWith("/status ")
|
|
148
|
-
|
|
149
|
-
|
|
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:
|
|
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);
|
package/dist/auto-reply/reply.js
CHANGED
|
@@ -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
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
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,
|
package/dist/browser/config.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|
package/dist/browser/profiles.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CDP port allocation for browser profiles.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/dist/cli/gateway-cli.js
CHANGED
|
@@ -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"
|
|
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"
|
|
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
|
+
}
|
package/dist/cli/program.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
273
|
+
`Credentials are stored under ${resolveWebAuthDir()}/ for future runs.`,
|
|
274
274
|
].join("\n"), "WhatsApp linking");
|
|
275
275
|
}
|
|
276
276
|
const wantsLink = await prompter.confirm({
|
package/dist/commands/setup.js
CHANGED
|
@@ -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)
|
|
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) {
|