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.
Files changed (116) hide show
  1. package/CHANGELOG.md +32 -6
  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 +9 -16
  71. package/dist/control-ui/assets/index-BFID3yAA.css +0 -1
  72. package/dist/control-ui/assets/index-CE_axlTS.js +0 -2235
  73. package/dist/control-ui/assets/index-CE_axlTS.js.map +0 -1
  74. package/dist/control-ui/index.html +0 -15
  75. package/dist/daemon/constants.js +0 -10
  76. package/dist/daemon/launchd.js +0 -276
  77. package/dist/daemon/legacy.js +0 -63
  78. package/dist/daemon/program-args.js +0 -76
  79. package/dist/daemon/schtasks.js +0 -257
  80. package/dist/daemon/service.js +0 -60
  81. package/dist/daemon/systemd.js +0 -266
  82. package/dist/imessage/client.js +0 -165
  83. package/dist/imessage/index.js +0 -3
  84. package/dist/imessage/monitor.js +0 -272
  85. package/dist/imessage/probe.js +0 -26
  86. package/dist/imessage/send.js +0 -83
  87. package/dist/imessage/targets.js +0 -176
  88. package/dist/signal/client.js +0 -134
  89. package/dist/signal/daemon.js +0 -69
  90. package/dist/signal/index.js +0 -3
  91. package/dist/signal/monitor.js +0 -336
  92. package/dist/signal/probe.js +0 -46
  93. package/dist/signal/send.js +0 -91
  94. package/dist/slack/actions.js +0 -97
  95. package/dist/slack/index.js +0 -5
  96. package/dist/slack/monitor.js +0 -1029
  97. package/dist/slack/probe.js +0 -47
  98. package/dist/slack/send.js +0 -131
  99. package/dist/slack/token.js +0 -10
  100. package/dist/tui/commands.js +0 -74
  101. package/dist/tui/components/assistant-message.js +0 -16
  102. package/dist/tui/components/chat-log.js +0 -92
  103. package/dist/tui/components/custom-editor.js +0 -53
  104. package/dist/tui/components/selectors.js +0 -8
  105. package/dist/tui/components/tool-execution.js +0 -111
  106. package/dist/tui/components/user-message.js +0 -17
  107. package/dist/tui/gateway-chat.js +0 -140
  108. package/dist/tui/layout.js +0 -41
  109. package/dist/tui/message-list.js +0 -57
  110. package/dist/tui/theme/theme.js +0 -80
  111. package/dist/tui/theme.js +0 -25
  112. package/dist/tui/tui.js +0 -708
  113. package/dist/wizard/clack-prompter.js +0 -56
  114. package/dist/wizard/onboarding.js +0 -452
  115. package/dist/wizard/prompts.js +0 -6
  116. package/dist/wizard/session.js +0 -203
