@chat-adapter/slack 4.11.0 → 4.13.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.
package/dist/index.js CHANGED
@@ -854,6 +854,21 @@ var SlackAdapter = class _SlackAdapter {
854
854
  }
855
855
  const contentType = request.headers.get("content-type") || "";
856
856
  if (contentType.includes("application/x-www-form-urlencoded")) {
857
+ const params = new URLSearchParams(body);
858
+ if (params.has("command") && !params.has("payload")) {
859
+ const teamId = params.get("team_id");
860
+ if (!this.defaultBotToken && teamId) {
861
+ const ctx = await this.resolveTokenForTeam(teamId);
862
+ if (ctx) {
863
+ return this.requestContext.run(
864
+ ctx,
865
+ () => this.handleSlashCommand(params, options)
866
+ );
867
+ }
868
+ this.logger.warn("Could not resolve token for slash command");
869
+ }
870
+ return this.handleSlashCommand(params, options);
871
+ }
857
872
  if (!this.defaultBotToken) {
858
873
  const teamId = this.extractTeamIdFromInteractive(body);
859
874
  if (teamId) {
@@ -909,6 +924,18 @@ var SlackAdapter = class _SlackAdapter {
909
924
  this.handleMessageEvent(slackEvent, options);
910
925
  } else if (event.type === "reaction_added" || event.type === "reaction_removed") {
911
926
  this.handleReactionEvent(event, options);
927
+ } else if (event.type === "assistant_thread_started") {
928
+ this.handleAssistantThreadStarted(
929
+ event,
930
+ options
931
+ );
932
+ } else if (event.type === "assistant_thread_context_changed") {
933
+ this.handleAssistantContextChanged(
934
+ event,
935
+ options
936
+ );
937
+ } else if (event.type === "app_home_opened" && event.tab === "home") {
938
+ this.handleAppHomeOpened(event, options);
912
939
  }
913
940
  }
914
941
  }
@@ -941,6 +968,46 @@ var SlackAdapter = class _SlackAdapter {
941
968
  return new Response("", { status: 200 });
942
969
  }
943
970
  }
971
+ /**
972
+ * Handle Slack slash command payloads.
973
+ * Slash commands are sent as form-urlencoded with command, text, user_id, channel_id, etc.
974
+ */
975
+ async handleSlashCommand(params, options) {
976
+ if (!this.chat) {
977
+ this.logger.warn("Chat instance not initialized, ignoring slash command");
978
+ return new Response("", { status: 200 });
979
+ }
980
+ const command = params.get("command") || "";
981
+ const text = params.get("text") || "";
982
+ const userId = params.get("user_id") || "";
983
+ const channelId = params.get("channel_id") || "";
984
+ const triggerId = params.get("trigger_id") || void 0;
985
+ this.logger.debug("Processing Slack slash command", {
986
+ command,
987
+ text,
988
+ userId,
989
+ channelId,
990
+ triggerId
991
+ });
992
+ const userInfo = await this.lookupUser(userId);
993
+ const event = {
994
+ command,
995
+ text,
996
+ user: {
997
+ userId,
998
+ userName: userInfo.displayName,
999
+ fullName: userInfo.realName,
1000
+ isBot: false,
1001
+ isMe: false
1002
+ },
1003
+ adapter: this,
1004
+ raw: Object.fromEntries(params),
1005
+ triggerId,
1006
+ channelId: channelId ? `slack:${channelId}` : ""
1007
+ };
1008
+ this.chat.processSlashCommand(event, options);
1009
+ return new Response("", { status: 200 });
1010
+ }
944
1011
  /**
945
1012
  * Handle block_actions payload (button clicks in Block Kit).
946
1013
  */
