claude-threads 0.45.0 → 0.46.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/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.46.0] - 2026-01-07
11
+
12
+ ### Added
13
+ - **Emoji reactions for `!update` command** - React with 👍 to update immediately or 👎 to defer for 1 hour, easier than typing commands on mobile
14
+
15
+ ### Fixed
16
+ - **Auto-update uses bun instead of npm** - Fixed updates to use `bun install -g` matching the actual install location
17
+ - **ESLint warnings resolved** - Fixed 8 non-null assertion warnings with proper null checks
18
+ - **Dead code removed** - Removed unused Discord formatter/types and other dead code via Knip
19
+
20
+ ### Changed
21
+ - **Knip added to CI and pre-commit** - Dead code detection now runs automatically
22
+
10
23
  ## [0.45.0] - 2026-01-07
11
24
 
12
25
  ### Added
package/dist/index.js CHANGED
@@ -45243,6 +45243,7 @@ async function flush(session, registerPost) {
45243
45243
  const { maxLength: MAX_POST_LENGTH, hardThreshold: HARD_CONTINUATION_THRESHOLD } = session.platform.getMessageLimits();
45244
45244
  const shouldBreakEarly = session.currentPostId && content.length > MIN_BREAK_THRESHOLD && shouldFlushEarly(content);
45245
45245
  if (session.currentPostId && (content.length > HARD_CONTINUATION_THRESHOLD || shouldBreakEarly)) {
45246
+ const currentPostId = session.currentPostId;
45246
45247
  let breakPoint;
45247
45248
  let codeBlockLanguage;
45248
45249
  if (content.length > HARD_CONTINUATION_THRESHOLD) {
@@ -45275,7 +45276,7 @@ async function flush(session, registerPost) {
45275
45276
  breakPoint = breakInfo.position;
45276
45277
  } else {
45277
45278
  try {
45278
- await session.platform.updatePost(session.currentPostId, content);
45279
+ await session.platform.updatePost(currentPostId, content);
45279
45280
  } catch {
45280
45281
  sessionLog(session).debug("Update failed (no breakpoint), will create new post on next flush");
45281
45282
  session.currentPostId = null;
@@ -45295,7 +45296,7 @@ async function flush(session, registerPost) {
45295
45296
 
45296
45297
  ` + formatter2.formatItalic("... (continued below)") : firstPart;
45297
45298
  try {
45298
- await session.platform.updatePost(session.currentPostId, firstPartWithMarker);
45299
+ await session.platform.updatePost(currentPostId, firstPartWithMarker);
45299
45300
  } catch {
45300
45301
  sessionLog(session).debug("Update failed during split, continuing with new post");
45301
45302
  }
@@ -45329,8 +45330,9 @@ async function flush(session, registerPost) {
45329
45330
  ` + formatter2.formatItalic("... (truncated)");
45330
45331
  }
45331
45332
  if (session.currentPostId) {
45333
+ const postId = session.currentPostId;
45332
45334
  try {
45333
- await session.platform.updatePost(session.currentPostId, content);
45335
+ await session.platform.updatePost(postId, content);
45334
45336
  } catch {
45335
45337
  sessionLog(session).debug("Update failed, will create new post on next flush");
45336
45338
  session.currentPostId = null;
@@ -46581,7 +46583,9 @@ async function handleTodoWrite(session, input, ctx) {
46581
46583
  await session.platform.pinPost(post.id).catch(() => {});
46582
46584
  }
46583
46585
  } finally {
46584
- resolveCreation();
46586
+ if (resolveCreation) {
46587
+ resolveCreation();
46588
+ }
46585
46589
  session.taskListCreationPromise = undefined;
46586
46590
  }
46587
46591
  }
@@ -46633,7 +46637,8 @@ async function handleCompactionComplete(session, compactMetadata, _ctx) {
46633
46637
  const formatter = session.platform.getFormatter();
46634
46638
  const completionMessage = `\u2705 ${formatter.formatBold("Context compacted")} ${formatter.formatItalic(`(${info})`)}`;
46635
46639
  if (session.compactionPostId) {
46636
- await withErrorHandling(() => session.platform.updatePost(session.compactionPostId, completionMessage), { action: "Update compaction complete", session });
46640
+ const postId = session.compactionPostId;
46641
+ await withErrorHandling(() => session.platform.updatePost(postId, completionMessage), { action: "Update compaction complete", session });
46637
46642
  session.compactionPostId = undefined;
46638
46643
  } else {
46639
46644
  const post = await withErrorHandling(() => session.platform.createPost(completionMessage, session.threadId), { action: "Post compaction complete", session });
@@ -46949,6 +46954,37 @@ async function handleExistingWorktreeReaction(session, postId, emojiName, userna
46949
46954
  }
46950
46955
  return true;
46951
46956
  }
46957
+ async function handleUpdateReaction(session, postId, emojiName, username, ctx, updateHandler) {
46958
+ const pending = session.pendingUpdatePrompt;
46959
+ if (!pending || pending.postId !== postId) {
46960
+ return false;
46961
+ }
46962
+ if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
46963
+ return false;
46964
+ }
46965
+ const isUpdateNow = isApprovalEmoji(emojiName);
46966
+ const isDefer = isDenialEmoji(emojiName);
46967
+ if (!isUpdateNow && !isDefer) {
46968
+ return false;
46969
+ }
46970
+ const formatter = session.platform.getFormatter();
46971
+ if (isUpdateNow) {
46972
+ await withErrorHandling(() => session.platform.updatePost(pending.postId, `\uD83D\uDD04 ${formatter.formatBold("Forcing update")} - restarting shortly...
46973
+ ` + formatter.formatItalic("Sessions will resume automatically")), { action: "Update post for force update", session });
46974
+ session.pendingUpdatePrompt = undefined;
46975
+ ctx.ops.persistSession(session);
46976
+ sessionLog3(session).info(`\uD83D\uDD04 @${username} triggered immediate update`);
46977
+ await updateHandler.forceUpdate();
46978
+ } else {
46979
+ updateHandler.deferUpdate(60);
46980
+ await withErrorHandling(() => session.platform.updatePost(pending.postId, `\u23F8\uFE0F ${formatter.formatBold("Update deferred")} for 1 hour
46981
+ ` + formatter.formatItalic("Use !update now to apply earlier")), { action: "Update post for defer update", session });
46982
+ session.pendingUpdatePrompt = undefined;
46983
+ ctx.ops.persistSession(session);
46984
+ sessionLog3(session).info(`\u23F8\uFE0F @${username} deferred update for 1 hour`);
46985
+ }
46986
+ return true;
46987
+ }
46952
46988
 
46953
46989
  // src/claude/cli.ts
46954
46990
  import { spawn as spawn2 } from "child_process";
@@ -50700,7 +50736,7 @@ function checkForUpdates() {
50700
50736
  cachedUpdateInfo = notifier.update;
50701
50737
  notifier.notify({
50702
50738
  message: `Update available: {currentVersion} \u2192 {latestVersion}
50703
- Run: npm install -g claude-threads`
50739
+ Run: bun install -g claude-threads`
50704
50740
  });
50705
50741
  } catch {}
50706
50742
  }
@@ -51144,7 +51180,7 @@ function validateClaudeCli() {
51144
51180
  installed: false,
51145
51181
  version: null,
51146
51182
  compatible: false,
51147
- message: "Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code"
51183
+ message: "Claude CLI not found. Install it with: bun install -g @anthropic-ai/claude-code"
51148
51184
  };
51149
51185
  }
51150
51186
  const compatible = isVersionCompatible(version);
@@ -51154,7 +51190,7 @@ function validateClaudeCli() {
51154
51190
  version,
51155
51191
  compatible: false,
51156
51192
  message: `Claude CLI version ${version} is not compatible. Required: ${CLAUDE_CLI_VERSION_RANGE}
51157
- ` + `Install a compatible version: npm install -g @anthropic-ai/claude-code@2.0.76`
51193
+ ` + `Install a compatible version: bun install -g @anthropic-ai/claude-code@2.0.76`
51158
51194
  };
51159
51195
  }
51160
51196
  return {
@@ -51477,7 +51513,7 @@ async function updateSessionHeader(session, ctx) {
51477
51513
  items.push(["\uD83C\uDD94", "Session ID", formatter.formatCode(session.claudeSessionId.substring(0, 8))]);
51478
51514
  const updateInfo = getUpdateInfo();
51479
51515
  const updateNotice = updateInfo ? `
51480
- > \u26A0\uFE0F ${formatter.formatBold("Update available:")} v${updateInfo.current} \u2192 v${updateInfo.latest} - Run ${formatter.formatCode("npm install -g claude-threads")}
51516
+ > \u26A0\uFE0F ${formatter.formatBold("Update available:")} v${updateInfo.current} \u2192 v${updateInfo.latest} - Run ${formatter.formatCode("bun install -g claude-threads")}
51481
51517
  ` : "";
51482
51518
  const releaseNotes = getReleaseNotes(VERSION);
51483
51519
  const whatsNew = releaseNotes ? getWhatsNewSummary(releaseNotes) : "";
@@ -51520,15 +51556,15 @@ async function showUpdateStatus(session, updateManager) {
51520
51556
  } else {
51521
51557
  statusLine = `Mode: ${config.autoRestartMode}`;
51522
51558
  }
51523
- await postInfo(session, `\uD83D\uDD04 ${formatter.formatBold("Update available")}
51559
+ const message = `\uD83D\uDD04 ${formatter.formatBold("Update available")}
51524
51560
 
51525
51561
  ` + `Current: v${updateInfo.currentVersion}
51526
51562
  ` + `Latest: v${updateInfo.latestVersion}
51527
51563
  ` + `${statusLine}
51528
51564
 
51529
- ` + `Commands:
51530
- ` + `\u2022 ${formatter.formatCode("!update now")} - Update immediately
51531
- ` + `\u2022 ${formatter.formatCode("!update defer")} - Defer for 1 hour`);
51565
+ ` + `React: \uD83D\uDC4D Update now | \uD83D\uDC4E Defer for 1 hour`;
51566
+ const post = await session.platform.createInteractivePost(message, [APPROVAL_EMOJIS[0], DENIAL_EMOJIS[0]], session.threadId);
51567
+ session.pendingUpdatePrompt = { postId: post.id };
51532
51568
  }
51533
51569
  async function forceUpdateNow(session, username, updateManager) {
51534
51570
  if (!await requireSessionOwner(session, username, "force updates")) {
@@ -51929,9 +51965,10 @@ Please start a new session.`, state.threadId), { action: "Post resume failure no
51929
51965
  sessionLog5(session).info(`\uD83D\uDD04 Session resumed (@${state.startedBy})`);
51930
51966
  const sessionFormatter = session.platform.getFormatter();
51931
51967
  if (session.lifecyclePostId) {
51968
+ const postId = session.lifecyclePostId;
51932
51969
  const resumeMsg = `\uD83D\uDD04 ${sessionFormatter.formatBold("Session resumed")} by ${sessionFormatter.formatUserMention(session.startedBy)}
51933
51970
  ${sessionFormatter.formatItalic("Reconnected to Claude session. You can continue where you left off.")}`;
51934
- await withErrorHandling(() => session.platform.updatePost(session.lifecyclePostId, resumeMsg), { action: "Update timeout/shutdown post for resume", session });
51971
+ await withErrorHandling(() => session.platform.updatePost(postId, resumeMsg), { action: "Update timeout/shutdown post for resume", session });
51935
51972
  session.lifecyclePostId = undefined;
51936
51973
  } else {
51937
51974
  const restartMsg = `${sessionFormatter.formatBold("Session resumed")} after bot restart (v${VERSION})
@@ -52163,7 +52200,8 @@ async function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
52163
52200
 
52164
52201
  \uD83D\uDCA1 React with \uD83D\uDD04 to resume, or send a new message to continue.`;
52165
52202
  if (session.lifecyclePostId) {
52166
- await withErrorHandling(() => session.platform.updatePost(session.lifecyclePostId, `\u23F1\uFE0F ${timeoutMessage}`), { action: "Update timeout post", session });
52203
+ const postId = session.lifecyclePostId;
52204
+ await withErrorHandling(() => session.platform.updatePost(postId, `\u23F1\uFE0F ${timeoutMessage}`), { action: "Update timeout post", session });
52167
52205
  } else {
52168
52206
  const timeoutPost = await withErrorHandling(() => postTimeout(session, timeoutMessage), { action: "Post session timeout", session });
52169
52207
  if (timeoutPost) {
@@ -53310,7 +53348,10 @@ class SessionManager extends EventEmitter4 {
53310
53348
  if (!this.worktreeUsers.has(worktreePath)) {
53311
53349
  this.worktreeUsers.set(worktreePath, new Set);
53312
53350
  }
53313
- this.worktreeUsers.get(worktreePath).add(sessionId);
53351
+ const users = this.worktreeUsers.get(worktreePath);
53352
+ if (users) {
53353
+ users.add(sessionId);
53354
+ }
53314
53355
  log18.debug(`Registered session ${sessionId.substring(0, 20)} as worktree user for ${worktreePath}`);
53315
53356
  }
53316
53357
  unregisterWorktreeUser(worktreePath, sessionId) {
@@ -53466,6 +53507,17 @@ class SessionManager extends EventEmitter4 {
53466
53507
  if (handled)
53467
53508
  return;
53468
53509
  }
53510
+ if (action === "added" && session.pendingUpdatePrompt?.postId === postId) {
53511
+ if (this.autoUpdateManager) {
53512
+ const updateHandler = {
53513
+ forceUpdate: () => this.autoUpdateManager.forceUpdate(),
53514
+ deferUpdate: (minutes) => this.autoUpdateManager.deferUpdate(minutes)
53515
+ };
53516
+ const handled = await handleUpdateReaction(session, postId, emojiName, username, this.getContext(), updateHandler);
53517
+ if (handled)
53518
+ return;
53519
+ }
53520
+ }
53469
53521
  if (action === "added" && session.pendingContextPrompt?.postId === postId) {
53470
53522
  await this.handleContextPromptReaction(session, emojiName, username);
53471
53523
  return;
@@ -64480,8 +64532,11 @@ async function installVersion(version) {
64480
64532
  justUpdated: false
64481
64533
  });
64482
64534
  return new Promise((resolve7) => {
64483
- const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
64484
- const child = spawn5(npmCmd, ["install", "-g", `${PACKAGE_NAME3}@${version}`], {
64535
+ const useBun = process.platform !== "win32";
64536
+ const cmd = useBun ? "bun" : "npm.cmd";
64537
+ const args = useBun ? ["install", "-g", `${PACKAGE_NAME3}@${version}`] : ["install", "-g", `${PACKAGE_NAME3}@${version}`];
64538
+ log21.debug(`Using ${useBun ? "bun" : "npm"} for installation`);
64539
+ const child = spawn5(cmd, args, {
64485
64540
  stdio: ["ignore", "pipe", "pipe"],
64486
64541
  env: {
64487
64542
  ...process.env,
@@ -64529,8 +64584,9 @@ async function installVersion(version) {
64529
64584
  });
64530
64585
  }
64531
64586
  function getRollbackInstructions(previousVersion) {
64587
+ const cmd = process.platform === "win32" ? "npm" : "bun";
64532
64588
  return `To rollback to the previous version, run:
64533
- npm install -g ${PACKAGE_NAME3}@${previousVersion}`;
64589
+ ${cmd} install -g ${PACKAGE_NAME3}@${previousVersion}`;
64534
64590
  }
64535
64591
 
64536
64592
  class UpdateInstaller {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "description": "Share Claude Code sessions live in a Mattermost channel with interactive features",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -63,6 +63,7 @@
63
63
  "dependencies": {
64
64
  "@inkjs/ui": "^2.0.0",
65
65
  "@modelcontextprotocol/sdk": "^1.25.1",
66
+ "cli-spinners": "^3.3.0",
66
67
  "commander": "^14.0.2",
67
68
  "diff": "^8.0.2",
68
69
  "ink": "^6.6.0",
@@ -76,7 +77,6 @@
76
77
  "devDependencies": {
77
78
  "@eslint/js": "^9.39.2",
78
79
  "@types/bun": "latest",
79
- "@types/diff": "^8.0.0",
80
80
  "@types/node": "^25.0.3",
81
81
  "@types/prompts": "^2.4.9",
82
82
  "@types/react": "^19.2.7",