@@ -1,1029 +0,0 @@
1
- import bolt from "@slack/bolt";
2
- import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
3
- import { formatAgentEnvelope } from "../auto-reply/envelope.js";
4
- import { getReplyFromConfig } from "../auto-reply/reply.js";
5
- import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
6
- import { loadConfig } from "../config/config.js";
7
- import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
8
- import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
9
- import { enqueueSystemEvent } from "../infra/system-events.js";
10
- import { getChildLogger } from "../logging.js";
11
- import { detectMime } from "../media/mime.js";
12
- import { saveMediaBuffer } from "../media/store.js";
13
- import { sendMessageSlack } from "./send.js";
14
- import { resolveSlackAppToken, resolveSlackBotToken } from "./token.js";
15
- function normalizeSlackSlug(raw) {
16
- const trimmed = raw?.trim().toLowerCase() ?? "";
17
- if (!trimmed)
18
- return "";
19
- const dashed = trimmed.replace(/\s+/g, "-");
20
- const cleaned = dashed.replace(/[^a-z0-9#@._+-]+/g, "-");
21
- return cleaned.replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
22
- }
23
- function normalizeAllowList(list) {
24
- return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean);
25
- }
26
- function normalizeAllowListLower(list) {
27
- return normalizeAllowList(list).map((entry) => entry.toLowerCase());
28
- }
29
- function allowListMatches(params) {
30
- const allowList = params.allowList;
31
- if (allowList.length === 0)
32
- return false;
33
- if (allowList.includes("*"))
34
- return true;
35
- const id = params.id?.toLowerCase();
36
- const name = params.name?.toLowerCase();
37
- const slug = normalizeSlackSlug(name);
38
- const candidates = [
39
- id,
40
- id ? `slack:${id}` : undefined,
41
- id ? `user:${id}` : undefined,
42
- name,
43
- name ? `slack:${name}` : undefined,
44
- slug,
45
- ].filter(Boolean);
46
- return candidates.some((value) => allowList.includes(value));
47
- }
48
- function resolveSlackSlashCommandConfig(raw) {
49
- return {
50
- enabled: raw?.enabled === true,
51
- name: raw?.name?.trim() || "clawd",
52
- sessionPrefix: raw?.sessionPrefix?.trim() || "slack:slash",
53
- ephemeral: raw?.ephemeral !== false,
54
- };
55
- }
56
- function shouldEmitSlackReactionNotification(params) {
57
- const { mode, botId, messageAuthorId, userId, userName, allowlist } = params;
58
- const effectiveMode = mode ?? "own";
59
- if (effectiveMode === "off")
60
- return false;
61
- if (effectiveMode === "own") {
62
- if (!botId || !messageAuthorId)
63
- return false;
64
- return messageAuthorId === botId;
65
- }
66
- if (effectiveMode === "allowlist") {
67
- if (!Array.isArray(allowlist) || allowlist.length === 0)
68
- return false;
69
- const users = normalizeAllowListLower(allowlist);
70
- return allowListMatches({
71
- allowList: users,
72
- id: userId,
73
- name: userName ?? undefined,
74
- });
75
- }
76
- return true;
77
- }
78
- function resolveSlackChannelLabel(params) {
79
- const channelName = params.channelName?.trim();
80
- if (channelName) {
81
- const slug = normalizeSlackSlug(channelName);
82
- return `#${slug || channelName}`;
83
- }
84
- const channelId = params.channelId?.trim();
85
- return channelId ? `#${channelId}` : "unknown channel";
86
- }
87
- function resolveSlackChannelConfig(params) {
88
- const { channelId, channelName, channels } = params;
89
- const entries = channels ?? {};
90
- const keys = Object.keys(entries);
91
- const normalizedName = channelName ? normalizeSlackSlug(channelName) : "";
92
- const directName = channelName ? channelName.trim() : "";
93
- const candidates = [
94
- channelId,
95
- channelName ? `#${directName}` : "",
96
- directName,
97
- normalizedName,
98
- ].filter(Boolean);
99
- let matched;
100
- for (const candidate of candidates) {
101
- if (candidate && entries[candidate]) {
102
- matched = entries[candidate];
103
- break;
104
- }
105
- }
106
- const fallback = entries["*"];
107
- if (keys.length === 0) {
108
- return { allowed: true, requireMention: true };
109
- }
110
- if (!matched && !fallback) {
111
- return { allowed: false, requireMention: true };
112
- }
113
- const resolved = matched ?? fallback ?? {};
114
- const allowed = resolved.allow ?? true;
115
- const requireMention = resolved.requireMention ?? fallback?.requireMention ?? true;
116
- return { allowed, requireMention };
117
- }
118
- async function resolveSlackMedia(params) {
119
- const files = params.files ?? [];
120
- for (const file of files) {
121
- const url = file.url_private_download ?? file.url_private;
122
- if (!url)
123
- continue;
124
- try {
125
- const res = await fetch(url, {
126
- headers: { Authorization: `Bearer ${params.token}` },
127
- });
128
- if (!res.ok)
129
- continue;
130
- const buffer = Buffer.from(await res.arrayBuffer());
131
- if (buffer.byteLength > params.maxBytes)
132
- continue;
133
- const contentType = await detectMime({
134
- buffer,
135
- headerMime: res.headers.get("content-type"),
136
- filePath: file.name,
137
- });
138
- const saved = await saveMediaBuffer(buffer, contentType ?? file.mimetype, "inbound", params.maxBytes);
139
- return {
140
- path: saved.path,
141
- contentType: saved.contentType,
142
- placeholder: file.name ? `[Slack file: ${file.name}]` : "[Slack file]",
143
- };
144
- }
145
- catch {
146
- // Ignore download failures and fall through to the next file.
147
- }
148
- }
149
- return null;
150
- }
151
- export async function monitorSlackProvider(opts = {}) {
152
- const cfg = loadConfig();
153
- const botToken = resolveSlackBotToken(opts.botToken ??
154
- process.env.SLACK_BOT_TOKEN ??
155
- cfg.slack?.botToken ??
156
- undefined);
157
- const appToken = resolveSlackAppToken(opts.appToken ??
158
- process.env.SLACK_APP_TOKEN ??
159
- cfg.slack?.appToken ??
160
- undefined);
161
- if (!botToken || !appToken) {
162
- throw new Error("SLACK_BOT_TOKEN and SLACK_APP_TOKEN (or slack.botToken/slack.appToken) are required for Slack socket mode");
163
- }
164
- const runtime = opts.runtime ?? {
165
- log: console.log,
166
- error: console.error,
167
- exit: (code) => {
168
- throw new Error(`exit ${code}`);
169
- },
170
- };
171
- const dmConfig = cfg.slack?.dm;
172
- const allowFrom = normalizeAllowList(dmConfig?.allowFrom);
173
- const groupDmEnabled = dmConfig?.groupEnabled ?? false;
174
- const groupDmChannels = normalizeAllowList(dmConfig?.groupChannels);
175
- const channelsConfig = cfg.slack?.channels;
176
- const dmEnabled = dmConfig?.enabled ?? true;
177
- const reactionMode = cfg.slack?.reactionNotifications ?? "own";
178
- const reactionAllowlist = cfg.slack?.reactionAllowlist ?? [];
179
- const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? cfg.slack?.slashCommand);
180
- const textLimit = resolveTextChunkLimit(cfg, "slack");
181
- const mediaMaxBytes = (opts.mediaMaxMb ?? cfg.slack?.mediaMaxMb ?? 20) * 1024 * 1024;
182
- const logger = getChildLogger({ module: "slack-auto-reply" });
183
- const channelCache = new Map();
184
- const userCache = new Map();
185
- const seenMessages = new Map();
186
- const markMessageSeen = (channelId, ts) => {
187
- if (!channelId || !ts)
188
- return false;
189
- const key = `${channelId}:${ts}`;
190
- if (seenMessages.has(key))
191
- return true;
192
- seenMessages.set(key, Date.now());
193
- if (seenMessages.size > 500) {
194
- const cutoff = Date.now() - 60_000;
195
- for (const [entry, seenAt] of seenMessages) {
196
- if (seenAt < cutoff || seenMessages.size > 450) {
197
- seenMessages.delete(entry);
198
- }
199
- else {
200
- break;
201
- }
202
- }
203
- }
204
- return false;
205
- };
206
- const { App } = bolt;
207
- const app = new App({
208
- token: botToken,
209
- appToken,
210
- socketMode: true,
211
- });
212
- let botUserId = "";
213
- try {
214
- const auth = await app.client.auth.test({ token: botToken });
215
- botUserId = auth.user_id ?? "";
216
- }
217
- catch (err) {
218
- runtime.error?.(danger(`slack auth failed: ${String(err)}`));
219
- }
220
- const resolveChannelName = async (channelId) => {
221
- const cached = channelCache.get(channelId);
222
- if (cached)
223
- return cached;
224
- try {
225
- const info = await app.client.conversations.info({
226
- token: botToken,
227
- channel: channelId,
228
- });
229
- const name = info.channel && "name" in info.channel ? info.channel.name : undefined;
230
- const channel = info.channel ?? undefined;
231
- const type = channel?.is_im
232
- ? "im"
233
- : channel?.is_mpim
234
- ? "mpim"
235
- : channel?.is_channel
236
- ? "channel"
237
- : channel?.is_group
238
- ? "group"
239
- : undefined;
240
- const entry = { name, type };
241
- channelCache.set(channelId, entry);
242
- return entry;
243
- }
244
- catch {
245
- return {};
246
- }
247
- };
248
- const resolveUserName = async (userId) => {
249
- const cached = userCache.get(userId);
250
- if (cached)
251
- return cached;
252
- try {
253
- const info = await app.client.users.info({
254
- token: botToken,
255
- user: userId,
256
- });
257
- const profile = info.user?.profile;
258
- const name = profile?.display_name ||
259
- profile?.real_name ||
260
- info.user?.name ||
261
- undefined;
262
- const entry = { name };
263
- userCache.set(userId, entry);
264
- return entry;
265
- }
266
- catch {
267
- return {};
268
- }
269
- };
270
- const isChannelAllowed = (params) => {
271
- const channelType = params.channelType;
272
- const isDirectMessage = channelType === "im";
273
- const isGroupDm = channelType === "mpim";
274
- const isRoom = channelType === "channel" || channelType === "group";
275
- if (isDirectMessage && !dmEnabled)
276
- return false;
277
- if (isGroupDm && !groupDmEnabled)
278
- return false;
279
- if (isGroupDm && groupDmChannels.length > 0) {
280
- const allowList = normalizeAllowListLower(groupDmChannels);
281
- const candidates = [
282
- params.channelId,
283
- params.channelName ? `#${params.channelName}` : undefined,
284
- params.channelName,
285
- params.channelName ? normalizeSlackSlug(params.channelName) : undefined,
286
- ]
287
- .filter((value) => Boolean(value))
288
- .map((value) => value.toLowerCase());
289
- const permitted = allowList.includes("*") ||
290
- candidates.some((candidate) => allowList.includes(candidate));
291
- if (!permitted)
292
- return false;
293
- }
294
- if (isRoom && params.channelId) {
295
- const channelConfig = resolveSlackChannelConfig({
296
- channelId: params.channelId,
297
- channelName: params.channelName,
298
- channels: channelsConfig,
299
- });
300
- if (channelConfig?.allowed === false)
301
- return false;
302
- }
303
- return true;
304
- };
305
- const handleSlackMessage = async (message, opts) => {
306
- if (opts.source === "message" && message.type !== "message")
307
- return;
308
- if (message.bot_id)
309
- return;
310
- if (opts.source === "message" &&
311
- message.subtype &&
312
- message.subtype !== "file_share") {
313
- return;
314
- }
315
- if (!message.user)
316
- return;
317
- if (markMessageSeen(message.channel, message.ts))
318
- return;
319
- let channelInfo = {};
320
- let channelType = message.channel_type;
321
- if (!channelType || channelType !== "im") {
322
- channelInfo = await resolveChannelName(message.channel);
323
- channelType = channelType ?? channelInfo.type;
324
- }
325
- const channelName = channelInfo?.name;
326
- const resolvedChannelType = channelType;
327
- const isDirectMessage = resolvedChannelType === "im";
328
- const isGroupDm = resolvedChannelType === "mpim";
329
- const isRoom = resolvedChannelType === "channel" || resolvedChannelType === "group";
330
- if (!isChannelAllowed({
331
- channelId: message.channel,
332
- channelName,
333
- channelType: resolvedChannelType,
334
- })) {
335
- logVerbose("slack: drop message (channel not allowed)");
336
- return;
337
- }
338
- if (isDirectMessage && allowFrom.length > 0) {
339
- const permitted = allowListMatches({
340
- allowList: normalizeAllowListLower(allowFrom),
341
- id: message.user,
342
- });
343
- if (!permitted) {
344
- logVerbose(`Blocked unauthorized slack sender ${message.user} (not in allowFrom)`);
345
- return;
346
- }
347
- }
348
- const channelConfig = isRoom
349
- ? resolveSlackChannelConfig({
350
- channelId: message.channel,
351
- channelName,
352
- channels: channelsConfig,
353
- })
354
- : null;
355
- const wasMentioned = opts.wasMentioned ??
356
- (!isDirectMessage &&
357
- Boolean(botUserId && message.text?.includes(`<@${botUserId}>`)));
358
- if (isRoom && channelConfig?.requireMention && !wasMentioned) {
359
- logger.info({ channel: message.channel, reason: "no-mention" }, "skipping room message");
360
- return;
361
- }
362
- const media = await resolveSlackMedia({
363
- files: message.files,
364
- token: botToken,
365
- maxBytes: mediaMaxBytes,
366
- });
367
- const rawBody = (message.text ?? "").trim() || media?.placeholder || "";
368
- if (!rawBody)
369
- return;
370
- const sender = await resolveUserName(message.user);
371
- const senderName = sender?.name ?? message.user;
372
- const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`;
373
- const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
374
- const inboundLabel = isDirectMessage
375
- ? `Slack DM from ${senderName}`
376
- : `Slack message in ${roomLabel} from ${senderName}`;
377
- enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
378
- contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`,
379
- });
380
- const textWithId = `${rawBody}\n[slack message id: ${message.ts} channel: ${message.channel}]`;
381
- const body = formatAgentEnvelope({
382
- surface: "Slack",
383
- from: senderName,
384
- timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
385
- body: textWithId,
386
- });
387
- const isRoomish = isRoom || isGroupDm;
388
- const ctxPayload = {
389
- Body: body,
390
- From: isDirectMessage
391
- ? `slack:${message.user}`
392
- : isRoom
393
- ? `slack:channel:${message.channel}`
394
- : `slack:group:${message.channel}`,
395
- To: isDirectMessage
396
- ? `user:${message.user}`
397
- : `channel:${message.channel}`,
398
- ChatType: isDirectMessage ? "direct" : isRoom ? "room" : "group",
399
- GroupSubject: isRoomish ? roomLabel : undefined,
400
- SenderName: senderName,
401
- Surface: "slack",
402
- MessageSid: message.ts,
403
- ReplyToId: message.thread_ts ?? message.ts,
404
- Timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
405
- WasMentioned: isRoomish ? wasMentioned : undefined,
406
- MediaPath: media?.path,
407
- MediaType: media?.contentType,
408
- MediaUrl: media?.path,
409
- };
410
- const replyTarget = ctxPayload.To ?? undefined;
411
- if (!replyTarget) {
412
- runtime.error?.(danger("slack: missing reply target"));
413
- return;
414
- }
415
- if (isDirectMessage) {
416
- const sessionCfg = cfg.session;
417
- const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
418
- const storePath = resolveStorePath(sessionCfg?.store);
419
- await updateLastRoute({
420
- storePath,
421
- sessionKey: mainKey,
422
- channel: "slack",
423
- to: `user:${message.user}`,
424
- });
425
- }
426
- if (shouldLogVerbose()) {
427
- logVerbose(`slack inbound: channel=${message.channel} from=${ctxPayload.From} preview="${preview}"`);
428
- }
429
- let blockSendChain = Promise.resolve();
430
- const sendBlockReply = (payload) => {
431
- if (!payload?.text &&
432
- !payload?.mediaUrl &&
433
- !(payload?.mediaUrls?.length ?? 0)) {
434
- return;
435
- }
436
- blockSendChain = blockSendChain
437
- .then(async () => {
438
- await deliverReplies({
439
- replies: [payload],
440
- target: replyTarget,
441
- token: botToken,
442
- runtime,
443
- textLimit,
444
- });
445
- })
446
- .catch((err) => {
447
- runtime.error?.(danger(`slack block reply failed: ${String(err)}`));
448
- });
449
- };
450
- const replyResult = await getReplyFromConfig(ctxPayload, {
451
- onBlockReply: sendBlockReply,
452
- }, cfg);
453
- const replies = replyResult
454
- ? Array.isArray(replyResult)
455
- ? replyResult
456
- : [replyResult]
457
- : [];
458
- await blockSendChain;
459
- if (replies.length === 0)
460
- return;
461
- await deliverReplies({
462
- replies,
463
- target: replyTarget,
464
- token: botToken,
465
- runtime,
466
- textLimit,
467
- });
468
- if (shouldLogVerbose()) {
469
- logVerbose(`slack: delivered ${replies.length} reply${replies.length === 1 ? "" : "ies"} to ${replyTarget}`);
470
- }
471
- };
472
- app.event("message", async ({ event }) => {
473
- try {
474
- const message = event;
475
- if (message.subtype === "message_changed") {
476
- const changed = event;
477
- const channelId = changed.channel;
478
- const channelInfo = channelId
479
- ? await resolveChannelName(channelId)
480
- : {};
481
- const channelType = channelInfo?.type;
482
- if (!isChannelAllowed({
483
- channelId,
484
- channelName: channelInfo?.name,
485
- channelType,
486
- })) {
487
- return;
488
- }
489
- const messageId = changed.message?.ts ?? changed.previous_message?.ts;
490
- const label = resolveSlackChannelLabel({
491
- channelId,
492
- channelName: channelInfo?.name,
493
- });
494
- enqueueSystemEvent(`Slack message edited in ${label}.`, {
495
- contextKey: `slack:message:changed:${channelId ?? "unknown"}:${messageId ?? changed.event_ts ?? "unknown"}`,
496
- });
497
- return;
498
- }
499
- if (message.subtype === "message_deleted") {
500
- const deleted = event;
501
- const channelId = deleted.channel;
502
- const channelInfo = channelId
503
- ? await resolveChannelName(channelId)
504
- : {};
505
- const channelType = channelInfo?.type;
506
- if (!isChannelAllowed({
507
- channelId,
508
- channelName: channelInfo?.name,
509
- channelType,
510
- })) {
511
- return;
512
- }
513
- const label = resolveSlackChannelLabel({
514
- channelId,
515
- channelName: channelInfo?.name,
516
- });
517
- enqueueSystemEvent(`Slack message deleted in ${label}.`, {
518
- contextKey: `slack:message:deleted:${channelId ?? "unknown"}:${deleted.deleted_ts ?? deleted.event_ts ?? "unknown"}`,
519
- });
520
- return;
521
- }
522
- if (message.subtype === "thread_broadcast") {
523
- const thread = event;
524
- const channelId = thread.channel;
525
- const channelInfo = channelId
526
- ? await resolveChannelName(channelId)
527
- : {};
528
- const channelType = channelInfo?.type;
529
- if (!isChannelAllowed({
530
- channelId,
531
- channelName: channelInfo?.name,
532
- channelType,
533
- })) {
534
- return;
535
- }
536
- const label = resolveSlackChannelLabel({
537
- channelId,
538
- channelName: channelInfo?.name,
539
- });
540
- const messageId = thread.message?.ts ?? thread.event_ts;
541
- enqueueSystemEvent(`Slack thread reply broadcast in ${label}.`, {
542
- contextKey: `slack:thread:broadcast:${channelId ?? "unknown"}:${messageId ?? "unknown"}`,
543
- });
544
- return;
545
- }
546
- await handleSlackMessage(message, { source: "message" });
547
- }
548
- catch (err) {
549
- runtime.error?.(danger(`slack handler failed: ${String(err)}`));
550
- }
551
- });
552
- app.event("app_mention", async ({ event }) => {
553
- try {
554
- const mention = event;
555
- await handleSlackMessage(mention, {
556
- source: "app_mention",
557
- wasMentioned: true,
558
- });
559
- }
560
- catch (err) {
561
- runtime.error?.(danger(`slack mention handler failed: ${String(err)}`));
562
- }
563
- });
564
- const handleReactionEvent = async (event, action) => {
565
- try {
566
- const item = event.item;
567
- if (!event.user)
568
- return;
569
- if (!item?.channel || !item?.ts)
570
- return;
571
- if (item.type && item.type !== "message")
572
- return;
573
- if (botUserId && event.user === botUserId)
574
- return;
575
- const channelInfo = await resolveChannelName(item.channel);
576
- const channelType = channelInfo?.type;
577
- const isDirectMessage = channelType === "im";
578
- const isGroupDm = channelType === "mpim";
579
- const isRoom = channelType === "channel" || channelType === "group";
580
- const channelName = channelInfo?.name;
581
- if (isDirectMessage && !dmEnabled)
582
- return;
583
- if (isGroupDm && !groupDmEnabled)
584
- return;
585
- if (isGroupDm && groupDmChannels.length > 0) {
586
- const allowList = normalizeAllowListLower(groupDmChannels);
587
- const candidates = [
588
- item.channel,
589
- channelName ? `#${channelName}` : undefined,
590
- channelName,
591
- channelName ? normalizeSlackSlug(channelName) : undefined,
592
- ]
593
- .filter((value) => Boolean(value))
594
- .map((value) => value.toLowerCase());
595
- const permitted = allowList.includes("*") ||
596
- candidates.some((candidate) => allowList.includes(candidate));
597
- if (!permitted)
598
- return;
599
- }
600
- if (isRoom) {
601
- const channelConfig = resolveSlackChannelConfig({
602
- channelId: item.channel,
603
- channelName,
604
- channels: channelsConfig,
605
- });
606
- if (channelConfig?.allowed === false)
607
- return;
608
- }
609
- const actor = await resolveUserName(event.user);
610
- const shouldNotify = shouldEmitSlackReactionNotification({
611
- mode: reactionMode,
612
- botId: botUserId,
613
- messageAuthorId: event.item_user ?? undefined,
614
- userId: event.user,
615
- userName: actor?.name ?? undefined,
616
- allowlist: reactionAllowlist,
617
- });
618
- if (!shouldNotify)
619
- return;
620
- const emojiLabel = event.reaction ?? "emoji";
621
- const actorLabel = actor?.name ?? event.user;
622
- const channelLabel = channelName
623
- ? `#${normalizeSlackSlug(channelName) || channelName}`
624
- : `#${item.channel}`;
625
- const authorInfo = event.item_user
626
- ? await resolveUserName(event.item_user)
627
- : undefined;
628
- const authorLabel = authorInfo?.name ?? event.item_user;
629
- const baseText = `Slack reaction ${action}: :${emojiLabel}: by ${actorLabel} in ${channelLabel} msg ${item.ts}`;
630
- const text = authorLabel ? `${baseText} from ${authorLabel}` : baseText;
631
- enqueueSystemEvent(text, {
632
- contextKey: `slack:reaction:${action}:${item.channel}:${item.ts}:${event.user}:${emojiLabel}`,
633
- });
634
- }
635
- catch (err) {
636
- runtime.error?.(danger(`slack reaction handler failed: ${String(err)}`));
637
- }
638
- };
639
- app.event("reaction_added", async ({ event }) => {
640
- await handleReactionEvent(event, "added");
641
- });
642
- app.event("reaction_removed", async ({ event }) => {
643
- await handleReactionEvent(event, "removed");
644
- });
645
- app.event("member_joined_channel", async ({ event }) => {
646
- try {
647
- const payload = event;
648
- const channelId = payload.channel;
649
- const channelInfo = channelId
650
- ? await resolveChannelName(channelId)
651
- : {};
652
- const channelType = payload.channel_type ?? channelInfo?.type;
653
- if (!isChannelAllowed({
654
- channelId,
655
- channelName: channelInfo?.name,
656
- channelType,
657
- })) {
658
- return;
659
- }
660
- const userInfo = payload.user
661
- ? await resolveUserName(payload.user)
662
- : {};
663
- const userLabel = userInfo?.name ?? payload.user ?? "someone";
664
- const label = resolveSlackChannelLabel({
665
- channelId,
666
- channelName: channelInfo?.name,
667
- });
668
- enqueueSystemEvent(`Slack: ${userLabel} joined ${label}.`, {
669
- contextKey: `slack:member:joined:${channelId ?? "unknown"}:${payload.user ?? "unknown"}`,
670
- });
671
- }
672
- catch (err) {
673
- runtime.error?.(danger(`slack join handler failed: ${String(err)}`));
674
- }
675
- });
676
- app.event("member_left_channel", async ({ event }) => {
677
- try {
678
- const payload = event;
679
- const channelId = payload.channel;
680
- const channelInfo = channelId
681
- ? await resolveChannelName(channelId)
682
- : {};
683
- const channelType = payload.channel_type ?? channelInfo?.type;
684
- if (!isChannelAllowed({
685
- channelId,
686
- channelName: channelInfo?.name,
687
- channelType,
688
- })) {
689
- return;
690
- }
691
- const userInfo = payload.user
692
- ? await resolveUserName(payload.user)
693
- : {};
694
- const userLabel = userInfo?.name ?? payload.user ?? "someone";
695
- const label = resolveSlackChannelLabel({
696
- channelId,
697
- channelName: channelInfo?.name,
698
- });
699
- enqueueSystemEvent(`Slack: ${userLabel} left ${label}.`, {
700
- contextKey: `slack:member:left:${channelId ?? "unknown"}:${payload.user ?? "unknown"}`,
701
- });
702
- }
703
- catch (err) {
704
- runtime.error?.(danger(`slack leave handler failed: ${String(err)}`));
705
- }
706
- });
707
- app.event("channel_created", async ({ event }) => {
708
- try {
709
- const payload = event;
710
- const channelId = payload.channel?.id;
711
- const channelName = payload.channel?.name;
712
- if (!isChannelAllowed({
713
- channelId,
714
- channelName,
715
- channelType: "channel",
716
- })) {
717
- return;
718
- }
719
- const label = resolveSlackChannelLabel({ channelId, channelName });
720
- enqueueSystemEvent(`Slack channel created: ${label}.`, {
721
- contextKey: `slack:channel:created:${channelId ?? channelName ?? "unknown"}`,
722
- });
723
- }
724
- catch (err) {
725
- runtime.error?.(danger(`slack channel created handler failed: ${String(err)}`));
726
- }
727
- });
728
- app.event("channel_rename", async ({ event }) => {
729
- try {
730
- const payload = event;
731
- const channelId = payload.channel?.id;
732
- const channelName = payload.channel?.name_normalized ?? payload.channel?.name;
733
- if (!isChannelAllowed({
734
- channelId,
735
- channelName,
736
- channelType: "channel",
737
- })) {
738
- return;
739
- }
740
- const label = resolveSlackChannelLabel({ channelId, channelName });
741
- enqueueSystemEvent(`Slack channel renamed: ${label}.`, {
742
- contextKey: `slack:channel:renamed:${channelId ?? channelName ?? "unknown"}`,
743
- });
744
- }
745
- catch (err) {
746
- runtime.error?.(danger(`slack channel rename handler failed: ${String(err)}`));
747
- }
748
- });
749
- app.event("pin_added", async ({ event }) => {
750
- try {
751
- const payload = event;
752
- const channelId = payload.channel_id;
753
- const channelInfo = channelId
754
- ? await resolveChannelName(channelId)
755
- : {};
756
- if (!isChannelAllowed({
757
- channelId,
758
- channelName: channelInfo?.name,
759
- channelType: channelInfo?.type,
760
- })) {
761
- return;
762
- }
763
- const label = resolveSlackChannelLabel({
764
- channelId,
765
- channelName: channelInfo?.name,
766
- });
767
- const userInfo = payload.user
768
- ? await resolveUserName(payload.user)
769
- : {};
770
- const userLabel = userInfo?.name ?? payload.user ?? "someone";
771
- const itemType = payload.item?.type ?? "item";
772
- const messageId = payload.item?.message?.ts ?? payload.event_ts;
773
- enqueueSystemEvent(`Slack: ${userLabel} pinned a ${itemType} in ${label}.`, {
774
- contextKey: `slack:pin:added:${channelId ?? "unknown"}:${messageId ?? "unknown"}`,
775
- });
776
- }
777
- catch (err) {
778
- runtime.error?.(danger(`slack pin added handler failed: ${String(err)}`));
779
- }
780
- });
781
- app.event("pin_removed", async ({ event }) => {
782
- try {
783
- const payload = event;
784
- const channelId = payload.channel_id;
785
- const channelInfo = channelId
786
- ? await resolveChannelName(channelId)
787
- : {};
788
- if (!isChannelAllowed({
789
- channelId,
790
- channelName: channelInfo?.name,
791
- channelType: channelInfo?.type,
792
- })) {
793
- return;
794
- }
795
- const label = resolveSlackChannelLabel({
796
- channelId,
797
- channelName: channelInfo?.name,
798
- });
799
- const userInfo = payload.user
800
- ? await resolveUserName(payload.user)
801
- : {};
802
- const userLabel = userInfo?.name ?? payload.user ?? "someone";
803
- const itemType = payload.item?.type ?? "item";
804
- const messageId = payload.item?.message?.ts ?? payload.event_ts;
805
- enqueueSystemEvent(`Slack: ${userLabel} unpinned a ${itemType} in ${label}.`, {
806
- contextKey: `slack:pin:removed:${channelId ?? "unknown"}:${messageId ?? "unknown"}`,
807
- });
808
- }
809
- catch (err) {
810
- runtime.error?.(danger(`slack pin removed handler failed: ${String(err)}`));
811
- }
812
- });
813
- if (slashCommand.enabled) {
814
- app.command(slashCommand.name, async ({ command, ack, respond }) => {
815
- try {
816
- const prompt = command.text?.trim();
817
- if (!prompt) {
818
- await ack({
819
- text: "Message required.",
820
- response_type: "ephemeral",
821
- });
822
- return;
823
- }
824
- await ack();
825
- if (botUserId && command.user_id === botUserId)
826
- return;
827
- const channelInfo = await resolveChannelName(command.channel_id);
828
- const channelType = channelInfo?.type ??
829
- (command.channel_name === "directmessage" ? "im" : undefined);
830
- const isDirectMessage = channelType === "im";
831
- const isGroupDm = channelType === "mpim";
832
- const isRoom = channelType === "channel" || channelType === "group";
833
- if (isDirectMessage && !dmEnabled) {
834
- await respond({
835
- text: "Slack DMs are disabled.",
836
- response_type: "ephemeral",
837
- });
838
- return;
839
- }
840
- if (isGroupDm && !groupDmEnabled) {
841
- await respond({
842
- text: "Slack group DMs are disabled.",
843
- response_type: "ephemeral",
844
- });
845
- return;
846
- }
847
- if (isGroupDm && groupDmChannels.length > 0) {
848
- const allowList = normalizeAllowListLower(groupDmChannels);
849
- const channelName = channelInfo?.name;
850
- const candidates = [
851
- command.channel_id,
852
- channelName ? `#${channelName}` : undefined,
853
- channelName,
854
- channelName ? normalizeSlackSlug(channelName) : undefined,
855
- ]
856
- .filter((value) => Boolean(value))
857
- .map((value) => value.toLowerCase());
858
- const permitted = allowList.includes("*") ||
859
- candidates.some((candidate) => allowList.includes(candidate));
860
- if (!permitted) {
861
- await respond({
862
- text: "This group DM is not allowed.",
863
- response_type: "ephemeral",
864
- });
865
- return;
866
- }
867
- }
868
- if (isDirectMessage && allowFrom.length > 0) {
869
- const sender = await resolveUserName(command.user_id);
870
- const permitted = allowListMatches({
871
- allowList: normalizeAllowListLower(allowFrom),
872
- id: command.user_id,
873
- name: sender?.name ?? undefined,
874
- });
875
- if (!permitted) {
876
- await respond({
877
- text: "You are not authorized to use this command.",
878
- response_type: "ephemeral",
879
- });
880
- return;
881
- }
882
- }
883
- if (isRoom) {
884
- const channelConfig = resolveSlackChannelConfig({
885
- channelId: command.channel_id,
886
- channelName: channelInfo?.name,
887
- channels: channelsConfig,
888
- });
889
- if (channelConfig?.allowed === false) {
890
- await respond({
891
- text: "This channel is not allowed.",
892
- response_type: "ephemeral",
893
- });
894
- return;
895
- }
896
- }
897
- const sender = await resolveUserName(command.user_id);
898
- const senderName = sender?.name ?? command.user_name ?? command.user_id;
899
- const channelName = channelInfo?.name;
900
- const roomLabel = channelName
901
- ? `#${channelName}`
902
- : `#${command.channel_id}`;
903
- const isRoomish = isRoom || isGroupDm;
904
- const ctxPayload = {
905
- Body: prompt,
906
- From: isDirectMessage
907
- ? `slack:${command.user_id}`
908
- : isRoom
909
- ? `slack:channel:${command.channel_id}`
910
- : `slack:group:${command.channel_id}`,
911
- To: `slash:${command.user_id}`,
912
- ChatType: isDirectMessage ? "direct" : isRoom ? "room" : "group",
913
- GroupSubject: isRoomish ? roomLabel : undefined,
914
- SenderName: senderName,
915
- Surface: "slack",
916
- WasMentioned: true,
917
- MessageSid: command.trigger_id,
918
- Timestamp: Date.now(),
919
- SessionKey: `${slashCommand.sessionPrefix}:${command.user_id}`,
920
- };
921
- const replyResult = await getReplyFromConfig(ctxPayload, undefined, cfg);
922
- const replies = replyResult
923
- ? Array.isArray(replyResult)
924
- ? replyResult
925
- : [replyResult]
926
- : [];
927
- await deliverSlackSlashReplies({
928
- replies,
929
- respond,
930
- ephemeral: slashCommand.ephemeral,
931
- textLimit,
932
- });
933
- }
934
- catch (err) {
935
- runtime.error?.(danger(`slack slash handler failed: ${String(err)}`));
936
- await respond({
937
- text: "Sorry, something went wrong handling that command.",
938
- response_type: "ephemeral",
939
- });
940
- }
941
- });
942
- }
943
- const stopOnAbort = () => {
944
- if (opts.abortSignal?.aborted)
945
- void app.stop();
946
- };
947
- opts.abortSignal?.addEventListener("abort", stopOnAbort, { once: true });
948
- try {
949
- await app.start();
950
- runtime.log?.("slack socket mode connected");
951
- if (opts.abortSignal?.aborted)
952
- return;
953
- await new Promise((resolve) => {
954
- opts.abortSignal?.addEventListener("abort", () => resolve(), {
955
- once: true,
956
- });
957
- });
958
- }
959
- finally {
960
- opts.abortSignal?.removeEventListener("abort", stopOnAbort);
961
- await app.stop().catch(() => undefined);
962
- }
963
- }
964
- async function deliverReplies(params) {
965
- const chunkLimit = Math.min(params.textLimit, 4000);
966
- for (const payload of params.replies) {
967
- const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
968
- const text = payload.text ?? "";
969
- if (!text && mediaList.length === 0)
970
- continue;
971
- if (mediaList.length === 0) {
972
- for (const chunk of chunkText(text, chunkLimit)) {
973
- const threadTs = undefined;
974
- const trimmed = chunk.trim();
975
- if (!trimmed || trimmed === SILENT_REPLY_TOKEN)
976
- continue;
977
- await sendMessageSlack(params.target, trimmed, {
978
- token: params.token,
979
- threadTs,
980
- });
981
- }
982
- }
983
- else {
984
- let first = true;
985
- for (const mediaUrl of mediaList) {
986
- const caption = first ? text : "";
987
- first = false;
988
- const threadTs = undefined;
989
- await sendMessageSlack(params.target, caption, {
990
- token: params.token,
991
- mediaUrl,
992
- threadTs,
993
- });
994
- }
995
- }
996
- params.runtime.log?.(`delivered reply to ${params.target}`);
997
- }
998
- }
999
- async function deliverSlackSlashReplies(params) {
1000
- const messages = [];
1001
- const chunkLimit = Math.min(params.textLimit, 4000);
1002
- for (const payload of params.replies) {
1003
- const textRaw = payload.text?.trim() ?? "";
1004
- const text = textRaw && textRaw !== SILENT_REPLY_TOKEN ? textRaw : undefined;
1005
- const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
1006
- const combined = [
1007
- text ?? "",
1008
- ...mediaList.map((url) => url.trim()).filter(Boolean),
1009
- ]
1010
- .filter(Boolean)
1011
- .join("\n");
1012
- if (!combined)
1013
- continue;
1014
- for (const chunk of chunkText(combined, chunkLimit)) {
1015
- messages.push(chunk);
1016
- }
1017
- }
1018
- if (messages.length === 0) {
1019
- await params.respond({
1020
- text: "No response was generated for that command.",
1021
- response_type: "ephemeral",
1022
- });
1023
- return;
1024
- }
1025
- const responseType = params.ephemeral ? "ephemeral" : "in_channel";
1026
- for (const message of messages) {
1027
- await params.respond({ text: message, response_type: responseType });
1028
- }
1029
- }