@geminixiang/mama 0.1.0

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 (83) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +158 -0
  3. package/dist/adapter.d.ts +38 -0
  4. package/dist/adapter.d.ts.map +1 -0
  5. package/dist/adapter.js +2 -0
  6. package/dist/adapter.js.map +1 -0
  7. package/dist/adapters/slack/bot.d.ts +130 -0
  8. package/dist/adapters/slack/bot.d.ts.map +1 -0
  9. package/dist/adapters/slack/bot.js +516 -0
  10. package/dist/adapters/slack/bot.js.map +1 -0
  11. package/dist/adapters/slack/context.d.ts +11 -0
  12. package/dist/adapters/slack/context.d.ts.map +1 -0
  13. package/dist/adapters/slack/context.js +178 -0
  14. package/dist/adapters/slack/context.js.map +1 -0
  15. package/dist/adapters/slack/index.d.ts +3 -0
  16. package/dist/adapters/slack/index.d.ts.map +1 -0
  17. package/dist/adapters/slack/index.js +3 -0
  18. package/dist/adapters/slack/index.js.map +1 -0
  19. package/dist/adapters/slack/tools/attach.d.ts +12 -0
  20. package/dist/adapters/slack/tools/attach.d.ts.map +1 -0
  21. package/dist/adapters/slack/tools/attach.js +38 -0
  22. package/dist/adapters/slack/tools/attach.js.map +1 -0
  23. package/dist/agent.d.ts +26 -0
  24. package/dist/agent.d.ts.map +1 -0
  25. package/dist/agent.js +763 -0
  26. package/dist/agent.js.map +1 -0
  27. package/dist/config.d.ts +10 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/config.js +54 -0
  30. package/dist/config.js.map +1 -0
  31. package/dist/context.d.ts +34 -0
  32. package/dist/context.d.ts.map +1 -0
  33. package/dist/context.js +110 -0
  34. package/dist/context.js.map +1 -0
  35. package/dist/download.d.ts +2 -0
  36. package/dist/download.d.ts.map +1 -0
  37. package/dist/download.js +89 -0
  38. package/dist/download.js.map +1 -0
  39. package/dist/events.d.ts +57 -0
  40. package/dist/events.d.ts.map +1 -0
  41. package/dist/events.js +310 -0
  42. package/dist/events.js.map +1 -0
  43. package/dist/log.d.ts +39 -0
  44. package/dist/log.d.ts.map +1 -0
  45. package/dist/log.js +222 -0
  46. package/dist/log.js.map +1 -0
  47. package/dist/main.d.ts +3 -0
  48. package/dist/main.d.ts.map +1 -0
  49. package/dist/main.js +247 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/sandbox.d.ts +34 -0
  52. package/dist/sandbox.d.ts.map +1 -0
  53. package/dist/sandbox.js +183 -0
  54. package/dist/sandbox.js.map +1 -0
  55. package/dist/store.d.ts +60 -0
  56. package/dist/store.d.ts.map +1 -0
  57. package/dist/store.js +180 -0
  58. package/dist/store.js.map +1 -0
  59. package/dist/tools/bash.d.ts +10 -0
  60. package/dist/tools/bash.d.ts.map +1 -0
  61. package/dist/tools/bash.js +78 -0
  62. package/dist/tools/bash.js.map +1 -0
  63. package/dist/tools/edit.d.ts +11 -0
  64. package/dist/tools/edit.d.ts.map +1 -0
  65. package/dist/tools/edit.js +131 -0
  66. package/dist/tools/edit.js.map +1 -0
  67. package/dist/tools/index.d.ts +7 -0
  68. package/dist/tools/index.d.ts.map +1 -0
  69. package/dist/tools/index.js +19 -0
  70. package/dist/tools/index.js.map +1 -0
  71. package/dist/tools/read.d.ts +11 -0
  72. package/dist/tools/read.d.ts.map +1 -0
  73. package/dist/tools/read.js +134 -0
  74. package/dist/tools/read.js.map +1 -0
  75. package/dist/tools/truncate.d.ts +57 -0
  76. package/dist/tools/truncate.d.ts.map +1 -0
  77. package/dist/tools/truncate.js +184 -0
  78. package/dist/tools/truncate.js.map +1 -0
  79. package/dist/tools/write.d.ts +10 -0
  80. package/dist/tools/write.d.ts.map +1 -0
  81. package/dist/tools/write.js +33 -0
  82. package/dist/tools/write.js.map +1 -0
  83. package/package.json +57 -0
