claude-threads 1.39.1 → 1.39.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.39.3] - 2026-10-01
9
+
10
+ ### Fixed
11
+ - **Mattermost: a skin-toned 👍 no longer denies** (#629). Mattermost names an emoji picked with a skin tone `+1_medium_skin_tone` (verified against Mattermost v11), and the permission prompt treats any emoji it does not recognize as a denial. 1.39.2 fixed Slack's `+1::skin-tone-3` form; the Mattermost form is now matched on its base name too, for permission prompts, plan approvals and the other reaction controls.
12
+ - **One failed update no longer turns a code block into a post per flush** (#620). When the platform refused an update of the post Claude was writing a code block into, the bot gave up on that post at once. The continuation started in the middle of the block without its opening fence, so the rest rendered as prose, the height check split it every few lines (a new post per flush, close to 200 posts for a 2000-line file), and the closing fence opened a new block. A failed update is now retried on the next flush, which recovers the post completely for a passing error; only after three failures in a row is the post given up, with its code block reopened at the start of the next one.
13
+
14
+ ## [1.39.2] - 2026-10-01
15
+
16
+ ### Fixed
17
+ - **Slack: reacting to a permission prompt works again** (#622, thanks @elganfd). With interactive permissions on Slack every 👍 / ✅ / 👎 on a tool permission prompt was ignored, so the prompt sat until it timed out and denied. Two causes, each enough on its own: the permission server sent its `users.info` lookup as a JSON body, which Slack ignores on read methods, so every reacting user came back `user_not_found` and counted as unauthorized; and it opened a Socket Mode connection of its own, while Slack spreads an app's events over all its connections, so it saw only part of the reactions (and took that part of the bot's own events while a prompt waited). Read methods now go out as GET requests, and the prompt is polled for reactions instead of watched over a second connection. The same JSON-body problem affected `read_post`, `read_thread`, `read_channel_history`, the channel lookups and DM recipient resolution on Slack; those are fixed too. Plan approvals and questions were never affected.
18
+ - **A skin-toned 👍 no longer denies** (#628). Slack names an emoji picked with a skin tone `+1::skin-tone-3`, and the permission prompt treats any emoji it does not recognize as a denial. Emoji are now matched on their base name, which also covers plan approvals and the other reaction controls.
19
+
20
+ ### Security
21
+ - `fast-uri` 4.1.4 → 4.2.1 (#625) and `ip-address` 10.4.0 → 10.7.2 (#626), both bundled into what users run.
22
+
23
+ ### Changed
24
+ - `@modelcontextprotocol/sdk` 1.30.1, `hono` 4.13.9, `ws` 8.22.0 (#624).
25
+
8
26
  ## [1.39.1] - 2026-09-25
9
27
 
10
28
  ### Fixed
package/dist/index.js CHANGED
@@ -5290,8 +5290,8 @@ function buildPermissionArgs(opts) {
5290
5290
  mcpEnv[AGENT_FEATURES_ENV.DCM] = "1";
5291
5291
  }
5292
5292
  }
5293
- if (opts.platformConfig.appToken) {
5294
- mcpEnv.PLATFORM_APP_TOKEN = opts.platformConfig.appToken;
5293
+ if (opts.platformConfig.apiUrl) {
5294
+ mcpEnv.PLATFORM_API_URL = opts.platformConfig.apiUrl;
5295
5295
  }
5296
5296
  if (opts.workingDir) {
5297
5297
  mcpEnv[OUTBOUND_ENV.SESSION_WORKING_DIR] = opts.workingDir;
@@ -6032,23 +6032,26 @@ var init_usage = __esm(() => {
6032
6032
  });
6033
6033
 
6034
6034
  // src/utils/emoji.ts
6035
+ function withoutSkinTone(emoji) {
6036
+ return emoji.replace(/::skin-tone-\d$/, "").replace(/(?<=.)_(?:light|medium_light|medium|medium_dark|dark)_skin_tone$/, "");
6037
+ }
6035
6038
  function isApprovalEmoji(emoji) {
6036
- return APPROVAL_EMOJIS.includes(emoji);
6039
+ return APPROVAL_EMOJIS.includes(withoutSkinTone(emoji));
6037
6040
  }
6038
6041
  function isDenialEmoji(emoji) {
6039
- return DENIAL_EMOJIS.includes(emoji);
6042
+ return DENIAL_EMOJIS.includes(withoutSkinTone(emoji));
6040
6043
  }
6041
6044
  function isAllowAllEmoji(emoji) {
6042
- return ALLOW_ALL_EMOJIS.includes(emoji);
6045
+ return ALLOW_ALL_EMOJIS.includes(withoutSkinTone(emoji));
6043
6046
  }
6044
6047
  function isCancelEmoji(emoji) {
6045
- return CANCEL_EMOJIS.includes(emoji);
6048
+ return CANCEL_EMOJIS.includes(withoutSkinTone(emoji));
6046
6049
  }
6047
6050
  function isEscapeEmoji(emoji) {
6048
- return ESCAPE_EMOJIS.includes(emoji);
6051
+ return ESCAPE_EMOJIS.includes(withoutSkinTone(emoji));
6049
6052
  }
6050
6053
  function isResumeEmoji(emoji) {
6051
- return RESUME_EMOJIS.includes(emoji);
6054
+ return RESUME_EMOJIS.includes(withoutSkinTone(emoji));
6052
6055
  }
6053
6056
  function isMinimizeToggleEmoji(emoji) {
6054
6057
  return MINIMIZE_TOGGLE_EMOJIS.includes(emoji);
@@ -17137,11 +17140,23 @@ var require_websocket = __commonJS(function(exports, module) {
17137
17140
  this._isServer = false;
17138
17141
  this._redirects = 0;
17139
17142
  if (protocols === undefined) {
17140
- protocols = [];
17143
+ if (!options || options.protocols === undefined) {
17144
+ protocols = [];
17145
+ } else if (Array.isArray(options.protocols)) {
17146
+ protocols = options.protocols;
17147
+ } else {
17148
+ protocols = [options.protocols];
17149
+ }
17141
17150
  } else if (!Array.isArray(protocols)) {
17142
17151
  if (typeof protocols === "object" && protocols !== null) {
17143
17152
  options = protocols;
17144
- protocols = [];
17153
+ if (options.protocols === undefined) {
17154
+ protocols = [];
17155
+ } else if (Array.isArray(options.protocols)) {
17156
+ protocols = options.protocols;
17157
+ } else {
17158
+ protocols = [options.protocols];
17159
+ }
17145
17160
  } else {
17146
17161
  protocols = [protocols];
17147
17162
  }
@@ -17260,7 +17275,6 @@ var require_websocket = __commonJS(function(exports, module) {
17260
17275
  }
17261
17276
  return;
17262
17277
  }
17263
- this._readyState = WebSocket.CLOSING;
17264
17278
  this._sender.close(code, data, !this._isServer, (err) => {
17265
17279
  if (err)
17266
17280
  return;
@@ -17269,6 +17283,7 @@ var require_websocket = __commonJS(function(exports, module) {
17269
17283
  this._socket.end();
17270
17284
  }
17271
17285
  });
17286
+ this._readyState = WebSocket.CLOSING;
17272
17287
  setCloseTimer(this);
17273
17288
  }
17274
17289
  pause() {
@@ -17456,6 +17471,7 @@ var require_websocket = __commonJS(function(exports, module) {
17456
17471
  socketPath: undefined,
17457
17472
  hostname: undefined,
17458
17473
  protocol: undefined,
17474
+ protocols: undefined,
17459
17475
  timeout: undefined,
17460
17476
  method: "GET",
17461
17477
  host: undefined,
@@ -30090,7 +30106,7 @@ var require_react_reconciler = __commonJS(function(exports, module) {
30090
30106
  });
30091
30107
 
30092
30108
  // node_modules/ws/wrapper.mjs
30093
- var import_stream, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket5, import_websocket_server, wrapper_default;
30109
+ var import_stream, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket4, import_websocket_server, wrapper_default;
30094
30110
  var init_wrapper = __esm(() => {
30095
30111
  import_stream = __toESM(require_stream(), 1);
30096
30112
  import_extension = __toESM(require_extension(), 1);
@@ -30098,9 +30114,9 @@ var init_wrapper = __esm(() => {
30098
30114
  import_receiver = __toESM(require_receiver(), 1);
30099
30115
  import_sender = __toESM(require_sender(), 1);
30100
30116
  import_subprotocol = __toESM(require_subprotocol(), 1);
30101
- import_websocket5 = __toESM(require_websocket(), 1);
30117
+ import_websocket4 = __toESM(require_websocket(), 1);
30102
30118
  import_websocket_server = __toESM(require_websocket_server(), 1);
30103
- wrapper_default = import_websocket5.default;
30119
+ wrapper_default = import_websocket4.default;
30104
30120
  });
30105
30121
 
30106
30122
  // node_modules/ink/build/devtools-window-polyfill.js
@@ -60281,7 +60297,8 @@ class ContentExecutor extends BaseExecutor {
60281
60297
  headerPostId: null,
60282
60298
  headerBody: "",
60283
60299
  turnOpen: false,
60284
- paragraphBreak: false
60300
+ paragraphBreak: false,
60301
+ updateFailures: 0
60285
60302
  };
60286
60303
  }
60287
60304
  setHeader(line) {
@@ -60302,6 +60319,7 @@ class ContentExecutor extends BaseExecutor {
60302
60319
  this.state.headerPostId = null;
60303
60320
  this.state.headerBody = "";
60304
60321
  this.state.turnOpen = false;
60322
+ this.state.updateFailures = 0;
60305
60323
  }
60306
60324
  getRenderedCurrentPost() {
60307
60325
  const postId = this.state.currentPostId;
@@ -60328,7 +60346,9 @@ ${body}` : this.state.header;
60328
60346
  this.state.headerDirty = false;
60329
60347
  if (this.state.headerPostId) {
60330
60348
  const postId = this.state.headerPostId;
60349
+ const failures = this.state.updateFailures;
60331
60350
  await this.tryUpdatePost(ctx, postId, this.state.headerBody, "header", { reason: "header_update", headerLength: this.state.header?.length ?? 0 }, { reason: "header_update_failed" }, () => {}, () => {});
60351
+ this.state.updateFailures = failures;
60332
60352
  return;
60333
60353
  }
60334
60354
  if (this.state.header && this.state.turnOpen) {
@@ -60351,6 +60371,7 @@ ${body}` : this.state.header;
60351
60371
  this.state.headerBody = content;
60352
60372
  this.state.headerDirty = false;
60353
60373
  }
60374
+ this.state.updateFailures = 0;
60354
60375
  onSuccess();
60355
60376
  ctx.threadLogger?.logExecutor("content", "update", postId, successDetails, logTag);
60356
60377
  } catch (err) {
@@ -60363,8 +60384,11 @@ ${body}` : this.state.header;
60363
60384
  closeCurrentPost(ctx) {
60364
60385
  const oldPostId = this.state.currentPostId;
60365
60386
  const contentLength = this.state.currentPostContent.length;
60387
+ if (this.state.updateFailures > 0 && this.state.pendingContent.trim())
60388
+ this.giveUpCurrentPost();
60366
60389
  this.state.currentPostId = null;
60367
60390
  this.state.currentPostContent = "";
60391
+ this.state.updateFailures = 0;
60368
60392
  if (ctx?.threadLogger && oldPostId) {
60369
60393
  ctx.threadLogger.logExecutor("content", "close", oldPostId, {
60370
60394
  contentLength,
@@ -60420,13 +60444,22 @@ ${body}` : this.state.header;
60420
60444
  this.state.turnOpen = false;
60421
60445
  return;
60422
60446
  }
60423
- await this.flushPending(ctx);
60424
- if (this.state.headerDirty)
60447
+ const postBefore = this.state.currentPostId;
60448
+ await this.flushPending(ctx, reason);
60449
+ if (reason === "result") {
60450
+ if (this.state.updateFailures > 0 && this.state.currentPostId !== null && this.state.pendingContent.trim()) {
60451
+ await this.flushPending(ctx, reason);
60452
+ }
60453
+ if (postBefore !== null && this.state.currentPostId === null && this.state.pendingContent.trim()) {
60454
+ await this.flushPending(ctx, reason);
60455
+ }
60456
+ }
60457
+ if (this.state.headerDirty && this.state.updateFailures === 0)
60425
60458
  await this.renderHeaderOnly(ctx);
60426
60459
  if (reason === "result")
60427
60460
  this.state.turnOpen = false;
60428
60461
  }
60429
- async flushPending(ctx) {
60462
+ async flushPending(ctx, reason) {
60430
60463
  const pendingAtFlushStart = this.state.pendingContent;
60431
60464
  let content = ctx.formatter.formatMarkdown(pendingAtFlushStart).trim();
60432
60465
  const { maxLength: MAX_POST_LENGTH, hardThreshold: HARD_CONTINUATION_THRESHOLD } = ctx.platform.getMessageLimits();
@@ -60444,7 +60477,7 @@ ${body}` : this.state.header;
60444
60477
  const shouldBreakEarly = this.state.currentPostId && combinedContent.length > MIN_BREAK_THRESHOLD && ctx.contentBreaker.shouldFlushEarly(combinedContent);
60445
60478
  const reserve = this.headerReserve(this.state.currentPostId);
60446
60479
  if (this.state.currentPostId && (combinedContent.length + reserve > HARD_CONTINUATION_THRESHOLD || shouldBreakEarly)) {
60447
- await this.handleSplit(ctx, combinedContent, pendingAtFlushStart, HARD_CONTINUATION_THRESHOLD);
60480
+ await this.handleSplit(ctx, combinedContent, pendingAtFlushStart, HARD_CONTINUATION_THRESHOLD, reason);
60448
60481
  return;
60449
60482
  }
60450
60483
  if (this.state.currentPostId && content.length + reserve > MAX_POST_LENGTH) {
@@ -60479,10 +60512,7 @@ ${body}` : this.state.header;
60479
60512
  await this.tryUpdatePost(ctx, postId, combinedContent, "flush", { newContentLength: content.length, combinedLength: combinedContent.length }, (err) => ({ failedOp: "updatePost", error: String(err) }), () => {
60480
60513
  this.state.currentPostContent = combinedContent;
60481
60514
  this.clearFlushedContent(pendingAtFlushStart);
60482
- }, () => {
60483
- this.state.currentPostId = null;
60484
- this.state.currentPostContent = "";
60485
- });
60515
+ }, () => this.onInPlaceUpdateFailed(reason));
60486
60516
  } else {
60487
60517
  const chunks = splitContentForHeight(content, ctx.contentBreaker).flatMap((chunk) => chunk.length + reserve > MAX_POST_LENGTH ? splitByLength(chunk, HARD_CONTINUATION_THRESHOLD - reserve) : [chunk]);
60488
60518
  ctx.threadLogger?.logExecutor("content", "create_start", "none", {
@@ -60493,7 +60523,7 @@ ${body}` : this.state.header;
60493
60523
  await this.postChunks(ctx, chunks, pendingAtFlushStart, false);
60494
60524
  }
60495
60525
  }
60496
- async splitOversized(ctx, content, pendingAtFlushStart, hardThreshold) {
60526
+ async splitOversized(ctx, content, pendingAtFlushStart, hardThreshold, reason) {
60497
60527
  const postId = this.state.currentPostId;
60498
60528
  if (!postId)
60499
60529
  return false;
@@ -60504,17 +60534,35 @@ ${body}` : this.state.header;
60504
60534
  let landed = false;
60505
60535
  await this.tryUpdatePost(ctx, postId, firstPiece, "handleSplit", { reason: "oversized_code_block", contentLength: content.length, pieces: rest.length + 1 }, { reason: "oversized_code_block_failed" }, () => {
60506
60536
  landed = true;
60507
- }, () => {
60508
- if (postId === this.state.headerPostId)
60509
- this.state.headerBody = firstPiece;
60510
- });
60537
+ }, () => {});
60538
+ if (!landed) {
60539
+ this.onInPlaceUpdateFailed(reason);
60540
+ return true;
60541
+ }
60511
60542
  this.state.currentPostId = null;
60512
60543
  this.state.currentPostContent = "";
60513
- if (landed)
60514
- this.clearFlushedContent(pendingAtFlushStart);
60515
- await this.postChunks(ctx, rest, pendingAtFlushStart, landed);
60544
+ this.clearFlushedContent(pendingAtFlushStart);
60545
+ await this.postChunks(ctx, rest, pendingAtFlushStart, true);
60516
60546
  return true;
60517
60547
  }
60548
+ onInPlaceUpdateFailed(reason) {
60549
+ this.state.updateFailures++;
60550
+ const limit = reason === "result" ? 2 : MAX_IN_PLACE_UPDATE_FAILURES;
60551
+ if (this.state.updateFailures < limit)
60552
+ return;
60553
+ this.giveUpCurrentPost();
60554
+ }
60555
+ giveUpCurrentPost() {
60556
+ const opener = openFenceOf(this.state.currentPostContent);
60557
+ this.state.currentPostId = null;
60558
+ this.state.currentPostContent = "";
60559
+ this.state.updateFailures = 0;
60560
+ if (!opener)
60561
+ return;
60562
+ const rest = this.state.pendingContent.replace(/^\n+/, "");
60563
+ this.state.pendingContent = /^```[ \t]*(\n|$)/.test(rest) ? rest.replace(/^```[ \t]*\n?/, "") : `${opener}
60564
+ ${rest}`;
60565
+ }
60518
60566
  async postChunks(ctx, chunks, pendingAtFlushStart, flushCleared) {
60519
60567
  let cleared = flushCleared;
60520
60568
  for (let i = 0;i < chunks.length; i++) {
@@ -60538,7 +60586,7 @@ ${this.state.pendingContent}` : "");
60538
60586
  }
60539
60587
  }
60540
60588
  }
60541
- async handleSplit(ctx, content, pendingAtFlushStart, hardThreshold) {
60589
+ async handleSplit(ctx, content, pendingAtFlushStart, hardThreshold, reason) {
60542
60590
  let breakPoint;
60543
60591
  let codeBlockOpenPosition;
60544
60592
  if (content.length > hardThreshold) {
@@ -60586,26 +60634,21 @@ ${this.state.pendingContent}` : "");
60586
60634
  await this.tryUpdatePost(ctx, postId, content, "handleSplit", { reason: "soft_break_no_breakpoint", contentLength: content.length }, { reason: "soft_break_no_breakpoint_failed" }, () => {
60587
60635
  this.state.currentPostContent = content;
60588
60636
  this.clearFlushedContent(pendingAtFlushStart);
60589
- }, () => {
60590
- this.state.currentPostId = null;
60591
- });
60637
+ }, () => this.onInPlaceUpdateFailed(reason));
60592
60638
  }
60593
60639
  return;
60594
60640
  }
60595
60641
  }
60596
60642
  if (codeBlockOpenPosition !== undefined) {
60597
60643
  if (codeBlockOpenPosition === 0) {
60598
- if (await this.splitOversized(ctx, content, pendingAtFlushStart, hardThreshold))
60644
+ if (await this.splitOversized(ctx, content, pendingAtFlushStart, hardThreshold, reason))
60599
60645
  return;
60600
60646
  if (this.state.currentPostId) {
60601
60647
  const postId = this.state.currentPostId;
60602
60648
  await this.tryUpdatePost(ctx, postId, content, "handleSplit", { reason: "code_block_at_start", contentLength: content.length }, { reason: "code_block_at_start_failed" }, () => {
60603
60649
  this.state.currentPostContent = content;
60604
60650
  this.clearFlushedContent(pendingAtFlushStart);
60605
- }, () => {
60606
- this.state.currentPostId = null;
60607
- this.state.currentPostContent = "";
60608
- });
60651
+ }, () => this.onInPlaceUpdateFailed(reason));
60609
60652
  }
60610
60653
  return;
60611
60654
  }
@@ -60614,17 +60657,14 @@ ${this.state.pendingContent}` : "");
60614
60657
  if (breakBeforeCodeBlock > 0) {
60615
60658
  breakPoint = breakBeforeCodeBlock;
60616
60659
  } else {
60617
- if (await this.splitOversized(ctx, content, pendingAtFlushStart, hardThreshold))
60660
+ if (await this.splitOversized(ctx, content, pendingAtFlushStart, hardThreshold, reason))
60618
60661
  return;
60619
60662
  if (this.state.currentPostId) {
60620
60663
  const postId = this.state.currentPostId;
60621
60664
  await this.tryUpdatePost(ctx, postId, content, "handleSplit", { reason: "no_break_before_code_block", contentLength: content.length }, { reason: "no_break_before_code_block_failed" }, () => {
60622
60665
  this.state.currentPostContent = content;
60623
60666
  this.clearFlushedContent(pendingAtFlushStart);
60624
- }, () => {
60625
- this.state.currentPostId = null;
60626
- this.state.currentPostContent = "";
60627
- });
60667
+ }, () => this.onInPlaceUpdateFailed(reason));
60628
60668
  }
60629
60669
  return;
60630
60670
  }
@@ -60636,11 +60676,11 @@ ${this.state.pendingContent}` : "");
60636
60676
  const postId = this.state.currentPostId;
60637
60677
  await this.tryUpdatePost(ctx, postId, firstPart, "handleSplit", { reason: "split_first_part", firstPartLength: firstPart.length, remainderLength: remainder.length }, { reason: "split_first_part_failed" }, () => {
60638
60678
  firstPartLanded = true;
60639
- }, () => {
60640
- if (postId === this.state.headerPostId) {
60641
- this.state.headerBody = firstPart;
60642
- }
60643
- });
60679
+ }, () => {});
60680
+ if (!firstPartLanded) {
60681
+ this.onInPlaceUpdateFailed(reason);
60682
+ return;
60683
+ }
60644
60684
  }
60645
60685
  this.state.currentPostId = null;
60646
60686
  this.state.currentPostContent = "";
@@ -60673,6 +60713,7 @@ ${content}` : header : content;
60673
60713
  if (bumpedPostId) {
60674
60714
  adoptAsHeaderPost(bumpedPostId);
60675
60715
  this.state.currentPostId = bumpedPostId;
60716
+ this.state.updateFailures = 0;
60676
60717
  this.state.currentPostContent = content;
60677
60718
  this.clearFlushedContent(pendingAtFlushStart);
60678
60719
  ctx.threadLogger?.logExecutor("content", "create", bumpedPostId, {
@@ -60689,6 +60730,7 @@ ${content}` : header : content;
60689
60730
  const post = await ctx.createPost(rendered, { type: "content" });
60690
60731
  adoptAsHeaderPost(post.id);
60691
60732
  this.state.currentPostId = post.id;
60733
+ this.state.updateFailures = 0;
60692
60734
  this.state.currentPostContent = content;
60693
60735
  this.clearFlushedContent(pendingAtFlushStart);
60694
60736
  ctx.logger.debug(`Created post ${formatShortId(post.id)}`);
@@ -60714,6 +60756,11 @@ ${content}` : header : content;
60714
60756
  }
60715
60757
  }
60716
60758
  var FENCE_LINE = /^```.*$/gm;
60759
+ var MAX_IN_PLACE_UPDATE_FAILURES = 3;
60760
+ function openFenceOf(text) {
60761
+ const fences = text.match(FENCE_LINE) ?? [];
60762
+ return fences.length % 2 === 1 ? fences[fences.length - 1] : null;
60763
+ }
60717
60764
  var FENCE_CLOSE = "\n```";
60718
60765
  function splitByLength(text, max) {
60719
60766
  const room = max - FENCE_CLOSE.length;
@@ -71889,7 +71936,7 @@ class SlackClient extends BasePlatformClient {
71889
71936
  token: this.botToken,
71890
71937
  channelId: this.channelId,
71891
71938
  allowedUsers: this.allowedUsers,
71892
- appToken: this.appToken,
71939
+ apiUrl: this.apiUrl,
71893
71940
  outboundFiles: this.outboundFiles,
71894
71941
  mcpServers: this.mcpServers,
71895
71942
  strictMcpConfig: this.strictMcpConfig,
@@ -72197,6 +72244,14 @@ var apiLog = createLogger("mm-api");
72197
72244
 
72198
72245
  // src/platform/slack/mcp-platform-api.ts
72199
72246
  init_logger();
72247
+ var READ_METHODS = new Set([
72248
+ "users.info",
72249
+ "conversations.history",
72250
+ "conversations.replies",
72251
+ "conversations.info",
72252
+ "conversations.members",
72253
+ "reactions.get"
72254
+ ]);
72200
72255
  // src/session/manager.ts
72201
72256
  import { EventEmitter as EventEmitter4 } from "events";
72202
72257
 
@@ -3547,6 +3547,22 @@ var require_utils = __commonJS(function(exports, module) {
3547
3547
  }
3548
3548
  return output;
3549
3549
  }
3550
+ function recomposeZonedIPv6Host(component) {
3551
+ const zone = component.ipv6Zone;
3552
+ if (zone === undefined)
3553
+ return;
3554
+ const host = component.host;
3555
+ const separator = host.indexOf("%");
3556
+ if (separator === -1)
3557
+ return;
3558
+ const address = host.slice(0, separator);
3559
+ const escaped = address + "%25" + zone;
3560
+ const derived = normalizeIPv6("[" + escaped + "]");
3561
+ if (derived.isIPV6 === true && derived.host === host) {
3562
+ return "[" + escaped + "]";
3563
+ }
3564
+ return;
3565
+ }
3550
3566
  function recomposeAuthority(component) {
3551
3567
  const uriTokens = [];
3552
3568
  if (component.userinfo !== undefined) {
@@ -3556,15 +3572,20 @@ var require_utils = __commonJS(function(exports, module) {
3556
3572
  if (component.host !== undefined) {
3557
3573
  let host = component.host;
3558
3574
  if (!isIPv4(host)) {
3559
- let ipV6res = normalizeIPv6(host);
3560
- if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
3561
- host = normalizePercentEncoding(host, true);
3562
- ipV6res = normalizeIPv6(host);
3563
- }
3564
- if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
3565
- host = `[${ipV6res.escapedHost}]`;
3575
+ const zonedHost = recomposeZonedIPv6Host(component);
3576
+ if (zonedHost !== undefined) {
3577
+ host = zonedHost;
3566
3578
  } else {
3567
- host = reescapeHostDelimiters(host, false);
3579
+ let ipV6res = normalizeIPv6(host);
3580
+ if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
3581
+ host = normalizePercentEncoding(host, true);
3582
+ ipV6res = normalizeIPv6(host);
3583
+ }
3584
+ if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
3585
+ host = `[${ipV6res.escapedHost}]`;
3586
+ } else {
3587
+ host = reescapeHostDelimiters(host, false);
3588
+ }
3568
3589
  }
3569
3590
  }
3570
3591
  uriTokens.push(host);
@@ -3777,6 +3798,7 @@ var require_schemes = __commonJS(function(exports, module) {
3777
3798
  var HEX_PAIR = /^[\da-f]{2}$/iu;
3778
3799
  var MAILTO_DOMAIN_LITERAL = /^\[[\x21-\x5A\x5E-\x7E]*\]$/u;
3779
3800
  var MAILTO_DOMAIN_ERROR = "URI mailto has an invalid recipient domain.";
3801
+ var MAILTO_AUTHORITY_ERROR = "URI mailto must not have an authority component.";
3780
3802
  var HAS_SURROGATE = /[\uD800-\uDFFF]/u;
3781
3803
  function decodeHex(str) {
3782
3804
  if (typeof str !== "string" || str.indexOf("%") === -1) {
@@ -3891,30 +3913,37 @@ var require_schemes = __commonJS(function(exports, module) {
3891
3913
  mailtoComponent.error = mailtoComponent.error || "URI mailto has malformed header fields.";
3892
3914
  continue;
3893
3915
  }
3894
- const name = eqIdx === -1 ? token : token.slice(0, eqIdx);
3916
+ const name = decodeHex(eqIdx === -1 ? token : token.slice(0, eqIdx));
3917
+ const normalizedName = name.toLowerCase();
3895
3918
  const value = eqIdx === -1 ? "" : token.slice(eqIdx + 1);
3896
- if (name === "to") {
3919
+ if (normalizedName === "to") {
3897
3920
  const addrs = value.split(",");
3898
3921
  for (let j = 0;j < addrs.length; j++)
3899
3922
  to.push(addrs[j]);
3900
3923
  continue;
3901
3924
  }
3902
- if (name === "subject") {
3925
+ if (normalizedName === "subject") {
3903
3926
  mailtoComponent.subject = decodeHex(value);
3904
3927
  continue;
3905
3928
  }
3906
- if (name === "body") {
3929
+ if (normalizedName === "body") {
3907
3930
  mailtoComponent.body = decodeHex(value);
3908
3931
  continue;
3909
3932
  }
3910
3933
  if (headers === null)
3911
3934
  headers = Object.create(null);
3912
- headers[decodeHex(name)] = decodeHex(value);
3935
+ headers[name] = decodeHex(value);
3913
3936
  }
3914
3937
  if (headers !== null)
3915
3938
  mailtoComponent.headers = headers;
3916
3939
  }
3917
3940
  mailtoComponent.query = undefined;
3941
+ if (to.length > 0 && (mailtoComponent.userinfo !== undefined || mailtoComponent.host !== undefined || mailtoComponent.port !== undefined)) {
3942
+ mailtoComponent.userinfo = undefined;
3943
+ mailtoComponent.host = undefined;
3944
+ mailtoComponent.port = undefined;
3945
+ mailtoComponent.error = mailtoComponent.error || MAILTO_AUTHORITY_ERROR;
3946
+ }
3918
3947
  for (let i = 0;i < to.length; i++) {
3919
3948
  const rawAddr = to[i];
3920
3949
  const atIdx = rawAddr.lastIndexOf("@");
@@ -3931,10 +3960,35 @@ var require_schemes = __commonJS(function(exports, module) {
3931
3960
  mailtoComponent.to = to;
3932
3961
  return mailtoComponent;
3933
3962
  }
3963
+ function mailtoEncodeHeaderName(name) {
3964
+ return encodeWithAllow(name.replace(/%/gu, "%25"), HFNAME);
3965
+ }
3934
3966
  function mailtoSerialize(component, options) {
3935
3967
  const mailtoComponent = component;
3936
3968
  const to = Array.isArray(mailtoComponent.to) ? mailtoComponent.to.slice() : [];
3969
+ const sourceHeaders = mailtoComponent.headers && typeof mailtoComponent.headers === "object" ? Object.assign(Object.create(null), mailtoComponent.headers) : Object.create(null);
3970
+ const headers = Object.create(null);
3971
+ for (const name in sourceHeaders) {
3972
+ const normalizedName = name.toLowerCase();
3973
+ if (normalizedName === "to") {
3974
+ const addrs = String(sourceHeaders[name]).split(",");
3975
+ for (let i = 0;i < addrs.length; i++)
3976
+ to.push(addrs[i]);
3977
+ } else if (normalizedName === "subject" || normalizedName === "body") {
3978
+ headers[normalizedName] = sourceHeaders[name];
3979
+ } else {
3980
+ headers[name] = sourceHeaders[name];
3981
+ }
3982
+ }
3983
+ if (mailtoComponent.subject !== undefined)
3984
+ headers.subject = mailtoComponent.subject;
3985
+ if (mailtoComponent.body !== undefined)
3986
+ headers.body = mailtoComponent.body;
3987
+ mailtoComponent.headers = headers;
3937
3988
  if (to.length) {
3989
+ mailtoComponent.userinfo = undefined;
3990
+ mailtoComponent.host = undefined;
3991
+ mailtoComponent.port = undefined;
3938
3992
  for (let i = 0;i < to.length; i++) {
3939
3993
  const addr = String(to[i]);
3940
3994
  const atIdx = addr.lastIndexOf("@");
@@ -3950,18 +4004,12 @@ var require_schemes = __commonJS(function(exports, module) {
3950
4004
  } else {
3951
4005
  mailtoComponent.path = undefined;
3952
4006
  }
3953
- const headers = mailtoComponent.headers && typeof mailtoComponent.headers === "object" ? Object.assign(Object.create(null), mailtoComponent.headers) : Object.create(null);
3954
- if (mailtoComponent.subject)
3955
- headers.subject = mailtoComponent.subject;
3956
- if (mailtoComponent.body)
3957
- headers.body = mailtoComponent.body;
3958
- mailtoComponent.headers = headers;
3959
4007
  let query = "";
3960
4008
  let count = 0;
3961
4009
  for (const name in headers) {
3962
4010
  if (count++ !== 0)
3963
4011
  query += "&";
3964
- query += encodeWithAllow(name, HFNAME) + "=" + encodeWithAllow(String(headers[name]), HFNAME);
4012
+ query += mailtoEncodeHeaderName(name) + "=" + encodeWithAllow(String(headers[name]), HFNAME);
3965
4013
  }
3966
4014
  if (count !== 0) {
3967
4015
  mailtoComponent.query = query;
@@ -4054,6 +4102,12 @@ var require_fast_uri = __commonJS(function(exports, module) {
4054
4102
  schemelessOptions.skipEscape = true;
4055
4103
  return serialize(resolved, schemelessOptions);
4056
4104
  }
4105
+ function copyHost(target, source) {
4106
+ target.host = source.host;
4107
+ if (source.ipv6Zone !== undefined) {
4108
+ target.ipv6Zone = source.ipv6Zone;
4109
+ }
4110
+ }
4057
4111
  function resolveComponent(base, relative, options, skipNormalization) {
4058
4112
  const target = {};
4059
4113
  if (!skipNormalization) {
@@ -4064,14 +4118,14 @@ var require_fast_uri = __commonJS(function(exports, module) {
4064
4118
  if (!options.tolerant && relative.scheme) {
4065
4119
  target.scheme = relative.scheme;
4066
4120
  target.userinfo = relative.userinfo;
4067
- target.host = relative.host;
4121
+ copyHost(target, relative);
4068
4122
  target.port = relative.port;
4069
4123
  target.path = removeDotSegments(relative.path || "");
4070
4124
  target.query = relative.query;
4071
4125
  } else {
4072
4126
  if (relative.userinfo !== undefined || relative.host !== undefined || relative.port !== undefined) {
4073
4127
  target.userinfo = relative.userinfo;
4074
- target.host = relative.host;
4128
+ copyHost(target, relative);
4075
4129
  target.port = relative.port;
4076
4130
  target.path = removeDotSegments(relative.path || "");
4077
4131
  target.query = relative.query;
@@ -4099,7 +4153,7 @@ var require_fast_uri = __commonJS(function(exports, module) {
4099
4153
  target.query = relative.query;
4100
4154
  }
4101
4155
  target.userinfo = base.userinfo;
4102
- target.host = base.host;
4156
+ copyHost(target, base);
4103
4157
  target.port = base.port;
4104
4158
  }
4105
4159
  target.scheme = base.scheme;
@@ -4115,6 +4169,7 @@ var require_fast_uri = __commonJS(function(exports, module) {
4115
4169
  function serialize(cmpts, opts) {
4116
4170
  const component = {
4117
4171
  host: cmpts.host,
4172
+ ipv6Zone: cmpts.ipv6Zone,
4118
4173
  scheme: cmpts.scheme,
4119
4174
  userinfo: cmpts.userinfo,
4120
4175
  port: cmpts.port,
@@ -4311,6 +4366,9 @@ var require_fast_uri = __commonJS(function(exports, module) {
4311
4366
  isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true;
4312
4367
  malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
4313
4368
  parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
4369
+ if (isIP && ipv6result.isIPV6 === true && parsed.host.indexOf("%") !== -1) {
4370
+ parsed.ipv6Zone = parsed.host.slice(parsed.host.indexOf("%") + 1);
4371
+ }
4314
4372
  if (malformedIPLiteral) {
4315
4373
  parsed.error = parsed.error || "URI host is malformed.";
4316
4374
  malformedAuthorityOrPort = true;
@@ -4335,13 +4393,14 @@ var require_fast_uri = __commonJS(function(exports, module) {
4335
4393
  if (!malformedIPLiteral) {
4336
4394
  malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
4337
4395
  }
4338
- if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4339
- if (uri.indexOf("%") !== -1) {
4340
- if (parsed.host !== undefined && !malformedIPLiteral) {
4341
- const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4342
- parsed.host = reescapeHostDelimiters(host, isIP);
4343
- }
4396
+ if (uri.indexOf("%") !== -1 && parsed.host !== undefined && !malformedIPLiteral) {
4397
+ let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4398
+ if (!isIP) {
4399
+ host = normalizePercentEncoding(host.toLowerCase());
4344
4400
  }
4401
+ parsed.host = reescapeHostDelimiters(host, isIP);
4402
+ }
4403
+ if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4345
4404
  if (parsed.path) {
4346
4405
  parsed.path = normalizePathEncoding(parsed.path);
4347
4406
  }
@@ -12663,11 +12722,14 @@ var require_dist = __commonJS(function(exports, module) {
12663
12722
  });
12664
12723
 
12665
12724
  // src/utils/emoji.ts
12725
+ function withoutSkinTone(emoji) {
12726
+ return emoji.replace(/::skin-tone-\d$/, "").replace(/(?<=.)_(?:light|medium_light|medium|medium_dark|dark)_skin_tone$/, "");
12727
+ }
12666
12728
  function isApprovalEmoji(emoji) {
12667
- return APPROVAL_EMOJIS.includes(emoji);
12729
+ return APPROVAL_EMOJIS.includes(withoutSkinTone(emoji));
12668
12730
  }
12669
12731
  function isAllowAllEmoji(emoji) {
12670
- return ALLOW_ALL_EMOJIS.includes(emoji);
12732
+ return ALLOW_ALL_EMOJIS.includes(withoutSkinTone(emoji));
12671
12733
  }
12672
12734
  var APPROVAL_EMOJIS, DENIAL_EMOJIS, ALLOW_ALL_EMOJIS;
12673
12735
  var init_emoji = __esm(() => {
@@ -17234,11 +17296,23 @@ var require_websocket = __commonJS(function(exports, module) {
17234
17296
  this._isServer = false;
17235
17297
  this._redirects = 0;
17236
17298
  if (protocols === undefined) {
17237
- protocols = [];
17299
+ if (!options || options.protocols === undefined) {
17300
+ protocols = [];
17301
+ } else if (Array.isArray(options.protocols)) {
17302
+ protocols = options.protocols;
17303
+ } else {
17304
+ protocols = [options.protocols];
17305
+ }
17238
17306
  } else if (!Array.isArray(protocols)) {
17239
17307
  if (typeof protocols === "object" && protocols !== null) {
17240
17308
  options = protocols;
17241
- protocols = [];
17309
+ if (options.protocols === undefined) {
17310
+ protocols = [];
17311
+ } else if (Array.isArray(options.protocols)) {
17312
+ protocols = options.protocols;
17313
+ } else {
17314
+ protocols = [options.protocols];
17315
+ }
17242
17316
  } else {
17243
17317
  protocols = [protocols];
17244
17318
  }
@@ -17357,7 +17431,6 @@ var require_websocket = __commonJS(function(exports, module) {
17357
17431
  }
17358
17432
  return;
17359
17433
  }
17360
- this._readyState = WebSocket.CLOSING;
17361
17434
  this._sender.close(code, data, !this._isServer, (err) => {
17362
17435
  if (err)
17363
17436
  return;
@@ -17366,6 +17439,7 @@ var require_websocket = __commonJS(function(exports, module) {
17366
17439
  this._socket.end();
17367
17440
  }
17368
17441
  });
17442
+ this._readyState = WebSocket.CLOSING;
17369
17443
  setCloseTimer(this);
17370
17444
  }
17371
17445
  pause() {
@@ -17553,6 +17627,7 @@ var require_websocket = __commonJS(function(exports, module) {
17553
17627
  socketPath: undefined,
17554
17628
  hostname: undefined,
17555
17629
  protocol: undefined,
17630
+ protocols: undefined,
17556
17631
  timeout: undefined,
17557
17632
  method: "GET",
17558
17633
  host: undefined,
@@ -40251,16 +40326,49 @@ async function uploadFileSlack(args) {
40251
40326
 
40252
40327
  // src/platform/slack/mcp-platform-api.ts
40253
40328
  var SLACK_API_BASE = "https://slack.com/api";
40254
- async function slackApi(method, token, body) {
40255
- const url = `${SLACK_API_BASE}/${method}`;
40256
- const response = await fetch(url, {
40257
- method: "POST",
40258
- headers: {
40259
- Authorization: `Bearer ${token}`,
40260
- "Content-Type": "application/json; charset=utf-8"
40261
- },
40262
- body: body ? JSON.stringify(body) : undefined
40263
- });
40329
+ var READ_METHODS = new Set([
40330
+ "users.info",
40331
+ "conversations.history",
40332
+ "conversations.replies",
40333
+ "conversations.info",
40334
+ "conversations.members",
40335
+ "reactions.get"
40336
+ ]);
40337
+
40338
+ class SlackRateLimitError extends Error {
40339
+ retryAfterMs;
40340
+ constructor(retryAfterMs) {
40341
+ super(`Slack API error: 429 rate limited, retry after ${retryAfterMs} ms`);
40342
+ this.retryAfterMs = retryAfterMs;
40343
+ }
40344
+ }
40345
+ async function slackApi(method, token, args, apiBase = SLACK_API_BASE) {
40346
+ let response;
40347
+ if (READ_METHODS.has(method)) {
40348
+ const query = new URLSearchParams;
40349
+ for (const [key, value] of Object.entries(args ?? {})) {
40350
+ if (value !== undefined && value !== null)
40351
+ query.set(key, String(value));
40352
+ }
40353
+ const qs = query.toString();
40354
+ response = await fetch(`${apiBase}/${method}${qs ? `?${qs}` : ""}`, {
40355
+ method: "GET",
40356
+ headers: { Authorization: `Bearer ${token}` }
40357
+ });
40358
+ } else {
40359
+ response = await fetch(`${apiBase}/${method}`, {
40360
+ method: "POST",
40361
+ headers: {
40362
+ Authorization: `Bearer ${token}`,
40363
+ "Content-Type": "application/json; charset=utf-8"
40364
+ },
40365
+ body: args ? JSON.stringify(args) : undefined
40366
+ });
40367
+ }
40368
+ if (response.status === 429) {
40369
+ const seconds = Number(response.headers.get("retry-after"));
40370
+ throw new SlackRateLimitError(Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : 5000);
40371
+ }
40264
40372
  if (!response.ok) {
40265
40373
  throw new Error(`Slack API error: ${response.status} ${response.statusText}`);
40266
40374
  }
@@ -40272,6 +40380,9 @@ async function slackApi(method, token, body) {
40272
40380
  }
40273
40381
 
40274
40382
  class SlackMcpPlatformApi {
40383
+ slackApi(method, token, args) {
40384
+ return slackApi(method, token, args, this.config.apiUrl || SLACK_API_BASE);
40385
+ }
40275
40386
  config;
40276
40387
  formatter = new SlackFormatter;
40277
40388
  botUserIdCache = null;
@@ -40287,7 +40398,7 @@ class SlackMcpPlatformApi {
40287
40398
  return this.botUserIdCache;
40288
40399
  }
40289
40400
  mcpLogger.debug("Fetching bot user ID via auth.test...");
40290
- const response = await slackApi("auth.test", this.config.botToken);
40401
+ const response = await this.slackApi("auth.test", this.config.botToken);
40291
40402
  this.botUserIdCache = response.user_id;
40292
40403
  mcpLogger.debug(`Bot user ID: ${response.user_id}`);
40293
40404
  return response.user_id;
@@ -40295,7 +40406,7 @@ class SlackMcpPlatformApi {
40295
40406
  async getUsername(userId) {
40296
40407
  try {
40297
40408
  mcpLogger.debug(`Looking up username for user ${userId}`);
40298
- const response = await slackApi("users.info", this.config.botToken, { user: userId });
40409
+ const response = await this.slackApi("users.info", this.config.botToken, { user: userId });
40299
40410
  const username = response.user?.name;
40300
40411
  if (username) {
40301
40412
  mcpLogger.debug(`User ${userId} is @${username}`);
@@ -40317,7 +40428,7 @@ class SlackMcpPlatformApi {
40317
40428
  }
40318
40429
  async createInteractivePost(message, reactions, threadTs) {
40319
40430
  mcpLogger.debug(`Creating interactive post with ${reactions.length} reaction options`);
40320
- const response = await slackApi("chat.postMessage", this.config.botToken, {
40431
+ const response = await this.slackApi("chat.postMessage", this.config.botToken, {
40321
40432
  channel: this.config.channelId,
40322
40433
  text: message,
40323
40434
  thread_ts: resolvePostThreadId(threadTs || this.config.threadTs),
@@ -40328,7 +40439,7 @@ class SlackMcpPlatformApi {
40328
40439
  for (const emoji of reactions) {
40329
40440
  try {
40330
40441
  const emojiName = getEmojiName(emoji);
40331
- await slackApi("reactions.add", this.config.botToken, {
40442
+ await this.slackApi("reactions.add", this.config.botToken, {
40332
40443
  channel: this.config.channelId,
40333
40444
  timestamp: messageTs,
40334
40445
  name: emojiName
@@ -40342,120 +40453,54 @@ class SlackMcpPlatformApi {
40342
40453
  }
40343
40454
  async updatePost(postId, message) {
40344
40455
  mcpLogger.debug(`Updating post ${postId}`);
40345
- await slackApi("chat.update", this.config.botToken, {
40456
+ await this.slackApi("chat.update", this.config.botToken, {
40346
40457
  channel: this.config.channelId,
40347
40458
  ts: postId,
40348
40459
  text: message,
40349
40460
  mrkdwn: true
40350
40461
  });
40351
40462
  }
40463
+ reactionPollMs = 2000;
40464
+ reactionSlowAfterMs = 30000;
40465
+ reactionSlowPollMs = 5000;
40466
+ seenTtlMs = 30000;
40467
+ seenReactions = new Map;
40352
40468
  async waitForReaction(postId, botUserId, timeoutMs) {
40353
- return new Promise((resolve) => {
40354
- let resolved = false;
40355
- let ws = null;
40356
- const cleanup = () => {
40357
- if (ws && ws.readyState === WS.OPEN) {
40358
- ws.close();
40359
- }
40360
- };
40361
- const timeout = setTimeout(() => {
40362
- if (!resolved) {
40363
- mcpLogger.debug(`Reaction wait timed out after ${timeoutMs}ms`);
40364
- resolved = true;
40365
- cleanup();
40366
- resolve(null);
40367
- }
40368
- }, timeoutMs);
40369
- this.getSocketModeUrl().then((wsUrl) => {
40370
- if (resolved)
40371
- return;
40372
- mcpLogger.debug(`Connecting to Socket Mode: ${wsUrl.substring(0, 50)}...`);
40373
- ws = new WS(wsUrl);
40374
- ws.onopen = () => {
40375
- mcpLogger.debug("Socket Mode WebSocket connected");
40376
- };
40377
- ws.onmessage = (event) => {
40378
- if (resolved)
40379
- return;
40380
- try {
40381
- const data = typeof event.data === "string" ? event.data : event.data.toString();
40382
- const socketEvent = JSON.parse(data);
40383
- if (socketEvent.envelope_id) {
40384
- ws?.send(JSON.stringify({ envelope_id: socketEvent.envelope_id }));
40385
- }
40386
- mcpLogger.debug(`Socket Mode event type: ${socketEvent.type}`);
40387
- if (socketEvent.type === "events_api" && socketEvent.payload?.event) {
40388
- const slackEvent = socketEvent.payload.event;
40389
- if (slackEvent.type === "reaction_added") {
40390
- const item = slackEvent.item;
40391
- if (item?.type !== "message" || item.ts !== postId || item.channel !== this.config.channelId) {
40392
- return;
40393
- }
40394
- if (slackEvent.user === botUserId) {
40395
- mcpLogger.debug("Ignoring bot's own reaction");
40396
- return;
40397
- }
40398
- const emojiName = slackEvent.reaction || "";
40399
- const userId = slackEvent.user || "";
40400
- mcpLogger.debug(`Reaction received: :${emojiName}: from user: ${userId}`);
40401
- resolved = true;
40402
- clearTimeout(timeout);
40403
- cleanup();
40404
- resolve({
40405
- postId: item.ts,
40406
- userId,
40407
- emojiName
40408
- });
40409
- }
40410
- }
40411
- if (socketEvent.type === "hello") {
40412
- mcpLogger.debug("Socket Mode hello received, connection established");
40413
- }
40414
- if (socketEvent.type === "disconnect") {
40415
- mcpLogger.debug("Socket Mode disconnect requested");
40416
- if (!resolved) {
40417
- resolved = true;
40418
- clearTimeout(timeout);
40419
- cleanup();
40420
- resolve(null);
40421
- }
40422
- }
40423
- } catch (err) {
40424
- mcpLogger.debug(`Error parsing Socket Mode message: ${err}`);
40425
- }
40426
- };
40427
- ws.onerror = (event) => {
40428
- mcpLogger.error(`Socket Mode WebSocket error: ${formatWebSocketError(event)}`);
40429
- if (!resolved) {
40430
- resolved = true;
40431
- clearTimeout(timeout);
40432
- cleanup();
40433
- resolve(null);
40434
- }
40435
- };
40436
- ws.onclose = () => {
40437
- mcpLogger.debug("Socket Mode WebSocket closed");
40438
- if (!resolved) {
40439
- resolved = true;
40440
- clearTimeout(timeout);
40441
- resolve(null);
40469
+ const started = Date.now();
40470
+ const deadline = started + timeoutMs;
40471
+ let warned = false;
40472
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Math.min(ms, deadline - Date.now()))));
40473
+ while (Date.now() < deadline) {
40474
+ let wait = Date.now() - started >= this.reactionSlowAfterMs ? this.reactionSlowPollMs : this.reactionPollMs;
40475
+ try {
40476
+ const response = await this.slackApi("reactions.get", this.config.botToken, { channel: this.config.channelId, timestamp: postId, full: true });
40477
+ for (const reaction of response.message?.reactions ?? []) {
40478
+ for (const userId of reaction.users ?? []) {
40479
+ if (userId === botUserId)
40480
+ continue;
40481
+ const key = `${postId}|${userId}|${reaction.name}`;
40482
+ const seenAt = this.seenReactions.get(key);
40483
+ if (seenAt !== undefined && Date.now() - seenAt < this.seenTtlMs)
40484
+ continue;
40485
+ this.seenReactions.set(key, Date.now());
40486
+ mcpLogger.debug(`Reaction received: :${reaction.name}: from user: ${userId}`);
40487
+ return { postId, userId, emojiName: reaction.name };
40442
40488
  }
40443
- };
40444
- }).catch((err) => {
40445
- mcpLogger.error(`Failed to get Socket Mode URL: ${err}`);
40446
- if (!resolved) {
40447
- resolved = true;
40448
- clearTimeout(timeout);
40449
- resolve(null);
40450
40489
  }
40451
- });
40452
- });
40453
- }
40454
- async getSocketModeUrl() {
40455
- mcpLogger.debug("Getting Socket Mode WebSocket URL...");
40456
- const response = await slackApi("apps.connections.open", this.config.appToken);
40457
- mcpLogger.debug("Got Socket Mode URL");
40458
- return response.url;
40490
+ } catch (err) {
40491
+ if (err instanceof SlackRateLimitError) {
40492
+ wait = err.retryAfterMs;
40493
+ } else if (!warned) {
40494
+ warned = true;
40495
+ mcpLogger.warn(`reactions.get failed, will keep retrying: ${err}`);
40496
+ } else {
40497
+ mcpLogger.debug(`reactions.get failed, retrying: ${err}`);
40498
+ }
40499
+ }
40500
+ await sleep(wait);
40501
+ }
40502
+ mcpLogger.debug(`Reaction wait timed out after ${timeoutMs}ms`);
40503
+ return null;
40459
40504
  }
40460
40505
  async uploadFile(filePath, threadId, options) {
40461
40506
  const filename = sanitizeFilename(options?.filename ?? filePath);
@@ -40466,14 +40511,15 @@ class SlackMcpPlatformApi {
40466
40511
  threadTs: threadId,
40467
40512
  filePath,
40468
40513
  filename,
40469
- caption: options?.caption
40514
+ caption: options?.caption,
40515
+ apiUrl: this.config.apiUrl
40470
40516
  });
40471
40517
  return { postId: result.postId };
40472
40518
  }
40473
40519
  async readPost(postId) {
40474
40520
  mcpLogger.debug(`readPost: ts ${postId}`);
40475
40521
  try {
40476
- const response = await slackApi("conversations.history", this.config.botToken, {
40522
+ const response = await this.slackApi("conversations.history", this.config.botToken, {
40477
40523
  channel: this.config.channelId,
40478
40524
  latest: postId,
40479
40525
  oldest: postId,
@@ -40493,7 +40539,7 @@ class SlackMcpPlatformApi {
40493
40539
  async addReaction(postId, emojiName) {
40494
40540
  const name = getEmojiName(emojiName);
40495
40541
  mcpLogger.debug(`addReaction: :${name}: on ts ${postId}`);
40496
- await slackApi("reactions.add", this.config.botToken, {
40542
+ await this.slackApi("reactions.add", this.config.botToken, {
40497
40543
  channel: this.config.channelId,
40498
40544
  timestamp: postId,
40499
40545
  name
@@ -40502,7 +40548,7 @@ class SlackMcpPlatformApi {
40502
40548
  async readThread(threadRootId, options) {
40503
40549
  mcpLogger.debug(`readThread: ts ${threadRootId}`);
40504
40550
  try {
40505
- const response = await slackApi("conversations.replies", this.config.botToken, {
40551
+ const response = await this.slackApi("conversations.replies", this.config.botToken, {
40506
40552
  channel: this.config.channelId,
40507
40553
  ts: threadRootId,
40508
40554
  limit: options?.limit ?? 100
@@ -40525,7 +40571,7 @@ class SlackMcpPlatformApi {
40525
40571
  const limit = options?.limit ?? 20;
40526
40572
  mcpLogger.debug(`readChannelHistory: ${channelId} (limit=${limit})`);
40527
40573
  try {
40528
- const response = await slackApi("conversations.history", this.config.botToken, {
40574
+ const response = await this.slackApi("conversations.history", this.config.botToken, {
40529
40575
  channel: channelId,
40530
40576
  limit
40531
40577
  });
@@ -40545,7 +40591,7 @@ class SlackMcpPlatformApi {
40545
40591
  async getChannelInfo(channelId) {
40546
40592
  mcpLogger.debug(`getChannelInfo: ${channelId}`);
40547
40593
  try {
40548
- const response = await slackApi("conversations.info", this.config.botToken, { channel: channelId });
40594
+ const response = await this.slackApi("conversations.info", this.config.botToken, { channel: channelId });
40549
40595
  const ch = response.channel;
40550
40596
  const isPrivate = ch.is_private || ch.is_im || ch.is_mpim || false;
40551
40597
  return {
@@ -40570,7 +40616,7 @@ class SlackMcpPlatformApi {
40570
40616
  };
40571
40617
  if (cursor)
40572
40618
  params.cursor = cursor;
40573
- const response = await slackApi("conversations.members", this.config.botToken, params);
40619
+ const response = await this.slackApi("conversations.members", this.config.botToken, params);
40574
40620
  all.push(...response.members ?? []);
40575
40621
  cursor = response.response_metadata?.next_cursor;
40576
40622
  if (!cursor)
@@ -40589,7 +40635,7 @@ class SlackMcpPlatformApi {
40589
40635
  return null;
40590
40636
  }
40591
40637
  try {
40592
- const response = await slackApi("users.info", this.config.botToken, { user: id });
40638
+ const response = await this.slackApi("users.info", this.config.botToken, { user: id });
40593
40639
  return { id: response.user.id, username: response.user.name ?? null };
40594
40640
  } catch (err) {
40595
40641
  mcpLogger.debug(`resolveRecipient ${id} failed: ${err}`);
@@ -40597,9 +40643,9 @@ class SlackMcpPlatformApi {
40597
40643
  }
40598
40644
  }
40599
40645
  async sendDirectMessage(recipientUserId, message) {
40600
- const opened = await slackApi("conversations.open", this.config.botToken, { users: recipientUserId });
40646
+ const opened = await this.slackApi("conversations.open", this.config.botToken, { users: recipientUserId });
40601
40647
  const dmChannelId = opened.channel.id;
40602
- const post = await slackApi("chat.postMessage", this.config.botToken, {
40648
+ const post = await this.slackApi("chat.postMessage", this.config.botToken, {
40603
40649
  channel: dmChannelId,
40604
40650
  text: message,
40605
40651
  mrkdwn: true
@@ -41046,7 +41092,8 @@ var SKIP_STANDARD_PERMISSION_PROMPT = new Set([
41046
41092
  var apiConfig = PLATFORM_TYPE === "slack" ? {
41047
41093
  platformType: "slack",
41048
41094
  botToken: PLATFORM_TOKEN,
41049
- appToken: process.env.PLATFORM_APP_TOKEN || "",
41095
+ appToken: "",
41096
+ apiUrl: process.env.PLATFORM_API_URL || undefined,
41050
41097
  channelId: PLATFORM_CHANNEL_ID,
41051
41098
  threadTs: PLATFORM_THREAD_ID || undefined,
41052
41099
  allowedUsers: ALLOWED_USERS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.39.1",
3
+ "version": "1.39.3",
4
4
  "description": "Run Claude Code from Slack or Mattermost. Sessions stream live into threads where your whole team can watch and steer.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  "test:integration:slack:setup": "bun run tests/integration/fixtures/slack/mock-server.ts &",
25
25
  "test:integration:slack:run": "TEST_PLATFORMS=slack INTEGRATION_TEST=1 bun test tests/integration/suites --timeout 120000",
26
26
  "test:integration:slack:teardown": "pkill -f 'mock-server.ts' || true",
27
- "test:integration:slack": "bun run test:integration:slack:teardown; bun run tests/integration/fixtures/slack/mock-server.ts & sleep 2 && TEST_PLATFORMS=slack INTEGRATION_TEST=1 bun test tests/integration/suites --timeout 120000; bun run test:integration:slack:teardown",
27
+ "test:integration:slack": "bun run test:integration:slack:teardown; bun run tests/integration/fixtures/slack/mock-server.ts & sleep 2 && TEST_PLATFORMS=slack INTEGRATION_TEST=1 bun test tests/integration/suites --timeout 120000; status=$?; bun run test:integration:slack:teardown; exit $status",
28
28
  "test:integration:all": "bun run test:integration:setup && bun run tests/integration/fixtures/slack/mock-server.ts & sleep 2 && TEST_PLATFORMS=mattermost,slack INTEGRATION_TEST=1 bun test tests/integration/suites --timeout 120000; bun run test:integration:slack:teardown",
29
29
  "lint": "eslint src/",
30
30
  "lint:fix": "eslint src/ --fix",
@@ -78,7 +78,7 @@
78
78
  "cross-spawn": "^7.0.6",
79
79
  "diff": "^8.0.3",
80
80
  "express-rate-limit": "^8.3.0",
81
- "hono": "4.13.8",
81
+ "hono": "4.13.9",
82
82
  "ink": "^6.6.0",
83
83
  "ink-scroll-view": "^0.3.5",
84
84
  "js-yaml": "^4.3.1",
@@ -86,7 +86,7 @@
86
86
  "react": "^19.2.3",
87
87
  "semver": "7.8.5",
88
88
  "update-notifier": "^7.3.1",
89
- "ws": "8.21.3",
89
+ "ws": "8.22.0",
90
90
  "zod": "^4.3.6"
91
91
  },
92
92
  "devDependencies": {
@@ -102,7 +102,7 @@
102
102
  "@types/ws": "^8.18.0",
103
103
  "eslint": "^10.7.0",
104
104
  "husky": "^9.1.7",
105
- "knip": "6.37.0",
105
+ "knip": "6.38.0",
106
106
  "lint-staged": "^17.0.4",
107
107
  "prettier": "^3.9.5",
108
108
  "react-devtools-core": "^8.0.0",
@@ -126,7 +126,7 @@
126
126
  "flatted": ">=3.4.0",
127
127
  "picomatch": ">=2.3.2",
128
128
  "path-to-regexp": ">=8.4.0",
129
- "fast-uri": ">=4.1.4",
129
+ "fast-uri": ">=4.2.1",
130
130
  "brace-expansion": ">=5.0.9",
131
131
  "ip-address": ">=10.3.1",
132
132
  "ws": "$ws",
@@ -139,7 +139,7 @@
139
139
  "flatted": ">=3.4.0",
140
140
  "picomatch": ">=2.3.2",
141
141
  "path-to-regexp": ">=8.4.0",
142
- "fast-uri": ">=4.1.4",
142
+ "fast-uri": ">=4.2.1",
143
143
  "brace-expansion": ">=5.0.9",
144
144
  "ip-address": ">=10.3.1",
145
145
  "ws": "$ws",