@@ -952,20 +1019,18 @@ var SlackAdapter = class _SlackAdapter {
952
1019
  const channel = payload.channel?.id || payload.container?.channel_id;
953
1020
  const messageTs = payload.message?.ts || payload.container?.message_ts;
954
1021
  const threadTs = payload.message?.thread_ts || payload.container?.thread_ts || messageTs;
955
- if (!channel || !messageTs) {
956
- this.logger.warn("Missing channel or message_ts in block_actions", {
957
- channel,
958
- messageTs
959
- });
1022
+ const isViewAction = payload.container?.type === "view";
1023
+ if (!isViewAction && !channel) {
1024
+ this.logger.warn("Missing channel in block_actions", { channel });
960
1025
  return;
961
1026
  }
962
- const threadId = this.encodeThreadId({
1027
+ const threadId = channel && (threadTs || messageTs) ? this.encodeThreadId({
963
1028
  channel,
964
- threadTs: threadTs || messageTs
965
- });
1029
+ threadTs: threadTs || messageTs || ""
1030
+ }) : "";
966
1031
  const isEphemeral = payload.container?.is_ephemeral === true;
967
1032
  const responseUrl = payload.response_url;
968
- const messageId = isEphemeral && responseUrl ? this.encodeEphemeralMessageId(messageTs, responseUrl, payload.user.id) : messageTs;
1033
+ const messageId = isEphemeral && responseUrl && messageTs ? this.encodeEphemeralMessageId(messageTs, responseUrl, payload.user.id) : messageTs || "";
969
1034
  for (const action of payload.actions) {
970
1035
  const actionValue = action.selected_option?.value ?? action.value;
971
1036
  const actionEvent = {
@@ -1149,17 +1214,20 @@ var SlackAdapter = class _SlackAdapter {
1149
1214
  return;
1150
1215
  }
1151
1216
  const isDM = event.channel_type === "im";
1152
- const threadTs = isDM ? "" : event.thread_ts || event.ts;
1217
+ const threadTs = isDM ? event.thread_ts || "" : event.thread_ts || event.ts;
1153
1218
  const threadId = this.encodeThreadId({
1154
1219
  channel: event.channel,
1155
1220
  threadTs
1156
1221
  });
1157
- this.chat.processMessage(
1158
- this,
1159
- threadId,
1160
- () => this.parseSlackMessage(event, threadId),
1161
- options
1162
- );
1222
+ const isMention = isDM || event.type === "app_mention";
1223
+ const factory = async () => {
1224
+ const msg = await this.parseSlackMessage(event, threadId);
1225
+ if (isMention) {
1226
+ msg.isMention = true;
1227
+ }
1228
+ return msg;
1229
+ };
1230
+ this.chat.processMessage(this, threadId, factory, options);
1163
1231
  }
1164
1232
  /**
1165
1233
  * Handle reaction events from Slack (reaction_added, reaction_removed).
@@ -1203,9 +1271,196 @@ var SlackAdapter = class _SlackAdapter {
1203
1271
  };
1204
1272
  this.chat.processReaction({ ...reactionEvent, adapter: this }, options);
1205
1273
  }
1206
- async parseSlackMessage(event, threadId) {
1274
+ /**
1275
+ * Handle assistant_thread_started events from Slack's Assistants API.
1276
+ * Fires when a user opens a new assistant thread (DM with the bot).
1277
+ */
1278
+ handleAssistantThreadStarted(event, options) {
1279
+ if (!this.chat) {
1280
+ this.logger.warn(
1281
+ "Chat instance not initialized, ignoring assistant_thread_started"
1282
+ );
1283
+ return;
1284
+ }
1285
+ if (!event.assistant_thread) {
1286
+ this.logger.warn(
1287
+ "Malformed assistant_thread_started: missing assistant_thread"
1288
+ );
1289
+ return;
1290
+ }
1291
+ const { channel_id, thread_ts, user_id, context } = event.assistant_thread;
1292
+ const threadId = this.encodeThreadId({
1293
+ channel: channel_id,
1294
+ threadTs: thread_ts
1295
+ });
1296
+ this.chat.processAssistantThreadStarted(
1297
+ {
1298
+ threadId,
1299
+ userId: user_id,
1300
+ channelId: channel_id,
1301
+ threadTs: thread_ts,
1302
+ context: {
1303
+ channelId: context.channel_id,
1304
+ teamId: context.team_id,
1305
+ enterpriseId: context.enterprise_id,
1306
+ threadEntryPoint: context.thread_entry_point,
1307
+ forceSearch: context.force_search
1308
+ },
1309
+ adapter: this
1310
+ },
1311
+ options
1312
+ );
1313
+ }
1314
+ /**
1315
+ * Handle assistant_thread_context_changed events from Slack's Assistants API.
1316
+ * Fires when a user navigates to a different channel with the assistant panel open.
1317
+ */
1318
+ handleAssistantContextChanged(event, options) {
1319
+ if (!this.chat) {
1320
+ this.logger.warn(
1321
+ "Chat instance not initialized, ignoring assistant_thread_context_changed"
1322
+ );
1323
+ return;
1324
+ }
1325
+ if (!event.assistant_thread) {
1326
+ this.logger.warn(
1327
+ "Malformed assistant_thread_context_changed: missing assistant_thread"
1328
+ );
1329
+ return;
1330
+ }
1331
+ const { channel_id, thread_ts, user_id, context } = event.assistant_thread;
1332
+ const threadId = this.encodeThreadId({
1333
+ channel: channel_id,
1334
+ threadTs: thread_ts
1335
+ });
1336
+ this.chat.processAssistantContextChanged(
1337
+ {
1338
+ threadId,
1339
+ userId: user_id,
1340
+ channelId: channel_id,
1341
+ threadTs: thread_ts,
1342
+ context: {
1343
+ channelId: context.channel_id,
1344
+ teamId: context.team_id,
1345
+ enterpriseId: context.enterprise_id,
1346
+ threadEntryPoint: context.thread_entry_point,
1347
+ forceSearch: context.force_search
1348
+ },
1349
+ adapter: this
1350
+ },
1351
+ options
1352
+ );
1353
+ }
1354
+ /**
1355
+ * Handle app_home_opened events from Slack.
1356
+ * Fires when a user opens the bot's Home tab.
1357
+ */
1358
+ handleAppHomeOpened(event, options) {
1359
+ if (!this.chat) {
1360
+ this.logger.warn(
1361
+ "Chat instance not initialized, ignoring app_home_opened"
1362
+ );
1363
+ return;
1364
+ }
1365
+ this.chat.processAppHomeOpened(
1366
+ {
1367
+ userId: event.user,
1368
+ channelId: event.channel,
1369
+ adapter: this
1370
+ },
1371
+ options
1372
+ );
1373
+ }
1374
+ /**
1375
+ * Publish a Home tab view for a user.
1376
+ * Slack API: views.publish
1377
+ */
1378
+ async publishHomeView(userId, view) {
1379
+ await this.client.views.publish(
1380
+ // biome-ignore lint/suspicious/noExplicitAny: view blocks are consumer-defined
1381
+ this.withToken({ user_id: userId, view })
1382
+ );
1383
+ }
1384
+ /**
1385
+ * Set suggested prompts for an assistant thread.
1386
+ * Slack Assistants API: assistant.threads.setSuggestedPrompts
1387
+ */
1388
+ async setSuggestedPrompts(channelId, threadTs, prompts, title) {
1389
+ await this.client.assistant.threads.setSuggestedPrompts(
1390
+ this.withToken({
1391
+ channel_id: channelId,
1392
+ thread_ts: threadTs,
1393
+ prompts,
1394
+ title
1395
+ })
1396
+ );
1397
+ }
1398
+ /**
1399
+ * Set status/thinking indicator for an assistant thread.
1400
+ * Slack Assistants API: assistant.threads.setStatus
1401
+ */
1402
+ async setAssistantStatus(channelId, threadTs, status, loadingMessages) {
1403
+ await this.client.assistant.threads.setStatus(
1404
+ this.withToken({
1405
+ channel_id: channelId,
1406
+ thread_ts: threadTs,
1407
+ status,
1408
+ ...loadingMessages && { loading_messages: loadingMessages }
1409
+ })
1410
+ );
1411
+ }
1412
+ /**
1413
+ * Set title for an assistant thread (shown in History tab).
1414
+ * Slack Assistants API: assistant.threads.setTitle
1415
+ */
1416
+ async setAssistantTitle(channelId, threadTs, title) {
1417
+ await this.client.assistant.threads.setTitle(
1418
+ this.withToken({
1419
+ channel_id: channelId,
1420
+ thread_ts: threadTs,
1421
+ title
1422
+ })
1423
+ );
1424
+ }
1425
+ /**
1426
+ * Resolve inline user mentions in Slack mrkdwn text.
1427
+ * Converts <@U123> to <@U123|displayName> so that toAst/extractPlainText
1428
+ * renders them as @displayName instead of @U123.
1429
+ *
1430
+ * @param skipSelfMention - When true, skips the bot's own user ID so that
1431
+ * mention detection (which looks for @botUserId in the text) continues to
1432
+ * work. Set to false when parsing historical/channel messages where mention
1433
+ * detection doesn't apply.
1434
+ */
1435
+ async resolveInlineMentions(text, skipSelfMention) {
1436
+ const mentionPattern = /<@([A-Z0-9]+)(?:\|[^>]*)?>/g;
1437
+ const userIds = /* @__PURE__ */ new Set();
1438
+ let match = mentionPattern.exec(text);
1439
+ while (match) {
1440
+ userIds.add(match[1]);
1441
+ match = mentionPattern.exec(text);
1442
+ }
1443
+ if (userIds.size === 0) return text;
1444
+ if (skipSelfMention && this._botUserId) {
1445
+ userIds.delete(this._botUserId);
1446
+ }
1447
+ if (userIds.size === 0) return text;
1448
+ const lookups = await Promise.all(
1449
+ [...userIds].map(async (uid) => {
1450
+ const info = await this.lookupUser(uid);
1451
+ return [uid, info.displayName];
1452
+ })
1453
+ );
1454
+ const nameMap = new Map(lookups);
1455
+ return text.replace(/<@([A-Z0-9]+)(?:\|[^>]*)?>/g, (_m, uid) => {
1456
+ const name = nameMap.get(uid);
1457
+ return name ? `<@${uid}|${name}>` : `<@${uid}>`;
1458
+ });
1459
+ }
1460
+ async parseSlackMessage(event, threadId, options) {
1207
1461
  const isMe = this.isMessageFromSelf(event);
1208
- const text = event.text || "";
1462
+ const skipSelfMention = options?.skipSelfMention ?? true;
1463
+ const rawText = event.text || "";
1209
1464
  let userName = event.username || "unknown";
1210
1465
  let fullName = event.username || "unknown";
1211
1466
  if (event.user && !event.username) {
@@ -1213,6 +1468,7 @@ var SlackAdapter = class _SlackAdapter {
1213
1468
  userName = userInfo.displayName;
1214
1469
  fullName = userInfo.realName;
1215
1470
  }
1471
+ const text = await this.resolveInlineMentions(rawText, skipSelfMention);
1216
1472
  return new Message({
1217
1473
  id: event.ts || "",
1218
1474
  threadId,
@@ -1686,7 +1942,10 @@ var SlackAdapter = class _SlackAdapter {
1686
1942
  await streamer.append({ markdown_text: chunk });
1687
1943
  }
1688
1944
  }
1689
- const result = await streamer.stop();
1945
+ const result = await streamer.stop(
1946
+ // biome-ignore lint/suspicious/noExplicitAny: stopBlocks are platform-specific Block Kit elements
1947
+ options?.stopBlocks ? { blocks: options.stopBlocks } : void 0
1948
+ );
1690
1949
  const messageTs = result.message?.ts ?? result.ts;
1691
1950
  this.logger.debug("Slack: stream complete", { messageId: messageTs });
1692
1951
  return {
@@ -1898,7 +2157,7 @@ var SlackAdapter = class _SlackAdapter {
1898
2157
  }
1899
2158
  decodeThreadId(threadId) {
1900
2159
  const parts = threadId.split(":");
1901
- if (parts.length !== 3 || parts[0] !== "slack") {
2160
+ if (parts.length < 2 || parts.length > 3 || parts[0] !== "slack") {
1902
2161
  throw new ValidationError(
1903
2162
  "slack",
1904
2163
  `Invalid Slack thread ID: ${threadId}`
@@ -1906,7 +2165,7 @@ var SlackAdapter = class _SlackAdapter {
1906
2165
  }
1907
2166
  return {
1908
2167
  channel: parts[1],
1909
- threadTs: parts[2]
2168
+ threadTs: parts.length === 3 ? parts[2] : ""
1910
2169
  };
1911
2170
  }
1912
2171
  parseMessage(raw) {
@@ -1950,6 +2209,221 @@ var SlackAdapter = class _SlackAdapter {
1950
2209
  )
1951
2210
  });
1952
2211
  }
2212
+ // =========================================================================
2213
+ // Channel-level methods
2214
+ // =========================================================================
2215
+ /**
2216
+ * Derive channel ID from a Slack thread ID.
2217
+ * Slack thread IDs are "slack:CHANNEL:THREAD_TS", channel ID is "slack:CHANNEL".
2218
+ */
2219
+ channelIdFromThreadId(threadId) {
2220
+ const { channel } = this.decodeThreadId(threadId);
2221
+ return `slack:${channel}`;
2222
+ }
2223
+ /**
2224
+ * Fetch channel-level messages (conversations.history, not thread replies).
2225
+ */
2226
+ async fetchChannelMessages(channelId, options = {}) {
2227
+ const channel = channelId.split(":")[1];
2228
+ if (!channel) {
2229
+ throw new ValidationError(
2230
+ "slack",
2231
+ `Invalid Slack channel ID: ${channelId}`
2232
+ );
2233
+ }
2234
+ const direction = options.direction ?? "backward";
2235
+ const limit = options.limit || 100;
2236
+ try {
2237
+ if (direction === "forward") {
2238
+ return await this.fetchChannelMessagesForward(
2239
+ channel,
2240
+ limit,
2241
+ options.cursor
2242
+ );
2243
+ }
2244
+ return await this.fetchChannelMessagesBackward(
2245
+ channel,
2246
+ limit,
2247
+ options.cursor
2248
+ );
2249
+ } catch (error) {
2250
+ this.handleSlackError(error);
2251
+ }
2252
+ }
2253
+ async fetchChannelMessagesForward(channel, limit, cursor) {
2254
+ this.logger.debug("Slack API: conversations.history (forward)", {
2255
+ channel,
2256
+ limit,
2257
+ cursor
2258
+ });
2259
+ const result = await this.client.conversations.history(
2260
+ this.withToken({
2261
+ channel,
2262
+ limit,
2263
+ oldest: cursor,
2264
+ inclusive: cursor ? false : void 0
2265
+ })
2266
+ );
2267
+ const slackMessages = (result.messages || []).reverse();
2268
+ const messages = await Promise.all(
2269
+ slackMessages.map((msg) => {
2270
+ const threadTs = msg.thread_ts || msg.ts || "";
2271
+ const threadId = `slack:${channel}:${threadTs}`;
2272
+ return this.parseSlackMessage(msg, threadId, {
2273
+ skipSelfMention: false
2274
+ });
2275
+ })
2276
+ );
2277
+ let nextCursor;
2278
+ if (result.has_more && slackMessages.length > 0) {
2279
+ const newest = slackMessages[slackMessages.length - 1];
2280
+ if (newest?.ts) {
2281
+ nextCursor = newest.ts;
2282
+ }
2283
+ }
2284
+ return {
2285
+ messages,
2286
+ nextCursor
2287
+ };
2288
+ }
2289
+ async fetchChannelMessagesBackward(channel, limit, cursor) {
2290
+ this.logger.debug("Slack API: conversations.history (backward)", {
2291
+ channel,
2292
+ limit,
2293
+ cursor
2294
+ });
2295
+ const result = await this.client.conversations.history(
2296
+ this.withToken({
2297
+ channel,
2298
+ limit,
2299
+ latest: cursor,
2300
+ inclusive: cursor ? false : void 0
2301
+ })
2302
+ );
2303
+ const slackMessages = result.messages || [];
2304
+ const chronological = [...slackMessages].reverse();
2305
+ const messages = await Promise.all(
2306
+ chronological.map((msg) => {
2307
+ const threadTs = msg.thread_ts || msg.ts || "";
2308
+ const threadId = `slack:${channel}:${threadTs}`;
2309
+ return this.parseSlackMessage(msg, threadId, {
2310
+ skipSelfMention: false
2311
+ });
2312
+ })
2313
+ );
2314
+ let nextCursor;
2315
+ if (result.has_more && chronological.length > 0) {
2316
+ const oldest = chronological[0];
2317
+ if (oldest?.ts) {
2318
+ nextCursor = oldest.ts;
2319
+ }
2320
+ }
2321
+ return {
2322
+ messages,
2323
+ nextCursor
2324
+ };
2325
+ }
2326
+ /**
2327
+ * List threads in a Slack channel.
2328
+ * Fetches channel history and filters for messages with replies.
2329
+ */
2330
+ async listThreads(channelId, options = {}) {
2331
+ const channel = channelId.split(":")[1];
2332
+ if (!channel) {
2333
+ throw new ValidationError(
2334
+ "slack",
2335
+ `Invalid Slack channel ID: ${channelId}`
2336
+ );
2337
+ }
2338
+ const limit = options.limit || 50;
2339
+ try {
2340
+ this.logger.debug("Slack API: conversations.history (listThreads)", {
2341
+ channel,
2342
+ limit,
2343
+ cursor: options.cursor
2344
+ });
2345
+ const result = await this.client.conversations.history(
2346
+ this.withToken({
2347
+ channel,
2348
+ limit: Math.min(limit * 3, 200),
2349
+ // Fetch extra since not all have threads
2350
+ cursor: options.cursor
2351
+ })
2352
+ );
2353
+ const slackMessages = result.messages || [];
2354
+ const threadMessages = slackMessages.filter(
2355
+ (msg) => (msg.reply_count ?? 0) > 0
2356
+ );
2357
+ const selected = threadMessages.slice(0, limit);
2358
+ const threads = await Promise.all(
2359
+ selected.map(async (msg) => {
2360
+ const threadTs = msg.ts || "";
2361
+ const threadId = `slack:${channel}:${threadTs}`;
2362
+ const rootMessage = await this.parseSlackMessage(msg, threadId, {
2363
+ skipSelfMention: false
2364
+ });
2365
+ return {
2366
+ id: threadId,
2367
+ rootMessage,
2368
+ replyCount: msg.reply_count,
2369
+ lastReplyAt: msg.latest_reply ? new Date(Number.parseFloat(msg.latest_reply) * 1e3) : void 0
2370
+ };
2371
+ })
2372
+ );
2373
+ const nextCursor = result.response_metadata?.next_cursor;
2374
+ return {
2375
+ threads,
2376
+ nextCursor: nextCursor || void 0
2377
+ };
2378
+ } catch (error) {
2379
+ this.handleSlackError(error);
2380
+ }
2381
+ }
2382
+ /**
2383
+ * Fetch Slack channel info/metadata.
2384
+ */
2385
+ async fetchChannelInfo(channelId) {
2386
+ const channel = channelId.split(":")[1];
2387
+ if (!channel) {
2388
+ throw new ValidationError(
2389
+ "slack",
2390
+ `Invalid Slack channel ID: ${channelId}`
2391
+ );
2392
+ }
2393
+ try {
2394
+ this.logger.debug("Slack API: conversations.info (channel)", { channel });
2395
+ const result = await this.client.conversations.info(
2396
+ this.withToken({ channel })
2397
+ );
2398
+ const info = result.channel;
2399
+ return {
2400
+ id: channelId,
2401
+ name: info?.name ? `#${info.name}` : void 0,
2402
+ isDM: info?.is_im || info?.is_mpim || false,
2403
+ memberCount: info?.num_members,
2404
+ metadata: {
2405
+ purpose: info?.purpose?.value,
2406
+ topic: info?.topic?.value
2407
+ }
2408
+ };
2409
+ } catch (error) {
2410
+ this.handleSlackError(error);
2411
+ }
2412
+ }
2413
+ /**
2414
+ * Post a top-level message to a channel (not in a thread).
2415
+ */
2416
+ async postChannelMessage(channelId, message) {
2417
+ const channel = channelId.split(":")[1];
2418
+ if (!channel) {
2419
+ throw new ValidationError(
2420
+ "slack",
2421
+ `Invalid Slack channel ID: ${channelId}`
2422
+ );
2423
+ }
2424
+ const syntheticThreadId = `slack:${channel}:`;
2425
+ return this.postMessage(syntheticThreadId, message);
2426
+ }
1953
2427
  renderFormatted(content) {
1954
2428
  return this.formatConverter.fromAst(content);
1955
2429
  }