clawdbot 2026.1.4-1 → 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 -6
- 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/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 -16
- package/dist/control-ui/assets/index-BFID3yAA.css +0 -1
- package/dist/control-ui/assets/index-CE_axlTS.js +0 -2235
- package/dist/control-ui/assets/index-CE_axlTS.js.map +0 -1
- package/dist/control-ui/index.html +0 -15
- package/dist/daemon/constants.js +0 -10
- package/dist/daemon/launchd.js +0 -276
- package/dist/daemon/legacy.js +0 -63
- package/dist/daemon/program-args.js +0 -76
- package/dist/daemon/schtasks.js +0 -257
- package/dist/daemon/service.js +0 -60
- package/dist/daemon/systemd.js +0 -266
- package/dist/imessage/client.js +0 -165
- package/dist/imessage/index.js +0 -3
- package/dist/imessage/monitor.js +0 -272
- package/dist/imessage/probe.js +0 -26
- package/dist/imessage/send.js +0 -83
- package/dist/imessage/targets.js +0 -176
- package/dist/signal/client.js +0 -134
- package/dist/signal/daemon.js +0 -69
- package/dist/signal/index.js +0 -3
- package/dist/signal/monitor.js +0 -336
- package/dist/signal/probe.js +0 -46
- package/dist/signal/send.js +0 -91
- package/dist/slack/actions.js +0 -97
- package/dist/slack/index.js +0 -5
- package/dist/slack/monitor.js +0 -1029
- package/dist/slack/probe.js +0 -47
- package/dist/slack/send.js +0 -131
- package/dist/slack/token.js +0 -10
- package/dist/tui/commands.js +0 -74
- package/dist/tui/components/assistant-message.js +0 -16
- package/dist/tui/components/chat-log.js +0 -92
- package/dist/tui/components/custom-editor.js +0 -53
- package/dist/tui/components/selectors.js +0 -8
- package/dist/tui/components/tool-execution.js +0 -111
- package/dist/tui/components/user-message.js +0 -17
- package/dist/tui/gateway-chat.js +0 -140
- package/dist/tui/layout.js +0 -41
- package/dist/tui/message-list.js +0 -57
- package/dist/tui/theme/theme.js +0 -80
- package/dist/tui/theme.js +0 -25
- package/dist/tui/tui.js +0 -708
- package/dist/wizard/clack-prompter.js +0 -56
- package/dist/wizard/onboarding.js +0 -452
- package/dist/wizard/prompts.js +0 -6
- package/dist/wizard/session.js +0 -203
package/dist/discord/monitor.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ChannelType, Client, Events, GatewayIntentBits, MessageType, Partials, } from "discord.js";
|
|
2
2
|
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
|
3
|
+
import { hasControlCommand } from "../auto-reply/command-detection.js";
|
|
3
4
|
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
|
4
5
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
|
5
|
-
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
|
6
6
|
import { loadConfig } from "../config/config.js";
|
|
7
|
-
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
|
8
|
-
import { danger, logVerbose, shouldLogVerbose
|
|
7
|
+
import { resolveSessionKey, resolveStorePath, updateLastRoute, } from "../config/sessions.js";
|
|
8
|
+
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
|
9
9
|
import { enqueueSystemEvent } from "../infra/system-events.js";
|
|
10
10
|
import { getChildLogger } from "../logging.js";
|
|
11
11
|
import { detectMime } from "../media/mime.js";
|
|
@@ -56,7 +56,6 @@ export async function monitorDiscordProvider(opts = {}) {
|
|
|
56
56
|
const dmConfig = cfg.discord?.dm;
|
|
57
57
|
const guildEntries = cfg.discord?.guilds;
|
|
58
58
|
const allowFrom = dmConfig?.allowFrom;
|
|
59
|
-
const slashCommand = resolveSlashCommandConfig(opts.slashCommand ?? cfg.discord?.slashCommand);
|
|
60
59
|
const mediaMaxBytes = (opts.mediaMaxMb ?? cfg.discord?.mediaMaxMb ?? 8) * 1024 * 1024;
|
|
61
60
|
const textLimit = resolveTextChunkLimit(cfg, "discord");
|
|
62
61
|
const historyLimit = Math.max(0, opts.historyLimit ?? cfg.discord?.historyLimit ?? 20);
|
|
@@ -87,9 +86,6 @@ export async function monitorDiscordProvider(opts = {}) {
|
|
|
87
86
|
const guildHistories = new Map();
|
|
88
87
|
client.once(Events.ClientReady, () => {
|
|
89
88
|
runtime.log?.(`logged in as ${client.user?.tag ?? "unknown"}`);
|
|
90
|
-
if (slashCommand.enabled) {
|
|
91
|
-
void ensureSlashCommand(client, slashCommand, runtime);
|
|
92
|
-
}
|
|
93
89
|
});
|
|
94
90
|
client.on(Events.Error, (err) => {
|
|
95
91
|
runtime.error?.(danger(`client error: ${String(err)}`));
|
|
@@ -182,8 +178,24 @@ export async function monitorDiscordProvider(opts = {}) {
|
|
|
182
178
|
guildHistories.set(message.channelId, history);
|
|
183
179
|
}
|
|
184
180
|
const resolvedRequireMention = channelConfig?.requireMention ?? guildInfo?.requireMention ?? true;
|
|
181
|
+
const hasAnyMention = Boolean(!isDirectMessage &&
|
|
182
|
+
(message.mentions?.everyone ||
|
|
183
|
+
(message.mentions?.users?.size ?? 0) > 0 ||
|
|
184
|
+
(message.mentions?.roles?.size ?? 0) > 0));
|
|
185
|
+
const commandAuthorized = resolveDiscordCommandAuthorized({
|
|
186
|
+
isDirectMessage,
|
|
187
|
+
allowFrom,
|
|
188
|
+
guildInfo,
|
|
189
|
+
author: message.author,
|
|
190
|
+
});
|
|
191
|
+
const shouldBypassMention = isGuildMessage &&
|
|
192
|
+
resolvedRequireMention &&
|
|
193
|
+
!wasMentioned &&
|
|
194
|
+
!hasAnyMention &&
|
|
195
|
+
commandAuthorized &&
|
|
196
|
+
hasControlCommand(baseText);
|
|
185
197
|
if (isGuildMessage && resolvedRequireMention) {
|
|
186
|
-
if (botId && !wasMentioned) {
|
|
198
|
+
if (botId && !wasMentioned && !shouldBypassMention) {
|
|
187
199
|
logVerbose(`discord: drop guild message (mention required, botId=${botId})`);
|
|
188
200
|
logger.info({
|
|
189
201
|
channelId: message.channelId,
|
|
@@ -229,7 +241,18 @@ export async function monitorDiscordProvider(opts = {}) {
|
|
|
229
241
|
}
|
|
230
242
|
const systemText = resolveDiscordSystemEvent(message);
|
|
231
243
|
if (systemText) {
|
|
244
|
+
const sessionCfg = cfg.session;
|
|
245
|
+
const sessionScope = sessionCfg?.scope ?? "per-sender";
|
|
246
|
+
const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
|
|
247
|
+
const sessionKey = resolveSessionKey(sessionScope, {
|
|
248
|
+
From: isDirectMessage
|
|
249
|
+
? `discord:${message.author.id}`
|
|
250
|
+
: `group:${message.channelId}`,
|
|
251
|
+
ChatType: isDirectMessage ? "direct" : "group",
|
|
252
|
+
Surface: "discord",
|
|
253
|
+
}, mainKey);
|
|
232
254
|
enqueueSystemEvent(systemText, {
|
|
255
|
+
sessionKey,
|
|
233
256
|
contextKey: `discord:system:${message.channelId}:${message.id}`,
|
|
234
257
|
});
|
|
235
258
|
return;
|
|
@@ -323,11 +346,14 @@ export async function monitorDiscordProvider(opts = {}) {
|
|
|
323
346
|
: `channel:${message.channelId}`,
|
|
324
347
|
ChatType: isDirectMessage ? "direct" : "group",
|
|
325
348
|
SenderName: message.member?.displayName ?? message.author.tag,
|
|
349
|
+
SenderId: message.author.id,
|
|
326
350
|
SenderUsername: message.author.username,
|
|
327
351
|
SenderTag: message.author.tag,
|
|
328
352
|
GroupSubject: groupSubject,
|
|
329
353
|
GroupRoom: groupRoom,
|
|
330
|
-
GroupSpace: isGuildMessage
|
|
354
|
+
GroupSpace: isGuildMessage
|
|
355
|
+
? (guildInfo?.id ?? guildSlug) || undefined
|
|
356
|
+
: undefined,
|
|
331
357
|
Surface: "discord",
|
|
332
358
|
WasMentioned: wasMentioned,
|
|
333
359
|
MessageSid: message.id,
|
|
@@ -335,6 +361,7 @@ export async function monitorDiscordProvider(opts = {}) {
|
|
|
335
361
|
MediaPath: media?.path,
|
|
336
362
|
MediaType: media?.contentType,
|
|
337
363
|
MediaUrl: media?.path,
|
|
364
|
+
CommandAuthorized: commandAuthorized,
|
|
338
365
|
};
|
|
339
366
|
const replyTarget = ctxPayload.To ?? undefined;
|
|
340
367
|
if (!replyTarget) {
|
|
@@ -482,7 +509,16 @@ export async function monitorDiscordProvider(opts = {}) {
|
|
|
482
509
|
const authorLabel = message.author?.tag ?? message.author?.username;
|
|
483
510
|
const baseText = `Discord reaction ${action}: ${emojiLabel} by ${actorLabel} on ${guildSlug} ${channelLabel} msg ${message.id}`;
|
|
484
511
|
const text = authorLabel ? `${baseText} from ${authorLabel}` : baseText;
|
|
512
|
+
const sessionCfg = cfg.session;
|
|
513
|
+
const sessionScope = sessionCfg?.scope ?? "per-sender";
|
|
514
|
+
const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
|
|
515
|
+
const sessionKey = resolveSessionKey(sessionScope, {
|
|
516
|
+
From: `group:${message.channelId}`,
|
|
517
|
+
ChatType: "group",
|
|
518
|
+
Surface: "discord",
|
|
519
|
+
}, mainKey);
|
|
485
520
|
enqueueSystemEvent(text, {
|
|
521
|
+
sessionKey,
|
|
486
522
|
contextKey: `discord:reaction:${action}:${message.id}:${user.id}:${emojiLabel}`,
|
|
487
523
|
});
|
|
488
524
|
}
|
|
@@ -496,162 +532,6 @@ export async function monitorDiscordProvider(opts = {}) {
|
|
|
496
532
|
client.on(Events.MessageReactionRemove, async (reaction, user) => {
|
|
497
533
|
await handleReactionEvent(reaction, user, "removed");
|
|
498
534
|
});
|
|
499
|
-
client.on(Events.InteractionCreate, async (interaction) => {
|
|
500
|
-
try {
|
|
501
|
-
if (!slashCommand.enabled)
|
|
502
|
-
return;
|
|
503
|
-
if (!interaction.isChatInputCommand())
|
|
504
|
-
return;
|
|
505
|
-
if (interaction.commandName !== slashCommand.name)
|
|
506
|
-
return;
|
|
507
|
-
if (interaction.user?.bot)
|
|
508
|
-
return;
|
|
509
|
-
const channelType = interaction.channel?.type;
|
|
510
|
-
const isGroupDm = channelType === ChannelType.GroupDM;
|
|
511
|
-
const isDirectMessage = !interaction.inGuild() && channelType === ChannelType.DM;
|
|
512
|
-
const isGuildMessage = interaction.inGuild();
|
|
513
|
-
if (isGroupDm && !groupDmEnabled) {
|
|
514
|
-
logVerbose("discord: drop slash (group dms disabled)");
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
if (isDirectMessage && !dmEnabled) {
|
|
518
|
-
logVerbose("discord: drop slash (dms disabled)");
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
if (shouldLogVerbose()) {
|
|
522
|
-
logVerbose(`discord: slash inbound guild=${interaction.guildId ?? "dm"} channel=${interaction.channelId} type=${isDirectMessage ? "dm" : isGroupDm ? "group-dm" : "guild"}`);
|
|
523
|
-
}
|
|
524
|
-
if (isGuildMessage) {
|
|
525
|
-
const guildInfo = resolveDiscordGuildEntry({
|
|
526
|
-
guild: interaction.guild ?? null,
|
|
527
|
-
guildEntries,
|
|
528
|
-
});
|
|
529
|
-
if (guildEntries &&
|
|
530
|
-
Object.keys(guildEntries).length > 0 &&
|
|
531
|
-
!guildInfo) {
|
|
532
|
-
logVerbose(`Blocked discord guild ${interaction.guildId ?? "unknown"} (not in discord.guilds)`);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
const channelName = interaction.channel &&
|
|
536
|
-
"name" in interaction.channel &&
|
|
537
|
-
typeof interaction.channel.name === "string"
|
|
538
|
-
? interaction.channel.name
|
|
539
|
-
: undefined;
|
|
540
|
-
const channelSlug = channelName
|
|
541
|
-
? normalizeDiscordSlug(channelName)
|
|
542
|
-
: "";
|
|
543
|
-
const channelConfig = resolveDiscordChannelConfig({
|
|
544
|
-
guildInfo,
|
|
545
|
-
channelId: interaction.channelId,
|
|
546
|
-
channelName,
|
|
547
|
-
channelSlug,
|
|
548
|
-
});
|
|
549
|
-
if (channelConfig?.allowed === false) {
|
|
550
|
-
logVerbose(`Blocked discord channel ${interaction.channelId} not in guild channel allowlist`);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
const userAllow = guildInfo?.users;
|
|
554
|
-
if (Array.isArray(userAllow) && userAllow.length > 0) {
|
|
555
|
-
const users = normalizeDiscordAllowList(userAllow, [
|
|
556
|
-
"discord:",
|
|
557
|
-
"user:",
|
|
558
|
-
]);
|
|
559
|
-
const userOk = !users ||
|
|
560
|
-
allowListMatches(users, {
|
|
561
|
-
id: interaction.user.id,
|
|
562
|
-
name: interaction.user.username,
|
|
563
|
-
tag: interaction.user.tag,
|
|
564
|
-
});
|
|
565
|
-
if (!userOk) {
|
|
566
|
-
logVerbose(`Blocked discord guild sender ${interaction.user.id} (not in guild users allowlist)`);
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
else if (isGroupDm) {
|
|
572
|
-
const channelName = interaction.channel &&
|
|
573
|
-
"name" in interaction.channel &&
|
|
574
|
-
typeof interaction.channel.name === "string"
|
|
575
|
-
? interaction.channel.name
|
|
576
|
-
: undefined;
|
|
577
|
-
const channelSlug = channelName
|
|
578
|
-
? normalizeDiscordSlug(channelName)
|
|
579
|
-
: "";
|
|
580
|
-
const groupDmAllowed = resolveGroupDmAllow({
|
|
581
|
-
channels: groupDmChannels,
|
|
582
|
-
channelId: interaction.channelId,
|
|
583
|
-
channelName,
|
|
584
|
-
channelSlug,
|
|
585
|
-
});
|
|
586
|
-
if (!groupDmAllowed)
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
else if (isDirectMessage) {
|
|
590
|
-
if (Array.isArray(allowFrom) && allowFrom.length > 0) {
|
|
591
|
-
const allowList = normalizeDiscordAllowList(allowFrom, [
|
|
592
|
-
"discord:",
|
|
593
|
-
"user:",
|
|
594
|
-
]);
|
|
595
|
-
const permitted = allowList &&
|
|
596
|
-
allowListMatches(allowList, {
|
|
597
|
-
id: interaction.user.id,
|
|
598
|
-
name: interaction.user.username,
|
|
599
|
-
tag: interaction.user.tag,
|
|
600
|
-
});
|
|
601
|
-
if (!permitted) {
|
|
602
|
-
logVerbose(`Blocked unauthorized discord sender ${interaction.user.id} (not in allowFrom)`);
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
const prompt = resolveSlashPrompt(interaction.options.data);
|
|
608
|
-
if (!prompt) {
|
|
609
|
-
await interaction.reply({
|
|
610
|
-
content: "Message required.",
|
|
611
|
-
ephemeral: true,
|
|
612
|
-
});
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
await interaction.deferReply({ ephemeral: slashCommand.ephemeral });
|
|
616
|
-
const userId = interaction.user.id;
|
|
617
|
-
const ctxPayload = {
|
|
618
|
-
Body: prompt,
|
|
619
|
-
From: `discord:${userId}`,
|
|
620
|
-
To: `slash:${userId}`,
|
|
621
|
-
ChatType: "direct",
|
|
622
|
-
SenderName: interaction.user.username,
|
|
623
|
-
Surface: "discord",
|
|
624
|
-
WasMentioned: true,
|
|
625
|
-
MessageSid: interaction.id,
|
|
626
|
-
Timestamp: interaction.createdTimestamp,
|
|
627
|
-
SessionKey: `${slashCommand.sessionPrefix}:${userId}`,
|
|
628
|
-
};
|
|
629
|
-
const replyResult = await getReplyFromConfig(ctxPayload, undefined, cfg);
|
|
630
|
-
const replies = replyResult
|
|
631
|
-
? Array.isArray(replyResult)
|
|
632
|
-
? replyResult
|
|
633
|
-
: [replyResult]
|
|
634
|
-
: [];
|
|
635
|
-
await deliverSlashReplies({
|
|
636
|
-
replies,
|
|
637
|
-
interaction,
|
|
638
|
-
ephemeral: slashCommand.ephemeral,
|
|
639
|
-
textLimit,
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
catch (err) {
|
|
643
|
-
runtime.error?.(danger(`slash handler failed: ${String(err)}`));
|
|
644
|
-
if (interaction.isRepliable()) {
|
|
645
|
-
const content = "Sorry, something went wrong handling that command.";
|
|
646
|
-
if (interaction.deferred || interaction.replied) {
|
|
647
|
-
await interaction.followUp({ content, ephemeral: true });
|
|
648
|
-
}
|
|
649
|
-
else {
|
|
650
|
-
await interaction.reply({ content, ephemeral: true });
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
535
|
await client.login(token);
|
|
656
536
|
await new Promise((resolve, reject) => {
|
|
657
537
|
const onAbort = () => {
|
|
@@ -914,6 +794,35 @@ export function allowListMatches(allowList, candidates) {
|
|
|
914
794
|
return true;
|
|
915
795
|
return false;
|
|
916
796
|
}
|
|
797
|
+
function resolveDiscordCommandAuthorized(params) {
|
|
798
|
+
const { isDirectMessage, allowFrom, guildInfo, author } = params;
|
|
799
|
+
if (isDirectMessage) {
|
|
800
|
+
if (!Array.isArray(allowFrom) || allowFrom.length === 0)
|
|
801
|
+
return true;
|
|
802
|
+
const allowList = normalizeDiscordAllowList(allowFrom, [
|
|
803
|
+
"discord:",
|
|
804
|
+
"user:",
|
|
805
|
+
]);
|
|
806
|
+
if (!allowList)
|
|
807
|
+
return true;
|
|
808
|
+
return allowListMatches(allowList, {
|
|
809
|
+
id: author.id,
|
|
810
|
+
name: author.username,
|
|
811
|
+
tag: author.tag,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
const users = guildInfo?.users;
|
|
815
|
+
if (!Array.isArray(users) || users.length === 0)
|
|
816
|
+
return true;
|
|
817
|
+
const allowList = normalizeDiscordAllowList(users, ["discord:", "user:"]);
|
|
818
|
+
if (!allowList)
|
|
819
|
+
return true;
|
|
820
|
+
return allowListMatches(allowList, {
|
|
821
|
+
id: author.id,
|
|
822
|
+
name: author.username,
|
|
823
|
+
tag: author.tag,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
917
826
|
export function shouldEmitDiscordReactionNotification(params) {
|
|
918
827
|
const { mode, botId, messageAuthorId, userId, userName, userTag, allowlist } = params;
|
|
919
828
|
const effectiveMode = mode ?? "own";
|
|
@@ -1027,75 +936,6 @@ export function resolveGroupDmAllow(params) {
|
|
|
1027
936
|
name: channelSlug || channelName,
|
|
1028
937
|
});
|
|
1029
938
|
}
|
|
1030
|
-
async function ensureSlashCommand(client, slashCommand, runtime) {
|
|
1031
|
-
try {
|
|
1032
|
-
const appCommands = client.application?.commands;
|
|
1033
|
-
if (!appCommands) {
|
|
1034
|
-
runtime.error?.(danger("discord slash commands unavailable"));
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
const existing = await appCommands.fetch();
|
|
1038
|
-
const hasCommand = Array.from(existing.values()).some((entry) => entry.name === slashCommand.name);
|
|
1039
|
-
if (hasCommand)
|
|
1040
|
-
return;
|
|
1041
|
-
await appCommands.create({
|
|
1042
|
-
name: slashCommand.name,
|
|
1043
|
-
description: "Ask Clawdbot a question",
|
|
1044
|
-
options: [
|
|
1045
|
-
{
|
|
1046
|
-
name: "prompt",
|
|
1047
|
-
description: "What should Clawdbot help with?",
|
|
1048
|
-
type: ApplicationCommandOptionType.String,
|
|
1049
|
-
required: true,
|
|
1050
|
-
},
|
|
1051
|
-
],
|
|
1052
|
-
});
|
|
1053
|
-
runtime.log?.(`registered discord slash command /${slashCommand.name}`);
|
|
1054
|
-
}
|
|
1055
|
-
catch (err) {
|
|
1056
|
-
const status = err?.status;
|
|
1057
|
-
const code = err?.code;
|
|
1058
|
-
const message = String(err);
|
|
1059
|
-
const isRateLimit = status === 429 || code === 429 || /rate ?limit/i.test(message);
|
|
1060
|
-
const text = `discord slash command setup failed: ${message}`;
|
|
1061
|
-
if (isRateLimit) {
|
|
1062
|
-
logVerbose(text);
|
|
1063
|
-
runtime.error?.(warn(text));
|
|
1064
|
-
}
|
|
1065
|
-
else {
|
|
1066
|
-
runtime.error?.(danger(text));
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
function resolveSlashCommandConfig(raw) {
|
|
1071
|
-
return {
|
|
1072
|
-
enabled: raw ? raw.enabled !== false : false,
|
|
1073
|
-
name: raw?.name?.trim() || "clawd",
|
|
1074
|
-
sessionPrefix: raw?.sessionPrefix?.trim() || "discord:slash",
|
|
1075
|
-
ephemeral: raw?.ephemeral !== false,
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
function resolveSlashPrompt(options) {
|
|
1079
|
-
const direct = findFirstStringOption(options);
|
|
1080
|
-
if (direct)
|
|
1081
|
-
return direct;
|
|
1082
|
-
return undefined;
|
|
1083
|
-
}
|
|
1084
|
-
function findFirstStringOption(options) {
|
|
1085
|
-
for (const option of options) {
|
|
1086
|
-
if (typeof option.value === "string") {
|
|
1087
|
-
const trimmed = option.value.trim();
|
|
1088
|
-
if (trimmed)
|
|
1089
|
-
return trimmed;
|
|
1090
|
-
}
|
|
1091
|
-
if (option.options && option.options.length > 0) {
|
|
1092
|
-
const nested = findFirstStringOption(option.options);
|
|
1093
|
-
if (nested)
|
|
1094
|
-
return nested;
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
return undefined;
|
|
1098
|
-
}
|
|
1099
939
|
async function sendTyping(message) {
|
|
1100
940
|
try {
|
|
1101
941
|
const channel = message.channel;
|
|
@@ -1155,34 +995,3 @@ async function deliverReplies({ replies, target, token, runtime, replyToMode, te
|
|
|
1155
995
|
runtime.log?.(`delivered reply to ${target}`);
|
|
1156
996
|
}
|
|
1157
997
|
}
|
|
1158
|
-
async function deliverSlashReplies({ replies, interaction, ephemeral, textLimit, }) {
|
|
1159
|
-
const messages = [];
|
|
1160
|
-
const chunkLimit = Math.min(textLimit, 2000);
|
|
1161
|
-
for (const payload of replies) {
|
|
1162
|
-
const textRaw = payload.text?.trim() ?? "";
|
|
1163
|
-
const text = textRaw && textRaw !== SILENT_REPLY_TOKEN ? textRaw : undefined;
|
|
1164
|
-
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
1165
|
-
const combined = [
|
|
1166
|
-
text ?? "",
|
|
1167
|
-
...mediaList.map((url) => url.trim()).filter(Boolean),
|
|
1168
|
-
]
|
|
1169
|
-
.filter(Boolean)
|
|
1170
|
-
.join("\n");
|
|
1171
|
-
if (!combined)
|
|
1172
|
-
continue;
|
|
1173
|
-
for (const chunk of chunkText(combined, chunkLimit)) {
|
|
1174
|
-
messages.push(chunk);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
if (messages.length === 0) {
|
|
1178
|
-
await interaction.editReply({
|
|
1179
|
-
content: "No response was generated for that command.",
|
|
1180
|
-
});
|
|
1181
|
-
return;
|
|
1182
|
-
}
|
|
1183
|
-
const [first, ...rest] = messages;
|
|
1184
|
-
await interaction.editReply({ content: first });
|
|
1185
|
-
for (const message of rest) {
|
|
1186
|
-
await interaction.followUp({ content: message, ephemeral });
|
|
1187
|
-
}
|
|
1188
|
-
}
|
package/dist/entry.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
|
|
4
|
+
const parsed = parseCliProfileArgs(process.argv);
|
|
5
|
+
if (!parsed.ok) {
|
|
6
|
+
// Keep it simple; Commander will handle rich help/errors after we strip flags.
|
|
7
|
+
console.error(`[clawdbot] ${parsed.error}`);
|
|
8
|
+
process.exit(2);
|
|
9
|
+
}
|
|
10
|
+
if (parsed.profile) {
|
|
11
|
+
applyCliProfileEnv({ profile: parsed.profile });
|
|
12
|
+
// Keep Commander and ad-hoc argv checks consistent.
|
|
13
|
+
process.argv = parsed.argv;
|
|
14
|
+
}
|
|
15
|
+
const { runCli } = await import("./cli/run-main.js");
|
|
16
|
+
await runCli(process.argv);
|
package/dist/gateway/call.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { loadConfig, resolveGatewayPort } from "../config/config.js";
|
|
3
|
+
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
|
|
3
4
|
import { GatewayClient } from "./client.js";
|
|
4
5
|
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
|
5
6
|
export async function callGateway(opts) {
|
|
@@ -9,13 +10,19 @@ export async function callGateway(opts) {
|
|
|
9
10
|
const remote = isRemoteMode ? config.gateway?.remote : undefined;
|
|
10
11
|
const authToken = config.gateway?.auth?.token;
|
|
11
12
|
const localPort = resolveGatewayPort(config);
|
|
13
|
+
const tailnetIPv4 = pickPrimaryTailnetIPv4();
|
|
14
|
+
const bindMode = config.gateway?.bind ?? "loopback";
|
|
15
|
+
const preferTailnet = bindMode === "tailnet" || (bindMode === "auto" && !!tailnetIPv4);
|
|
16
|
+
const localUrl = preferTailnet && tailnetIPv4
|
|
17
|
+
? `ws://${tailnetIPv4}:${localPort}`
|
|
18
|
+
: `ws://127.0.0.1:${localPort}`;
|
|
12
19
|
const url = (typeof opts.url === "string" && opts.url.trim().length > 0
|
|
13
20
|
? opts.url.trim()
|
|
14
21
|
: undefined) ||
|
|
15
22
|
(typeof remote?.url === "string" && remote.url.trim().length > 0
|
|
16
23
|
? remote.url.trim()
|
|
17
24
|
: undefined) ||
|
|
18
|
-
|
|
25
|
+
localUrl;
|
|
19
26
|
const token = (typeof opts.token === "string" && opts.token.trim().length > 0
|
|
20
27
|
? opts.token.trim()
|
|
21
28
|
: undefined) ||
|
|
@@ -79,7 +79,7 @@ export const chatHandlers = {
|
|
|
79
79
|
context.bridgeSendToSession(sessionKey, "chat", payload);
|
|
80
80
|
respond(true, { ok: true, aborted: true });
|
|
81
81
|
},
|
|
82
|
-
"chat.send": async ({ params, respond, context
|
|
82
|
+
"chat.send": async ({ params, respond, context }) => {
|
|
83
83
|
if (!validateChatSendParams(params)) {
|
|
84
84
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid chat.send params: ${formatValidationErrors(validateChatSendParams.errors)}`));
|
|
85
85
|
return;
|
package/dist/gateway/server.js
CHANGED
|
@@ -10,6 +10,7 @@ import { createCanvasHostHandler, startCanvasHost, } from "../canvas-host/server
|
|
|
10
10
|
import { createDefaultDeps } from "../cli/deps.js";
|
|
11
11
|
import { getHealthSnapshot } from "../commands/health.js";
|
|
12
12
|
import { CONFIG_PATH_CLAWDBOT, isNixMode, loadConfig, migrateLegacyConfig, readConfigFileSnapshot, STATE_DIR_CLAWDBOT, writeConfigFile, } from "../config/config.js";
|
|
13
|
+
import { deriveDefaultBridgePort, deriveDefaultCanvasHostPort, } from "../config/port-defaults.js";
|
|
13
14
|
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
|
|
14
15
|
import { runCronIsolatedAgentTurn } from "../cron/isolated-agent.js";
|
|
15
16
|
import { appendCronRunLog, resolveCronRunLogPath } from "../cron/run-log.js";
|
|
@@ -195,6 +196,8 @@ async function refreshHealthSnapshot(_opts) {
|
|
|
195
196
|
return healthRefresh;
|
|
196
197
|
}
|
|
197
198
|
export async function startGatewayServer(port = 18789, opts = {}) {
|
|
199
|
+
// Ensure all default port derivations (browser/bridge/canvas) see the actual runtime port.
|
|
200
|
+
process.env.CLAWDBOT_GATEWAY_PORT = String(port);
|
|
198
201
|
const configSnapshot = await readConfigFileSnapshot();
|
|
199
202
|
if (configSnapshot.legacyIssues.length > 0) {
|
|
200
203
|
if (isNixMode) {
|
|
@@ -559,9 +562,11 @@ export async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
559
562
|
}
|
|
560
563
|
if (process.env.CLAWDBOT_BRIDGE_PORT !== undefined) {
|
|
561
564
|
const parsed = Number.parseInt(process.env.CLAWDBOT_BRIDGE_PORT, 10);
|
|
562
|
-
return Number.isFinite(parsed) && parsed > 0
|
|
565
|
+
return Number.isFinite(parsed) && parsed > 0
|
|
566
|
+
? parsed
|
|
567
|
+
: deriveDefaultBridgePort(port);
|
|
563
568
|
}
|
|
564
|
-
return
|
|
569
|
+
return deriveDefaultBridgePort(port);
|
|
565
570
|
})();
|
|
566
571
|
const bridgeHost = (() => {
|
|
567
572
|
// Back-compat: allow an env var override when no bind policy is configured.
|
|
@@ -586,10 +591,16 @@ export async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
586
591
|
return "0.0.0.0";
|
|
587
592
|
})();
|
|
588
593
|
const canvasHostPort = (() => {
|
|
594
|
+
if (process.env.CLAWDBOT_CANVAS_HOST_PORT !== undefined) {
|
|
595
|
+
const parsed = Number.parseInt(process.env.CLAWDBOT_CANVAS_HOST_PORT, 10);
|
|
596
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
597
|
+
return parsed;
|
|
598
|
+
return deriveDefaultCanvasHostPort(port);
|
|
599
|
+
}
|
|
589
600
|
const configured = cfgAtStart.canvasHost?.port;
|
|
590
601
|
if (typeof configured === "number" && configured > 0)
|
|
591
602
|
return configured;
|
|
592
|
-
return
|
|
603
|
+
return deriveDefaultCanvasHostPort(port);
|
|
593
604
|
})();
|
|
594
605
|
if (canvasHostEnabled && bridgeEnabled && bridgeHost) {
|
|
595
606
|
try {
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import dotenv from "dotenv";
|
|
5
4
|
import { getReplyFromConfig } from "./auto-reply/reply.js";
|
|
6
5
|
import { applyTemplate } from "./auto-reply/templating.js";
|
|
7
6
|
import { createDefaultDeps } from "./cli/deps.js";
|
|
@@ -10,6 +9,7 @@ import { waitForever } from "./cli/wait.js";
|
|
|
10
9
|
import { loadConfig } from "./config/config.js";
|
|
11
10
|
import { deriveSessionKey, loadSessionStore, resolveSessionKey, resolveStorePath, saveSessionStore, } from "./config/sessions.js";
|
|
12
11
|
import { ensureBinary } from "./infra/binaries.js";
|
|
12
|
+
import { loadDotEnv } from "./infra/dotenv.js";
|
|
13
13
|
import { normalizeEnv } from "./infra/env.js";
|
|
14
14
|
import { isMainModule } from "./infra/is-main.js";
|
|
15
15
|
import { ensureClawdbotCliOnPath } from "./infra/path-env.js";
|
|
@@ -19,7 +19,7 @@ import { enableConsoleCapture } from "./logging.js";
|
|
|
19
19
|
import { runCommandWithTimeout, runExec } from "./process/exec.js";
|
|
20
20
|
import { monitorWebProvider } from "./provider-web.js";
|
|
21
21
|
import { assertProvider, normalizeE164, toWhatsappJid } from "./utils.js";
|
|
22
|
-
|
|
22
|
+
loadDotEnv({ quiet: true });
|
|
23
23
|
normalizeEnv();
|
|
24
24
|
ensureClawdbotCliOnPath();
|
|
25
25
|
// Capture all console output into structured logs while keeping stdout/stderr behavior.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { runCommandWithTimeout, runExec } from "../process/exec.js";
|
|
4
|
+
import { defaultRuntime } from "../runtime.js";
|
|
5
|
+
export function resolveControlUiRepoRoot(argv1 = process.argv[1]) {
|
|
6
|
+
if (!argv1)
|
|
7
|
+
return null;
|
|
8
|
+
const normalized = path.resolve(argv1);
|
|
9
|
+
const parts = normalized.split(path.sep);
|
|
10
|
+
const srcIndex = parts.lastIndexOf("src");
|
|
11
|
+
if (srcIndex !== -1) {
|
|
12
|
+
const root = parts.slice(0, srcIndex).join(path.sep);
|
|
13
|
+
if (fs.existsSync(path.join(root, "ui", "vite.config.ts")))
|
|
14
|
+
return root;
|
|
15
|
+
}
|
|
16
|
+
let dir = path.dirname(normalized);
|
|
17
|
+
for (let i = 0; i < 8; i++) {
|
|
18
|
+
if (fs.existsSync(path.join(dir, "package.json")) &&
|
|
19
|
+
fs.existsSync(path.join(dir, "ui", "vite.config.ts"))) {
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
const parent = path.dirname(dir);
|
|
23
|
+
if (parent === dir)
|
|
24
|
+
break;
|
|
25
|
+
dir = parent;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export function resolveControlUiDistIndexPath(argv1 = process.argv[1]) {
|
|
30
|
+
if (!argv1)
|
|
31
|
+
return null;
|
|
32
|
+
const normalized = path.resolve(argv1);
|
|
33
|
+
const distDir = path.dirname(normalized);
|
|
34
|
+
if (path.basename(distDir) !== "dist")
|
|
35
|
+
return null;
|
|
36
|
+
return path.join(distDir, "control-ui", "index.html");
|
|
37
|
+
}
|
|
38
|
+
function summarizeCommandOutput(text) {
|
|
39
|
+
const lines = text
|
|
40
|
+
.split(/\r?\n/g)
|
|
41
|
+
.map((l) => l.trim())
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
if (!lines.length)
|
|
44
|
+
return undefined;
|
|
45
|
+
const last = lines.at(-1);
|
|
46
|
+
if (!last)
|
|
47
|
+
return undefined;
|
|
48
|
+
return last.length > 240 ? `${last.slice(0, 239)}…` : last;
|
|
49
|
+
}
|
|
50
|
+
export async function ensureControlUiAssetsBuilt(runtime = defaultRuntime, opts) {
|
|
51
|
+
const indexFromDist = resolveControlUiDistIndexPath(process.argv[1]);
|
|
52
|
+
if (indexFromDist && fs.existsSync(indexFromDist)) {
|
|
53
|
+
return { ok: true, built: false };
|
|
54
|
+
}
|
|
55
|
+
const repoRoot = resolveControlUiRepoRoot(process.argv[1]);
|
|
56
|
+
if (!repoRoot) {
|
|
57
|
+
const hint = indexFromDist
|
|
58
|
+
? `Missing Control UI assets at ${indexFromDist}`
|
|
59
|
+
: "Missing Control UI assets";
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
built: false,
|
|
63
|
+
message: `${hint}. Build them with \`pnpm ui:build\`.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const indexPath = path.join(repoRoot, "dist", "control-ui", "index.html");
|
|
67
|
+
if (fs.existsSync(indexPath)) {
|
|
68
|
+
return { ok: true, built: false };
|
|
69
|
+
}
|
|
70
|
+
const pnpmWhich = process.platform === "win32" ? "where" : "which";
|
|
71
|
+
const pnpm = await runExec(pnpmWhich, ["pnpm"])
|
|
72
|
+
.then((r) => r.stdout
|
|
73
|
+
.split(/\r?\n/g)
|
|
74
|
+
.map((l) => l.trim())
|
|
75
|
+
.find(Boolean) ?? "")
|
|
76
|
+
.catch(() => "");
|
|
77
|
+
if (!pnpm) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
built: false,
|
|
81
|
+
message: "Control UI assets not found and pnpm missing. Install pnpm, then run `pnpm ui:build`.",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
runtime.log("Control UI assets missing; building (pnpm ui:build)…");
|
|
85
|
+
const ensureInstalled = !fs.existsSync(path.join(repoRoot, "ui", "node_modules"));
|
|
86
|
+
if (ensureInstalled) {
|
|
87
|
+
const install = await runCommandWithTimeout([pnpm, "ui:install"], {
|
|
88
|
+
cwd: repoRoot,
|
|
89
|
+
timeoutMs: opts?.timeoutMs ?? 10 * 60_000,
|
|
90
|
+
});
|
|
91
|
+
if (install.code !== 0) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
built: false,
|
|
95
|
+
message: `Control UI install failed: ${summarizeCommandOutput(install.stderr) ?? `exit ${install.code}`}`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const build = await runCommandWithTimeout([pnpm, "ui:build"], {
|
|
100
|
+
cwd: repoRoot,
|
|
101
|
+
timeoutMs: opts?.timeoutMs ?? 10 * 60_000,
|
|
102
|
+
});
|
|
103
|
+
if (build.code !== 0) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
built: false,
|
|
107
|
+
message: `Control UI build failed: ${summarizeCommandOutput(build.stderr) ?? `exit ${build.code}`}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (!fs.existsSync(indexPath)) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
built: true,
|
|
114
|
+
message: `Control UI build completed but ${indexPath} is still missing.`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return { ok: true, built: true };
|
|
118
|
+
}
|