@@ -0,0 +1,516 @@
1
+ import { SocketModeClient } from "@slack/socket-mode";
2
+ import { WebClient } from "@slack/web-api";
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
4
+ import { readFile } from "fs/promises";
5
+ import { basename, join } from "path";
6
+ import * as log from "../../log.js";
7
+ // ============================================================================
8
+ // Exponential backoff utility for Slack API calls
9
+ // ============================================================================
10
+ /**
11
+ * Retry a function with exponential backoff on rate limit errors.
12
+ */
13
+ async function withRetry(fn, maxRetries = 3, baseDelayMs = 1000) {
14
+ let lastError;
15
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
16
+ try {
17
+ return await fn();
18
+ }
19
+ catch (err) {
20
+ lastError = err instanceof Error ? err : new Error(String(err));
21
+ // Check for rate limit errors
22
+ let isRateLimited = false;
23
+ // Check for rate_limited error code (Slack SDK)
24
+ if ("code" in lastError && lastError.code === "rate_limited") {
25
+ isRateLimited = true;
26
+ }
27
+ // Check for rate_limited in error response
28
+ if ("data" in lastError) {
29
+ const data = lastError.data;
30
+ if (data?.error === "rate_limited" || data?.response?.status === 429) {
31
+ isRateLimited = true;
32
+ }
33
+ }
34
+ if (isRateLimited) {
35
+ const delay = baseDelayMs * Math.pow(2, attempt);
36
+ log.logWarning(`Rate limited, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
37
+ await new Promise((resolve) => setTimeout(resolve, delay));
38
+ continue;
39
+ }
40
+ // Non-retryable error
41
+ throw lastError;
42
+ }
43
+ }
44
+ throw lastError;
45
+ }
46
+ class ChannelQueue {
47
+ queue = [];
48
+ processing = false;
49
+ enqueue(work) {
50
+ this.queue.push(work);
51
+ this.processNext();
52
+ }
53
+ size() {
54
+ return this.queue.length;
55
+ }
56
+ async processNext() {
57
+ if (this.processing || this.queue.length === 0)
58
+ return;
59
+ this.processing = true;
60
+ const work = this.queue.shift();
61
+ try {
62
+ await work();
63
+ }
64
+ catch (err) {
65
+ log.logWarning("Queue error", err instanceof Error ? err.message : String(err));
66
+ }
67
+ this.processing = false;
68
+ this.processNext();
69
+ }
70
+ }
71
+ // ============================================================================
72
+ // SlackBot
73
+ // ============================================================================
74
+ export class SlackBot {
75
+ socketClient;
76
+ webClient;
77
+ handler;
78
+ workingDir;
79
+ store;
80
+ botUserId = null;
81
+ startupTs = null; // Messages older than this are just logged, not processed
82
+ users = new Map();
83
+ channels = new Map();
84
+ queues = new Map();
85
+ constructor(handler, config) {
86
+ this.handler = handler;
87
+ this.workingDir = config.workingDir;
88
+ this.store = config.store;
89
+ this.socketClient = new SocketModeClient({ appToken: config.appToken });
90
+ this.webClient = new WebClient(config.botToken);
91
+ }
92
+ // ==========================================================================
93
+ // Public API
94
+ // ==========================================================================
95
+ async start() {
96
+ const auth = await this.webClient.auth.test();
97
+ this.botUserId = auth.user_id;
98
+ await Promise.all([this.fetchUsers(), this.fetchChannels()]);
99
+ log.logInfo(`Loaded ${this.channels.size} channels, ${this.users.size} users`);
100
+ await this.backfillAllChannels();
101
+ this.setupEventHandlers();
102
+ await this.socketClient.start();
103
+ // Record startup time - messages older than this are just logged, not processed
104
+ this.startupTs = (Date.now() / 1000).toFixed(6);
105
+ log.logConnected();
106
+ }
107
+ getUser(userId) {
108
+ return this.users.get(userId);
109
+ }
110
+ getChannel(channelId) {
111
+ return this.channels.get(channelId);
112
+ }
113
+ getAllUsers() {
114
+ return Array.from(this.users.values());
115
+ }
116
+ getAllChannels() {
117
+ return Array.from(this.channels.values());
118
+ }
119
+ async postMessage(channel, text) {
120
+ return withRetry(async () => {
121
+ const result = await this.webClient.chat.postMessage({ channel, text });
122
+ return result.ts;
123
+ });
124
+ }
125
+ async updateMessage(channel, ts, text) {
126
+ return withRetry(async () => {
127
+ await this.webClient.chat.update({ channel, ts, text });
128
+ });
129
+ }
130
+ async deleteMessage(channel, ts) {
131
+ return withRetry(async () => {
132
+ await this.webClient.chat.delete({ channel, ts });
133
+ });
134
+ }
135
+ async postInThread(channel, threadTs, text) {
136
+ return withRetry(async () => {
137
+ const result = await this.webClient.chat.postMessage({ channel, thread_ts: threadTs, text });
138
+ return result.ts;
139
+ });
140
+ }
141
+ async uploadFile(channel, filePath, title) {
142
+ return withRetry(async () => {
143
+ const fileName = title || basename(filePath);
144
+ const fileContent = readFileSync(filePath);
145
+ await this.webClient.files.uploadV2({
146
+ channel_id: channel,
147
+ file: fileContent,
148
+ filename: fileName,
149
+ title: fileName,
150
+ });
151
+ });
152
+ }
153
+ /**
154
+ * Log a message to log.jsonl (SYNC)
155
+ * This is the ONLY place messages are written to log.jsonl
156
+ */
157
+ logToFile(channel, entry) {
158
+ const dir = join(this.workingDir, channel);
159
+ if (!existsSync(dir))
160
+ mkdirSync(dir, { recursive: true });
161
+ appendFileSync(join(dir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
162
+ }
163
+ /**
164
+ * Log a bot response to log.jsonl
165
+ */
166
+ logBotResponse(channel, text, ts) {
167
+ this.logToFile(channel, {
168
+ date: new Date().toISOString(),
169
+ ts,
170
+ user: "bot",
171
+ text,
172
+ attachments: [],
173
+ isBot: true,
174
+ });
175
+ }
176
+ // ==========================================================================
177
+ // Events Integration
178
+ // ==========================================================================
179
+ /**
180
+ * Enqueue an event for processing. Always queues (no "already working" rejection).
181
+ * Returns true if enqueued, false if queue is full (max 5).
182
+ */
183
+ enqueueEvent(event) {
184
+ const queue = this.getQueue(event.channel);
185
+ if (queue.size() >= 5) {
186
+ log.logWarning(`Event queue full for ${event.channel}, discarding: ${event.text.substring(0, 50)}`);
187
+ return false;
188
+ }
189
+ log.logInfo(`Enqueueing event for ${event.channel}: ${event.text.substring(0, 50)}`);
190
+ queue.enqueue(() => this.handler.handleEvent(event, this, true));
191
+ return true;
192
+ }
193
+ // ==========================================================================
194
+ // Private - Event Handlers
195
+ // ==========================================================================
196
+ getQueue(channelId) {
197
+ let queue = this.queues.get(channelId);
198
+ if (!queue) {
199
+ queue = new ChannelQueue();
200
+ this.queues.set(channelId, queue);
201
+ }
202
+ return queue;
203
+ }
204
+ setupEventHandlers() {
205
+ // Channel @mentions
206
+ this.socketClient.on("app_mention", ({ event, ack }) => {
207
+ const e = event;
208
+ // Skip DMs (handled by message event)
209
+ if (e.channel.startsWith("D")) {
210
+ ack();
211
+ return;
212
+ }
213
+ // Derive session key from thread context
214
+ const rootTs = e.thread_ts ?? e.ts;
215
+ const sessionKey = `${e.channel}:${rootTs}`;
216
+ const slackEvent = {
217
+ type: "mention",
218
+ channel: e.channel,
219
+ ts: e.ts,
220
+ thread_ts: e.thread_ts,
221
+ user: e.user,
222
+ text: e.text.replace(/<@[A-Z0-9]+>/gi, "").trim(),
223
+ files: e.files,
224
+ };
225
+ // SYNC: Log to log.jsonl (ALWAYS, even for old messages)
226
+ // Also downloads attachments in background and stores local paths
227
+ slackEvent.attachments = this.logUserMessage(slackEvent);
228
+ // Only trigger processing for messages AFTER startup (not replayed old messages)
229
+ if (this.startupTs && e.ts < this.startupTs) {
230
+ log.logInfo(`[${e.channel}] Logged old message (pre-startup), not triggering: ${slackEvent.text.substring(0, 30)}`);
231
+ ack();
232
+ return;
233
+ }
234
+ // Check for stop command - execute immediately, don't queue!
235
+ if (slackEvent.text.toLowerCase().trim() === "stop") {
236
+ if (this.handler.isRunning(sessionKey)) {
237
+ this.handler.handleStop(sessionKey, e.channel, this); // Don't await, don't queue
238
+ }
239
+ else {
240
+ this.postMessage(e.channel, "_Nothing running_");
241
+ }
242
+ ack();
243
+ return;
244
+ }
245
+ // SYNC: Check if busy (per-thread)
246
+ if (this.handler.isRunning(sessionKey)) {
247
+ this.postMessage(e.channel, "_Already working in this thread. Say `@mama stop` to cancel._");
248
+ }
249
+ else {
250
+ this.getQueue(sessionKey).enqueue(() => this.handler.handleEvent(slackEvent, this));
251
+ }
252
+ ack();
253
+ });
254
+ // All messages (for logging) + DMs (for triggering)
255
+ this.socketClient.on("message", ({ event, ack }) => {
256
+ const e = event;
257
+ // Skip bot messages, edits, etc.
258
+ if (e.bot_id || !e.user || e.user === this.botUserId) {
259
+ ack();
260
+ return;
261
+ }
262
+ if (e.subtype !== undefined && e.subtype !== "file_share") {
263
+ ack();
264
+ return;
265
+ }
266
+ if (!e.text && (!e.files || e.files.length === 0)) {
267
+ ack();
268
+ return;
269
+ }
270
+ const isDM = e.channel_type === "im";
271
+ const isBotMention = e.text?.includes(`<@${this.botUserId}>`);
272
+ // Skip channel @mentions - already handled by app_mention event
273
+ if (!isDM && isBotMention) {
274
+ ack();
275
+ return;
276
+ }
277
+ const slackEvent = {
278
+ type: isDM ? "dm" : "mention",
279
+ channel: e.channel,
280
+ ts: e.ts,
281
+ thread_ts: e.thread_ts,
282
+ user: e.user,
283
+ text: (e.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim(),
284
+ files: e.files,
285
+ };
286
+ // SYNC: Log to log.jsonl (ALL messages - channel chatter and DMs)
287
+ // Also downloads attachments in background and stores local paths
288
+ slackEvent.attachments = this.logUserMessage(slackEvent);
289
+ // Only trigger processing for messages AFTER startup (not replayed old messages)
290
+ if (this.startupTs && e.ts < this.startupTs) {
291
+ log.logInfo(`[${e.channel}] Skipping old message (pre-startup): ${slackEvent.text.substring(0, 30)}`);
292
+ ack();
293
+ return;
294
+ }
295
+ // Only trigger handler for DMs
296
+ if (isDM) {
297
+ const dmRootTs = e.thread_ts ?? e.ts;
298
+ const dmSessionKey = `${e.channel}:${dmRootTs}`;
299
+ // Check for stop command - execute immediately, don't queue!
300
+ if (slackEvent.text.toLowerCase().trim() === "stop") {
301
+ if (this.handler.isRunning(dmSessionKey)) {
302
+ this.handler.handleStop(dmSessionKey, e.channel, this); // Don't await, don't queue
303
+ }
304
+ else {
305
+ this.postMessage(e.channel, "_Nothing running_");
306
+ }
307
+ ack();
308
+ return;
309
+ }
310
+ if (this.handler.isRunning(dmSessionKey)) {
311
+ this.postMessage(e.channel, "_Already working. Say `stop` to cancel._");
312
+ }
313
+ else {
314
+ this.getQueue(dmSessionKey).enqueue(() => this.handler.handleEvent(slackEvent, this));
315
+ }
316
+ }
317
+ ack();
318
+ });
319
+ }
320
+ /**
321
+ * Log a user message to log.jsonl (SYNC)
322
+ * Downloads attachments in background via store
323
+ */
324
+ logUserMessage(event) {
325
+ const user = this.users.get(event.user);
326
+ // Process attachments - queues downloads in background
327
+ const attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];
328
+ this.logToFile(event.channel, {
329
+ date: new Date(parseFloat(event.ts) * 1000).toISOString(),
330
+ ts: event.ts,
331
+ user: event.user,
332
+ userName: user?.userName,
333
+ displayName: user?.displayName,
334
+ text: event.text,
335
+ attachments,
336
+ isBot: false,
337
+ });
338
+ return attachments;
339
+ }
340
+ // ==========================================================================
341
+ // Private - Backfill
342
+ // ==========================================================================
343
+ async getExistingTimestamps(channelId) {
344
+ const logPath = join(this.workingDir, channelId, "log.jsonl");
345
+ const timestamps = new Set();
346
+ if (!existsSync(logPath))
347
+ return timestamps;
348
+ const content = await readFile(logPath, "utf-8");
349
+ const lines = content.trim().split("\n").filter(Boolean);
350
+ for (const line of lines) {
351
+ try {
352
+ const entry = JSON.parse(line);
353
+ if (entry.ts)
354
+ timestamps.add(entry.ts);
355
+ }
356
+ catch { }
357
+ }
358
+ return timestamps;
359
+ }
360
+ async backfillChannel(channelId) {
361
+ const existingTs = await this.getExistingTimestamps(channelId);
362
+ // Find the biggest ts in log.jsonl
363
+ let latestTs;
364
+ for (const ts of existingTs) {
365
+ if (!latestTs || parseFloat(ts) > parseFloat(latestTs))
366
+ latestTs = ts;
367
+ }
368
+ const allMessages = [];
369
+ let cursor;
370
+ let pageCount = 0;
371
+ const maxPages = 3;
372
+ do {
373
+ const result = await this.webClient.conversations.history({
374
+ channel: channelId,
375
+ oldest: latestTs, // Only fetch messages newer than what we have
376
+ inclusive: false,
377
+ limit: 1000,
378
+ cursor,
379
+ });
380
+ if (result.messages) {
381
+ allMessages.push(...result.messages);
382
+ }
383
+ cursor = result.response_metadata?.next_cursor;
384
+ pageCount++;
385
+ } while (cursor && pageCount < maxPages);
386
+ // Filter: include mama's messages, exclude other bots, skip already logged
387
+ const relevantMessages = allMessages.filter((msg) => {
388
+ if (!msg.ts || existingTs.has(msg.ts))
389
+ return false; // Skip duplicates
390
+ if (msg.user === this.botUserId)
391
+ return true;
392
+ if (msg.bot_id)
393
+ return false;
394
+ if (msg.subtype !== undefined && msg.subtype !== "file_share")
395
+ return false;
396
+ if (!msg.user)
397
+ return false;
398
+ if (!msg.text && (!msg.files || msg.files.length === 0))
399
+ return false;
400
+ return true;
401
+ });
402
+ // Reverse to chronological order
403
+ relevantMessages.reverse();
404
+ // Log each message to log.jsonl
405
+ for (const msg of relevantMessages) {
406
+ const isMamaMessage = msg.user === this.botUserId;
407
+ const user = this.users.get(msg.user);
408
+ // Strip @mentions from text (same as live messages)
409
+ const text = (msg.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim();
410
+ // Process attachments - queues downloads in background
411
+ const attachments = msg.files ? this.store.processAttachments(channelId, msg.files, msg.ts) : [];
412
+ this.logToFile(channelId, {
413
+ date: new Date(parseFloat(msg.ts) * 1000).toISOString(),
414
+ ts: msg.ts,
415
+ user: isMamaMessage ? "bot" : msg.user,
416
+ userName: isMamaMessage ? undefined : user?.userName,
417
+ displayName: isMamaMessage ? undefined : user?.displayName,
418
+ text,
419
+ attachments,
420
+ isBot: isMamaMessage,
421
+ });
422
+ }
423
+ return relevantMessages.length;
424
+ }
425
+ async backfillAllChannels() {
426
+ const startTime = Date.now();
427
+ // Only backfill channels that already have a log.jsonl (mama has interacted with them before)
428
+ const channelsToBackfill = [];
429
+ for (const [channelId, channel] of this.channels) {
430
+ const logPath = join(this.workingDir, channelId, "log.jsonl");
431
+ if (existsSync(logPath)) {
432
+ channelsToBackfill.push([channelId, channel]);
433
+ }
434
+ }
435
+ log.logBackfillStart(channelsToBackfill.length);
436
+ let totalMessages = 0;
437
+ for (const [channelId, channel] of channelsToBackfill) {
438
+ try {
439
+ const count = await this.backfillChannel(channelId);
440
+ if (count > 0)
441
+ log.logBackfillChannel(channel.name, count);
442
+ totalMessages += count;
443
+ }
444
+ catch (error) {
445
+ log.logWarning(`Failed to backfill #${channel.name}`, String(error));
446
+ }
447
+ // Add delay between channels to avoid hitting Slack rate limits
448
+ if (channelId !== channelsToBackfill[channelsToBackfill.length - 1][0]) {
449
+ await new Promise((resolve) => setTimeout(resolve, 500));
450
+ }
451
+ }
452
+ const durationMs = Date.now() - startTime;
453
+ log.logBackfillComplete(totalMessages, durationMs);
454
+ }
455
+ // ==========================================================================
456
+ // Private - Fetch Users/Channels
457
+ // ==========================================================================
458
+ async fetchUsers() {
459
+ let cursor;
460
+ do {
461
+ const result = await this.webClient.users.list({ limit: 200, cursor });
462
+ const members = result.members;
463
+ if (members) {
464
+ for (const u of members) {
465
+ if (u.id && u.name && !u.deleted) {
466
+ this.users.set(u.id, { id: u.id, userName: u.name, displayName: u.real_name || u.name });
467
+ }
468
+ }
469
+ }
470
+ cursor = result.response_metadata?.next_cursor;
471
+ } while (cursor);
472
+ }
473
+ async fetchChannels() {
474
+ // Fetch public/private channels
475
+ let cursor;
476
+ do {
477
+ const result = await this.webClient.conversations.list({
478
+ types: "public_channel,private_channel",
479
+ exclude_archived: true,
480
+ limit: 200,
481
+ cursor,
482
+ });
483
+ const channels = result.channels;
484
+ if (channels) {
485
+ for (const c of channels) {
486
+ if (c.id && c.name && c.is_member) {
487
+ this.channels.set(c.id, { id: c.id, name: c.name });
488
+ }
489
+ }
490
+ }
491
+ cursor = result.response_metadata?.next_cursor;
492
+ } while (cursor);
493
+ // Also fetch DM channels (IMs)
494
+ cursor = undefined;
495
+ do {
496
+ const result = await this.webClient.conversations.list({
497
+ types: "im",
498
+ limit: 200,
499
+ cursor,
500
+ });
501
+ const ims = result.channels;
502
+ if (ims) {
503
+ for (const im of ims) {
504
+ if (im.id) {
505
+ // Use user's name as channel name for DMs
506
+ const user = im.user ? this.users.get(im.user) : undefined;
507
+ const name = user ? `DM:${user.userName}` : `DM:${im.id}`;
508
+ this.channels.set(im.id, { id: im.id, name });
509
+ }
510
+ }
511
+ }
512
+ cursor = result.response_metadata?.next_cursor;
513
+ } while (cursor);
514
+ }
515
+ }
516
+ //# sourceMappingURL=bot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bot.js","sourceRoot":"","sources":["../../../src/adapters/slack/bot.ts"],"names":[],"mappings":"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","sourcesContent":["import { SocketModeClient } from \"@slack/socket-mode\";\nimport { WebClient } from \"@slack/web-api\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync } from \"fs\";\nimport { readFile } from \"fs/promises\";\nimport { basename, join } from \"path\";\nimport * as log from \"../../log.js\";\nimport type { Attachment, ChannelStore } from \"../../store.js\";\n\n// ============================================================================\n// Exponential backoff utility for Slack API calls\n// ============================================================================\n\n/**\n * Retry a function with exponential backoff on rate limit errors.\n */\nasync function withRetry<T>(\n\tfn: () => Promise<T>,\n\tmaxRetries: number = 3,\n\tbaseDelayMs: number = 1000,\n): Promise<T> {\n\tlet lastError: Error | undefined;\n\tfor (let attempt = 0; attempt < maxRetries; attempt++) {\n\t\ttry {\n\t\t\treturn await fn();\n\t\t} catch (err) {\n\t\t\tlastError = err instanceof Error ? err : new Error(String(err));\n\n\t\t\t// Check for rate limit errors\n\t\t\tlet isRateLimited = false;\n\n\t\t\t// Check for rate_limited error code (Slack SDK)\n\t\t\tif (\"code\" in lastError && lastError.code === \"rate_limited\") {\n\t\t\t\tisRateLimited = true;\n\t\t\t}\n\n\t\t\t// Check for rate_limited in error response\n\t\t\tif (\"data\" in lastError) {\n\t\t\t\tconst data = (lastError as { data?: { error?: string; response?: { status?: number } } }).data;\n\t\t\t\tif (data?.error === \"rate_limited\" || data?.response?.status === 429) {\n\t\t\t\t\tisRateLimited = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isRateLimited) {\n\t\t\t\tconst delay = baseDelayMs * Math.pow(2, attempt);\n\t\t\t\tlog.logWarning(`Rate limited, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, delay));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Non-retryable error\n\t\t\tthrow lastError;\n\t\t}\n\t}\n\tthrow lastError;\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface SlackEvent {\n\ttype: \"mention\" | \"dm\";\n\tchannel: string;\n\tts: string;\n\tthread_ts?: string;\n\tuser: string;\n\ttext: string;\n\tfiles?: Array<{ name?: string; url_private_download?: string; url_private?: string }>;\n\t/** Processed attachments with local paths (populated after logUserMessage) */\n\tattachments?: Attachment[];\n}\n\nexport interface SlackUser {\n\tid: string;\n\tuserName: string;\n\tdisplayName: string;\n}\n\nexport interface SlackChannel {\n\tid: string;\n\tname: string;\n}\n\n// Types used by agent.ts\nexport interface ChannelInfo {\n\tid: string;\n\tname: string;\n}\n\nexport interface UserInfo {\n\tid: string;\n\tuserName: string;\n\tdisplayName: string;\n}\n\nexport interface SlackContext {\n\tmessage: {\n\t\ttext: string;\n\t\trawText: string;\n\t\tuser: string;\n\t\tuserName?: string;\n\t\tchannel: string;\n\t\tts: string;\n\t\tattachments: Array<{ local: string }>;\n\t};\n\tchannelName?: string;\n\tchannels: ChannelInfo[];\n\tusers: UserInfo[];\n\trespond: (text: string, shouldLog?: boolean) => Promise<void>;\n\treplaceMessage: (text: string) => Promise<void>;\n\trespondInThread: (text: string) => Promise<void>;\n\tsetTyping: (isTyping: boolean) => Promise<void>;\n\tuploadFile: (filePath: string, title?: string) => Promise<void>;\n\tsetWorking: (working: boolean) => Promise<void>;\n\tdeleteMessage: () => Promise<void>;\n}\n\nexport interface MomHandler {\n\t/**\n\t * Check if session is currently running (SYNC).\n\t * sessionKey format: \"channelId:rootTs\"\n\t */\n\tisRunning(sessionKey: string): boolean;\n\n\t/**\n\t * Handle an event that triggers mama (ASYNC)\n\t * Called only when isRunning() returned false for user messages.\n\t * Events always queue and pass isEvent=true.\n\t */\n\thandleEvent(event: SlackEvent, slack: SlackBot, isEvent?: boolean): Promise<void>;\n\n\t/**\n\t * Handle stop command (ASYNC)\n\t * Called when user says \"stop\" while mama is running\n\t */\n\thandleStop(sessionKey: string, channelId: string, slack: SlackBot): Promise<void>;\n}\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n\tprivate queue: QueuedWork[] = [];\n\tprivate processing = false;\n\n\tenqueue(work: QueuedWork): void {\n\t\tthis.queue.push(work);\n\t\tthis.processNext();\n\t}\n\n\tsize(): number {\n\t\treturn this.queue.length;\n\t}\n\n\tprivate async processNext(): Promise<void> {\n\t\tif (this.processing || this.queue.length === 0) return;\n\t\tthis.processing = true;\n\t\tconst work = this.queue.shift()!;\n\t\ttry {\n\t\t\tawait work();\n\t\t} catch (err) {\n\t\t\tlog.logWarning(\"Queue error\", err instanceof Error ? err.message : String(err));\n\t\t}\n\t\tthis.processing = false;\n\t\tthis.processNext();\n\t}\n}\n\n// ============================================================================\n// SlackBot\n// ============================================================================\n\nexport class SlackBot {\n\tprivate socketClient: SocketModeClient;\n\tprivate webClient: WebClient;\n\tprivate handler: MomHandler;\n\tprivate workingDir: string;\n\tprivate store: ChannelStore;\n\tprivate botUserId: string | null = null;\n\tprivate startupTs: string | null = null; // Messages older than this are just logged, not processed\n\n\tprivate users = new Map<string, SlackUser>();\n\tprivate channels = new Map<string, SlackChannel>();\n\tprivate queues = new Map<string, ChannelQueue>();\n\n\tconstructor(\n\t\thandler: MomHandler,\n\t\tconfig: { appToken: string; botToken: string; workingDir: string; store: ChannelStore },\n\t) {\n\t\tthis.handler = handler;\n\t\tthis.workingDir = config.workingDir;\n\t\tthis.store = config.store;\n\t\tthis.socketClient = new SocketModeClient({ appToken: config.appToken });\n\t\tthis.webClient = new WebClient(config.botToken);\n\t}\n\n\t// ==========================================================================\n\t// Public API\n\t// ==========================================================================\n\n\tasync start(): Promise<void> {\n\t\tconst auth = await this.webClient.auth.test();\n\t\tthis.botUserId = auth.user_id as string;\n\n\t\tawait Promise.all([this.fetchUsers(), this.fetchChannels()]);\n\t\tlog.logInfo(`Loaded ${this.channels.size} channels, ${this.users.size} users`);\n\n\t\tawait this.backfillAllChannels();\n\n\t\tthis.setupEventHandlers();\n\t\tawait this.socketClient.start();\n\n\t\t// Record startup time - messages older than this are just logged, not processed\n\t\tthis.startupTs = (Date.now() / 1000).toFixed(6);\n\n\t\tlog.logConnected();\n\t}\n\n\tgetUser(userId: string): SlackUser | undefined {\n\t\treturn this.users.get(userId);\n\t}\n\n\tgetChannel(channelId: string): SlackChannel | undefined {\n\t\treturn this.channels.get(channelId);\n\t}\n\n\tgetAllUsers(): SlackUser[] {\n\t\treturn Array.from(this.users.values());\n\t}\n\n\tgetAllChannels(): SlackChannel[] {\n\t\treturn Array.from(this.channels.values());\n\t}\n\n\tasync postMessage(channel: string, text: string): Promise<string> {\n\t\treturn withRetry(async () => {\n\t\t\tconst result = await this.webClient.chat.postMessage({ channel, text });\n\t\t\treturn result.ts as string;\n\t\t});\n\t}\n\n\tasync updateMessage(channel: string, ts: string, text: string): Promise<void> {\n\t\treturn withRetry(async () => {\n\t\t\tawait this.webClient.chat.update({ channel, ts, text });\n\t\t});\n\t}\n\n\tasync deleteMessage(channel: string, ts: string): Promise<void> {\n\t\treturn withRetry(async () => {\n\t\t\tawait this.webClient.chat.delete({ channel, ts });\n\t\t});\n\t}\n\n\tasync postInThread(channel: string, threadTs: string, text: string): Promise<string> {\n\t\treturn withRetry(async () => {\n\t\t\tconst result = await this.webClient.chat.postMessage({ channel, thread_ts: threadTs, text });\n\t\t\treturn result.ts as string;\n\t\t});\n\t}\n\n\tasync uploadFile(channel: string, filePath: string, title?: string): Promise<void> {\n\t\treturn withRetry(async () => {\n\t\t\tconst fileName = title || basename(filePath);\n\t\t\tconst fileContent = readFileSync(filePath);\n\t\t\tawait this.webClient.files.uploadV2({\n\t\t\t\tchannel_id: channel,\n\t\t\t\tfile: fileContent,\n\t\t\t\tfilename: fileName,\n\t\t\t\ttitle: fileName,\n\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Log a message to log.jsonl (SYNC)\n\t * This is the ONLY place messages are written to log.jsonl\n\t */\n\tlogToFile(channel: string, entry: object): void {\n\t\tconst dir = join(this.workingDir, channel);\n\t\tif (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\t\tappendFileSync(join(dir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n\t}\n\n\t/**\n\t * Log a bot response to log.jsonl\n\t */\n\tlogBotResponse(channel: string, text: string, ts: string): void {\n\t\tthis.logToFile(channel, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tattachments: [],\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t// ==========================================================================\n\t// Events Integration\n\t// ==========================================================================\n\n\t/**\n\t * Enqueue an event for processing. Always queues (no \"already working\" rejection).\n\t * Returns true if enqueued, false if queue is full (max 5).\n\t */\n\tenqueueEvent(event: SlackEvent): boolean {\n\t\tconst queue = this.getQueue(event.channel);\n\t\tif (queue.size() >= 5) {\n\t\t\tlog.logWarning(`Event queue full for ${event.channel}, discarding: ${event.text.substring(0, 50)}`);\n\t\t\treturn false;\n\t\t}\n\t\tlog.logInfo(`Enqueueing event for ${event.channel}: ${event.text.substring(0, 50)}`);\n\t\tqueue.enqueue(() => this.handler.handleEvent(event, this, true));\n\t\treturn true;\n\t}\n\n\t// ==========================================================================\n\t// Private - Event Handlers\n\t// ==========================================================================\n\n\tprivate getQueue(channelId: string): ChannelQueue {\n\t\tlet queue = this.queues.get(channelId);\n\t\tif (!queue) {\n\t\t\tqueue = new ChannelQueue();\n\t\t\tthis.queues.set(channelId, queue);\n\t\t}\n\t\treturn queue;\n\t}\n\n\tprivate setupEventHandlers(): void {\n\t\t// Channel @mentions\n\t\tthis.socketClient.on(\"app_mention\", ({ event, ack }) => {\n\t\t\tconst e = event as {\n\t\t\t\ttext: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t\t\t};\n\n\t\t\t// Skip DMs (handled by message event)\n\t\t\tif (e.channel.startsWith(\"D\")) {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Derive session key from thread context\n\t\t\tconst rootTs = e.thread_ts ?? e.ts;\n\t\t\tconst sessionKey = `${e.channel}:${rootTs}`;\n\n\t\t\tconst slackEvent: SlackEvent = {\n\t\t\t\ttype: \"mention\",\n\t\t\t\tchannel: e.channel,\n\t\t\t\tts: e.ts,\n\t\t\t\tthread_ts: e.thread_ts,\n\t\t\t\tuser: e.user,\n\t\t\t\ttext: e.text.replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t};\n\n\t\t\t// SYNC: Log to log.jsonl (ALWAYS, even for old messages)\n\t\t\t// Also downloads attachments in background and stores local paths\n\t\t\tslackEvent.attachments = this.logUserMessage(slackEvent);\n\n\t\t\t// Only trigger processing for messages AFTER startup (not replayed old messages)\n\t\t\tif (this.startupTs && e.ts < this.startupTs) {\n\t\t\t\tlog.logInfo(\n\t\t\t\t\t`[${e.channel}] Logged old message (pre-startup), not triggering: ${slackEvent.text.substring(0, 30)}`,\n\t\t\t\t);\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for stop command - execute immediately, don't queue!\n\t\t\tif (slackEvent.text.toLowerCase().trim() === \"stop\") {\n\t\t\t\tif (this.handler.isRunning(sessionKey)) {\n\t\t\t\t\tthis.handler.handleStop(sessionKey, e.channel, this); // Don't await, don't queue\n\t\t\t\t} else {\n\t\t\t\t\tthis.postMessage(e.channel, \"_Nothing running_\");\n\t\t\t\t}\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// SYNC: Check if busy (per-thread)\n\t\t\tif (this.handler.isRunning(sessionKey)) {\n\t\t\t\tthis.postMessage(e.channel, \"_Already working in this thread. Say `@mama stop` to cancel._\");\n\t\t\t} else {\n\t\t\t\tthis.getQueue(sessionKey).enqueue(() => this.handler.handleEvent(slackEvent, this));\n\t\t\t}\n\n\t\t\tack();\n\t\t});\n\n\t\t// All messages (for logging) + DMs (for triggering)\n\t\tthis.socketClient.on(\"message\", ({ event, ack }) => {\n\t\t\tconst e = event as {\n\t\t\t\ttext?: string;\n\t\t\t\tchannel: string;\n\t\t\t\tuser?: string;\n\t\t\t\tts: string;\n\t\t\t\tthread_ts?: string;\n\t\t\t\tchannel_type?: string;\n\t\t\t\tsubtype?: string;\n\t\t\t\tbot_id?: string;\n\t\t\t\tfiles?: Array<{ name: string; url_private_download?: string; url_private?: string }>;\n\t\t\t};\n\n\t\t\t// Skip bot messages, edits, etc.\n\t\t\tif (e.bot_id || !e.user || e.user === this.botUserId) {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (e.subtype !== undefined && e.subtype !== \"file_share\") {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!e.text && (!e.files || e.files.length === 0)) {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst isDM = e.channel_type === \"im\";\n\t\t\tconst isBotMention = e.text?.includes(`<@${this.botUserId}>`);\n\n\t\t\t// Skip channel @mentions - already handled by app_mention event\n\t\t\tif (!isDM && isBotMention) {\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst slackEvent: SlackEvent = {\n\t\t\t\ttype: isDM ? \"dm\" : \"mention\",\n\t\t\t\tchannel: e.channel,\n\t\t\t\tts: e.ts,\n\t\t\t\tthread_ts: e.thread_ts,\n\t\t\t\tuser: e.user,\n\t\t\t\ttext: (e.text || \"\").replace(/<@[A-Z0-9]+>/gi, \"\").trim(),\n\t\t\t\tfiles: e.files,\n\t\t\t};\n\n\t\t\t// SYNC: Log to log.jsonl (ALL messages - channel chatter and DMs)\n\t\t\t// Also downloads attachments in background and stores local paths\n\t\t\tslackEvent.attachments = this.logUserMessage(slackEvent);\n\n\t\t\t// Only trigger processing for messages AFTER startup (not replayed old messages)\n\t\t\tif (this.startupTs && e.ts < this.startupTs) {\n\t\t\t\tlog.logInfo(`[${e.channel}] Skipping old message (pre-startup): ${slackEvent.text.substring(0, 30)}`);\n\t\t\t\tack();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Only trigger handler for DMs\n\t\t\tif (isDM) {\n\t\t\t\tconst dmRootTs = e.thread_ts ?? e.ts;\n\t\t\t\tconst dmSessionKey = `${e.channel}:${dmRootTs}`;\n\n\t\t\t\t// Check for stop command - execute immediately, don't queue!\n\t\t\t\tif (slackEvent.text.toLowerCase().trim() === \"stop\") {\n\t\t\t\t\tif (this.handler.isRunning(dmSessionKey)) {\n\t\t\t\t\t\tthis.handler.handleStop(dmSessionKey, e.channel, this); // Don't await, don't queue\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.postMessage(e.channel, \"_Nothing running_\");\n\t\t\t\t\t}\n\t\t\t\t\tack();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (this.handler.isRunning(dmSessionKey)) {\n\t\t\t\t\tthis.postMessage(e.channel, \"_Already working. Say `stop` to cancel._\");\n\t\t\t\t} else {\n\t\t\t\t\tthis.getQueue(dmSessionKey).enqueue(() => this.handler.handleEvent(slackEvent, this));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tack();\n\t\t});\n\t}\n\n\t/**\n\t * Log a user message to log.jsonl (SYNC)\n\t * Downloads attachments in background via store\n\t */\n\tprivate logUserMessage(event: SlackEvent): Attachment[] {\n\t\tconst user = this.users.get(event.user);\n\t\t// Process attachments - queues downloads in background\n\t\tconst attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];\n\t\tthis.logToFile(event.channel, {\n\t\t\tdate: new Date(parseFloat(event.ts) * 1000).toISOString(),\n\t\t\tts: event.ts,\n\t\t\tuser: event.user,\n\t\t\tuserName: user?.userName,\n\t\t\tdisplayName: user?.displayName,\n\t\t\ttext: event.text,\n\t\t\tattachments,\n\t\t\tisBot: false,\n\t\t});\n\t\treturn attachments;\n\t}\n\n\t// ==========================================================================\n\t// Private - Backfill\n\t// ==========================================================================\n\n\tprivate async getExistingTimestamps(channelId: string): Promise<Set<string>> {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tconst timestamps = new Set<string>();\n\t\tif (!existsSync(logPath)) return timestamps;\n\n\t\tconst content = await readFile(logPath, \"utf-8\");\n\t\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\t\tfor (const line of lines) {\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.ts) timestamps.add(entry.ts);\n\t\t\t} catch {}\n\t\t}\n\t\treturn timestamps;\n\t}\n\n\tprivate async backfillChannel(channelId: string): Promise<number> {\n\t\tconst existingTs = await this.getExistingTimestamps(channelId);\n\n\t\t// Find the biggest ts in log.jsonl\n\t\tlet latestTs: string | undefined;\n\t\tfor (const ts of existingTs) {\n\t\t\tif (!latestTs || parseFloat(ts) > parseFloat(latestTs)) latestTs = ts;\n\t\t}\n\n\t\ttype Message = {\n\t\t\tuser?: string;\n\t\t\tbot_id?: string;\n\t\t\ttext?: string;\n\t\t\tts?: string;\n\t\t\tsubtype?: string;\n\t\t\tfiles?: Array<{ name: string }>;\n\t\t};\n\t\tconst allMessages: Message[] = [];\n\n\t\tlet cursor: string | undefined;\n\t\tlet pageCount = 0;\n\t\tconst maxPages = 3;\n\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.history({\n\t\t\t\tchannel: channelId,\n\t\t\t\toldest: latestTs, // Only fetch messages newer than what we have\n\t\t\t\tinclusive: false,\n\t\t\t\tlimit: 1000,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tif (result.messages) {\n\t\t\t\tallMessages.push(...(result.messages as Message[]));\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t\tpageCount++;\n\t\t} while (cursor && pageCount < maxPages);\n\n\t\t// Filter: include mama's messages, exclude other bots, skip already logged\n\t\tconst relevantMessages = allMessages.filter((msg) => {\n\t\t\tif (!msg.ts || existingTs.has(msg.ts)) return false; // Skip duplicates\n\t\t\tif (msg.user === this.botUserId) return true;\n\t\t\tif (msg.bot_id) return false;\n\t\t\tif (msg.subtype !== undefined && msg.subtype !== \"file_share\") return false;\n\t\t\tif (!msg.user) return false;\n\t\t\tif (!msg.text && (!msg.files || msg.files.length === 0)) return false;\n\t\t\treturn true;\n\t\t});\n\n\t\t// Reverse to chronological order\n\t\trelevantMessages.reverse();\n\n\t\t// Log each message to log.jsonl\n\t\tfor (const msg of relevantMessages) {\n\t\t\tconst isMamaMessage = msg.user === this.botUserId;\n\t\t\tconst user = this.users.get(msg.user!);\n\t\t\t// Strip @mentions from text (same as live messages)\n\t\t\tconst text = (msg.text || \"\").replace(/<@[A-Z0-9]+>/gi, \"\").trim();\n\t\t\t// Process attachments - queues downloads in background\n\t\t\tconst attachments = msg.files ? this.store.processAttachments(channelId, msg.files, msg.ts!) : [];\n\n\t\t\tthis.logToFile(channelId, {\n\t\t\t\tdate: new Date(parseFloat(msg.ts!) * 1000).toISOString(),\n\t\t\t\tts: msg.ts!,\n\t\t\t\tuser: isMamaMessage ? \"bot\" : msg.user!,\n\t\t\t\tuserName: isMamaMessage ? undefined : user?.userName,\n\t\t\t\tdisplayName: isMamaMessage ? undefined : user?.displayName,\n\t\t\t\ttext,\n\t\t\t\tattachments,\n\t\t\t\tisBot: isMamaMessage,\n\t\t\t});\n\t\t}\n\n\t\treturn relevantMessages.length;\n\t}\n\n\tprivate async backfillAllChannels(): Promise<void> {\n\t\tconst startTime = Date.now();\n\n\t\t// Only backfill channels that already have a log.jsonl (mama has interacted with them before)\n\t\tconst channelsToBackfill: Array<[string, SlackChannel]> = [];\n\t\tfor (const [channelId, channel] of this.channels) {\n\t\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\t\tif (existsSync(logPath)) {\n\t\t\t\tchannelsToBackfill.push([channelId, channel]);\n\t\t\t}\n\t\t}\n\n\t\tlog.logBackfillStart(channelsToBackfill.length);\n\n\t\tlet totalMessages = 0;\n\t\tfor (const [channelId, channel] of channelsToBackfill) {\n\t\t\ttry {\n\t\t\t\tconst count = await this.backfillChannel(channelId);\n\t\t\t\tif (count > 0) log.logBackfillChannel(channel.name, count);\n\t\t\t\ttotalMessages += count;\n\t\t\t} catch (error) {\n\t\t\t\tlog.logWarning(`Failed to backfill #${channel.name}`, String(error));\n\t\t\t}\n\n\t\t\t// Add delay between channels to avoid hitting Slack rate limits\n\t\t\tif (channelId !== channelsToBackfill[channelsToBackfill.length - 1][0]) {\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, 500));\n\t\t\t}\n\t\t}\n\n\t\tconst durationMs = Date.now() - startTime;\n\t\tlog.logBackfillComplete(totalMessages, durationMs);\n\t}\n\n\t// ==========================================================================\n\t// Private - Fetch Users/Channels\n\t// ==========================================================================\n\n\tprivate async fetchUsers(): Promise<void> {\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.users.list({ limit: 200, cursor });\n\t\t\tconst members = result.members as\n\t\t\t\t| Array<{ id?: string; name?: string; real_name?: string; deleted?: boolean }>\n\t\t\t\t| undefined;\n\t\t\tif (members) {\n\t\t\t\tfor (const u of members) {\n\t\t\t\t\tif (u.id && u.name && !u.deleted) {\n\t\t\t\t\t\tthis.users.set(u.id, { id: u.id, userName: u.name, displayName: u.real_name || u.name });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n\n\tprivate async fetchChannels(): Promise<void> {\n\t\t// Fetch public/private channels\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\ttypes: \"public_channel,private_channel\",\n\t\t\t\texclude_archived: true,\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tconst channels = result.channels as Array<{ id?: string; name?: string; is_member?: boolean }> | undefined;\n\t\t\tif (channels) {\n\t\t\t\tfor (const c of channels) {\n\t\t\t\t\tif (c.id && c.name && c.is_member) {\n\t\t\t\t\t\tthis.channels.set(c.id, { id: c.id, name: c.name });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\n\t\t// Also fetch DM channels (IMs)\n\t\tcursor = undefined;\n\t\tdo {\n\t\t\tconst result = await this.webClient.conversations.list({\n\t\t\t\ttypes: \"im\",\n\t\t\t\tlimit: 200,\n\t\t\t\tcursor,\n\t\t\t});\n\t\t\tconst ims = result.channels as Array<{ id?: string; user?: string }> | undefined;\n\t\t\tif (ims) {\n\t\t\t\tfor (const im of ims) {\n\t\t\t\t\tif (im.id) {\n\t\t\t\t\t\t// Use user's name as channel name for DMs\n\t\t\t\t\t\tconst user = im.user ? this.users.get(im.user) : undefined;\n\t\t\t\t\t\tconst name = user ? `DM:${user.userName}` : `DM:${im.id}`;\n\t\t\t\t\t\tthis.channels.set(im.id, { id: im.id, name });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = result.response_metadata?.next_cursor;\n\t\t} while (cursor);\n\t}\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import type { ChatMessage, ChatResponseContext, PlatformInfo } from "../../adapter.js";
2
+ import type { SlackBot, SlackEvent } from "./bot.js";
3
+ export declare const SLACK_FORMATTING_GUIDE = "## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: `code`, Block: ```code```, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).";
4
+ export declare function createSlackAdapters(event: SlackEvent, slack: SlackBot, isEvent?: boolean): {
5
+ message: ChatMessage;
6
+ responseCtx: ChatResponseContext & {
7
+ setTyping(isTyping: boolean): Promise<void>;
8
+ };
9
+ platform: PlatformInfo;
10
+ };
11
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/adapters/slack/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEvF,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAErD,eAAO,MAAM,sBAAsB,wLAEmB,CAAC;AAEvD,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,QAAQ,EACf,OAAO,CAAC,EAAE,OAAO,GACf;IACF,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,EAAE,mBAAmB,GAAG;QAAE,SAAS,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IACnF,QAAQ,EAAE,YAAY,CAAC;CACvB,CAoLA","sourcesContent":["import type { ChatMessage, ChatResponseContext, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport type { SlackBot, SlackEvent } from \"./bot.js\";\n\nexport const SLACK_FORMATTING_GUIDE = `## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).`;\n\nexport function createSlackAdapters(\n\tevent: SlackEvent,\n\tslack: SlackBot,\n\tisEvent?: boolean,\n): {\n\tmessage: ChatMessage;\n\tresponseCtx: ChatResponseContext & { setTyping(isTyping: boolean): Promise<void> };\n\tplatform: PlatformInfo;\n} {\n\tlet messageTs: string | null = null;\n\tconst threadMessageTs: string[] = [];\n\tlet accumulatedText = \"\";\n\tlet isWorking = true;\n\tconst workingIndicator = \" ...\";\n\tlet updatePromise = Promise.resolve();\n\n\tconst user = slack.getUser(event.user);\n\n\t// Extract event filename for status message\n\tconst eventFilename = isEvent ? event.text.match(/^\\[EVENT:([^:]+):/)?.[1] : undefined;\n\n\tconst rootTs = event.thread_ts ?? event.ts;\n\tconst isThreaded = !!event.thread_ts;\n\n\tconst message: ChatMessage = {\n\t\tid: event.ts,\n\t\tsessionKey: `${event.channel}:${rootTs}`,\n\t\tuserId: event.user,\n\t\tuserName: user?.userName,\n\t\ttext: event.text,\n\t\tattachments: (event.attachments || []).map((a) => ({ name: a.local, localPath: a.local })),\n\t};\n\n\tconst platform: PlatformInfo = {\n\t\tname: \"slack\",\n\t\tformattingGuide: SLACK_FORMATTING_GUIDE,\n\t\tchannels: slack.getAllChannels().map((c) => ({ id: c.id, name: c.name })),\n\t\tusers: slack.getAllUsers().map((u) => ({ id: u.id, userName: u.userName, displayName: u.displayName })),\n\t};\n\n\tconst responseCtx = {\n\t\trespond: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\ttry {\n\t\t\t\t\taccumulatedText = accumulatedText ? `${accumulatedText}\\n${text}` : text;\n\n\t\t\t\t\t// Truncate accumulated text if too long (Slack limit is 40K, we use 35K for safety)\n\t\t\t\t\tconst MAX_MAIN_LENGTH = 35000;\n\t\t\t\t\tconst truncationNote = \"\\n\\n_(message truncated, ask me to elaborate on specific parts)_\";\n\t\t\t\t\tif (accumulatedText.length > MAX_MAIN_LENGTH) {\n\t\t\t\t\t\taccumulatedText =\n\t\t\t\t\t\t\taccumulatedText.substring(0, MAX_MAIN_LENGTH - truncationNote.length) + truncationNote;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t\t} else if (isThreaded) {\n\t\t\t\t\t\t// Reply within the user's thread\n\t\t\t\t\t\tmessageTs = await slack.postInThread(event.channel, rootTs, displayText);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmessageTs = await slack.postMessage(event.channel, displayText);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\tslack.logBotResponse(event.channel, text, messageTs);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlog.logWarning(\"Slack respond error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\treplaceResponse: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// Replace the accumulated text entirely, with truncation\n\t\t\t\t\tconst MAX_MAIN_LENGTH = 35000;\n\t\t\t\t\tconst truncationNote = \"\\n\\n_(message truncated, ask me to elaborate on specific parts)_\";\n\t\t\t\t\tif (text.length > MAX_MAIN_LENGTH) {\n\t\t\t\t\t\taccumulatedText = text.substring(0, MAX_MAIN_LENGTH - truncationNote.length) + truncationNote;\n\t\t\t\t\t} else {\n\t\t\t\t\t\taccumulatedText = text;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t\t} else if (isThreaded) {\n\t\t\t\t\t\tmessageTs = await slack.postInThread(event.channel, rootTs, displayText);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tmessageTs = await slack.postMessage(event.channel, displayText);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlog.logWarning(\"Slack replaceResponse error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\trespondInThread: async (text: string) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// For threaded sessions, anchor to the user's root thread\n\t\t\t\t\t// For channel sessions, anchor to the main bot message\n\t\t\t\t\tconst threadAnchor = isThreaded ? rootTs : messageTs;\n\t\t\t\t\tif (threadAnchor) {\n\t\t\t\t\t\t// Truncate thread messages if too long (20K limit for safety)\n\t\t\t\t\t\tconst MAX_THREAD_LENGTH = 20000;\n\t\t\t\t\t\tlet threadText = text;\n\t\t\t\t\t\tif (threadText.length > MAX_THREAD_LENGTH) {\n\t\t\t\t\t\t\tthreadText = `${threadText.substring(0, MAX_THREAD_LENGTH - 50)}\\n\\n_(truncated)_`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst ts = await slack.postInThread(event.channel, threadAnchor, threadText);\n\t\t\t\t\t\tthreadMessageTs.push(ts);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlog.logWarning(\"Slack respondInThread error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\tsetTyping: async (isTyping: boolean) => {\n\t\t\tif (isTyping && !messageTs) {\n\t\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (!messageTs) {\n\t\t\t\t\t\t\taccumulatedText = eventFilename ? `_Starting event: ${eventFilename}_` : \"_Thinking_\";\n\t\t\t\t\t\t\tif (isThreaded) {\n\t\t\t\t\t\t\t\tmessageTs = await slack.postInThread(event.channel, rootTs, accumulatedText + workingIndicator);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tmessageTs = await slack.postMessage(event.channel, accumulatedText + workingIndicator);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tlog.logWarning(\"Slack setTyping error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tawait updatePromise;\n\t\t\t}\n\t\t},\n\n\t\tuploadFile: async (filePath: string, title?: string) => {\n\t\t\tawait slack.uploadFile(event.channel, filePath, title);\n\t\t},\n\n\t\tsetWorking: async (working: boolean) => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\ttry {\n\t\t\t\t\tisWorking = working;\n\t\t\t\t\tif (messageTs) {\n\t\t\t\t\t\tconst displayText = isWorking ? accumulatedText + workingIndicator : accumulatedText;\n\t\t\t\t\t\tawait slack.updateMessage(event.channel, messageTs, displayText);\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tlog.logWarning(\"Slack setWorking error\", err instanceof Error ? err.message : String(err));\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\n\t\tdeleteResponse: async () => {\n\t\t\tupdatePromise = updatePromise.then(async () => {\n\t\t\t\t// Delete thread messages first (in reverse order)\n\t\t\t\tfor (let i = threadMessageTs.length - 1; i >= 0; i--) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait slack.deleteMessage(event.channel, threadMessageTs[i]);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Ignore errors deleting thread messages\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthreadMessageTs.length = 0;\n\t\t\t\t// Then delete main message\n\t\t\t\tif (messageTs) {\n\t\t\t\t\tawait slack.deleteMessage(event.channel, messageTs);\n\t\t\t\t\tmessageTs = null;\n\t\t\t\t}\n\t\t\t});\n\t\t\tawait updatePromise;\n\t\t},\n\t};\n\n\treturn { message, responseCtx, platform };\n}\n"]}