claude-threads 1.14.0 → 1.15.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
@@ -51799,6 +51799,26 @@ async function getThreadRaw(config, threadRootId) {
51799
51799
  return null;
51800
51800
  }
51801
51801
  }
51802
+ async function getChannelPostsRaw(config, channelId, perPage) {
51803
+ try {
51804
+ return await mattermostApi(config, "GET", `/channels/${channelId}/posts?per_page=${perPage}`);
51805
+ } catch (err) {
51806
+ apiLog.debug(`Failed to get channel posts ${channelId}: ${err}`);
51807
+ return null;
51808
+ }
51809
+ }
51810
+ async function searchPostsForTeam(config, teamId, terms, perPage) {
51811
+ try {
51812
+ return await mattermostApi(config, "POST", `/teams/${teamId}/posts/search`, {
51813
+ terms,
51814
+ is_or_search: false,
51815
+ per_page: perPage
51816
+ });
51817
+ } catch (err) {
51818
+ apiLog.debug(`Search failed on team ${teamId}: ${err}`);
51819
+ return null;
51820
+ }
51821
+ }
51802
51822
  async function addReaction(config, postId, userId, emojiName) {
51803
51823
  await mattermostApi(config, "POST", "/reactions", {
51804
51824
  user_id: userId,
@@ -51806,6 +51826,22 @@ async function addReaction(config, postId, userId, emojiName) {
51806
51826
  emoji_name: emojiName
51807
51827
  });
51808
51828
  }
51829
+ async function getUserByUsernameRaw(config, username) {
51830
+ try {
51831
+ return await mattermostApi(config, "GET", `/users/username/${encodeURIComponent(username)}`);
51832
+ } catch (err) {
51833
+ apiLog.debug(`Failed to lookup user @${username}: ${err}`);
51834
+ return null;
51835
+ }
51836
+ }
51837
+ async function createDirectChannelRaw(config, userIdA, userIdB) {
51838
+ try {
51839
+ return await mattermostApi(config, "POST", "/channels/direct", [userIdA, userIdB]);
51840
+ } catch (err) {
51841
+ apiLog.debug(`Failed to open direct channel ${userIdA}↔${userIdB}: ${err}`);
51842
+ return null;
51843
+ }
51844
+ }
51809
51845
  function isUserInAllowList(username, allowList) {
51810
51846
  if (allowList.length === 0)
51811
51847
  return true;
@@ -51983,6 +52019,11 @@ class MattermostMcpPlatformApi {
51983
52019
  this.channelTypeCache.set(channelId, visibility);
51984
52020
  return visibility;
51985
52021
  }
52022
+ async addReaction(postId, emojiName) {
52023
+ mcpLogger.debug(`addReaction: :${emojiName}: on post ${formatShortId(postId)}`);
52024
+ const botUserId = await this.getBotUserId();
52025
+ await addReaction(this.apiConfig, postId, botUserId, emojiName);
52026
+ }
51986
52027
  async readThread(threadRootId, options) {
51987
52028
  mcpLogger.debug(`readThread: ${formatShortId(threadRootId)}`);
51988
52029
  const thread = await getThreadRaw(this.apiConfig, threadRootId);
@@ -51990,14 +52031,93 @@ class MattermostMcpPlatformApi {
51990
52031
  return [];
51991
52032
  const ordered = thread.order.map((id) => thread.posts[id]).filter((p) => Boolean(p)).sort((a, b) => (a.create_at ?? 0) - (b.create_at ?? 0));
51992
52033
  const limited = options?.limit !== undefined ? ordered.slice(-options.limit) : ordered;
52034
+ return this.hydratePosts(limited);
52035
+ }
52036
+ async readChannelHistory(channelId, options) {
52037
+ const limit = options?.limit ?? 20;
52038
+ mcpLogger.debug(`readChannelHistory: ${formatShortId(channelId)} (limit=${limit})`);
52039
+ const response = await getChannelPostsRaw(this.apiConfig, channelId, limit);
52040
+ if (!response)
52041
+ return null;
52042
+ const ordered = response.order.map((id) => response.posts[id]).filter((p) => Boolean(p)).sort((a, b) => (a.create_at ?? 0) - (b.create_at ?? 0));
52043
+ return this.hydratePosts(ordered);
52044
+ }
52045
+ async getChannelInfo(channelId) {
52046
+ mcpLogger.debug(`getChannelInfo: ${formatShortId(channelId)}`);
52047
+ const channel = await getChannelRaw(this.apiConfig, channelId);
52048
+ if (!channel)
52049
+ return null;
52050
+ const channelType = channel.type === "O" ? "public" : "private";
52051
+ this.channelTypeCache.set(channelId, channelType);
52052
+ const name = channel.display_name || channel.name;
52053
+ return { id: channel.id, channelType, name };
52054
+ }
52055
+ async getChannelMembers(channelId) {
52056
+ try {
52057
+ const members = await mattermostApi(this.apiConfig, "GET", `/channels/${channelId}/members?per_page=200`);
52058
+ return members.map((m) => m.user_id);
52059
+ } catch (err) {
52060
+ mcpLogger.debug(`getChannelMembers ${channelId} failed: ${err}`);
52061
+ return null;
52062
+ }
52063
+ }
52064
+ async resolveRecipient(recipient) {
52065
+ const normalized = recipient.replace(/^@/, "");
52066
+ const user = await getUserByUsernameRaw(this.apiConfig, normalized);
52067
+ if (!user)
52068
+ return null;
52069
+ return { id: user.id, username: user.username };
52070
+ }
52071
+ async sendDirectMessage(recipientUserId, message) {
52072
+ const botUserId = await this.getBotUserId();
52073
+ const dmChannel = await createDirectChannelRaw(this.apiConfig, botUserId, recipientUserId);
52074
+ if (!dmChannel) {
52075
+ throw new Error("failed to open direct channel with recipient");
52076
+ }
52077
+ const post = await createPost(this.apiConfig, dmChannel.id, message);
52078
+ return { postId: post.id };
52079
+ }
52080
+ async searchMessages(query, options) {
52081
+ const limit = options?.limit ?? 10;
52082
+ mcpLogger.debug(`searchMessages: '${query}' (limit=${limit})`);
52083
+ const teamId = await this.resolveTeamIdForBotChannel();
52084
+ if (!teamId) {
52085
+ mcpLogger.warn("searchMessages: could not resolve a team for the bot channel");
52086
+ return null;
52087
+ }
52088
+ const response = await searchPostsForTeam(this.apiConfig, teamId, query, limit);
52089
+ if (!response)
52090
+ return null;
52091
+ const ordered = response.order.map((id) => response.posts[id]).filter((p) => Boolean(p));
52092
+ return this.hydratePosts(ordered);
52093
+ }
52094
+ teamIdForBotChannelCache;
52095
+ async resolveTeamIdForBotChannel() {
52096
+ if (this.teamIdForBotChannelCache !== undefined) {
52097
+ return this.teamIdForBotChannelCache;
52098
+ }
52099
+ const channel = await getChannelRaw(this.apiConfig, this.config.channelId);
52100
+ const teamId = channel?.team_id || null;
52101
+ this.teamIdForBotChannelCache = teamId;
52102
+ if (teamId) {
52103
+ mcpLogger.debug(`Resolved team id ${formatShortId(teamId)} for bot channel`);
52104
+ }
52105
+ return teamId;
52106
+ }
52107
+ async hydratePosts(posts) {
51993
52108
  const usernameByUserId = new Map;
51994
- for (const p of limited) {
52109
+ for (const p of posts) {
51995
52110
  if (p.user_id && !usernameByUserId.has(p.user_id)) {
51996
52111
  usernameByUserId.set(p.user_id, await this.getUsername(p.user_id));
51997
52112
  }
51998
52113
  }
51999
- const channelType = limited[0] ? await this.getChannelType(limited[0].channel_id) : undefined;
52000
- return limited.map((p) => toMcpPost(p, p.user_id ? usernameByUserId.get(p.user_id) ?? null : null, channelType));
52114
+ const channelTypeByChannelId = new Map;
52115
+ for (const p of posts) {
52116
+ if (!channelTypeByChannelId.has(p.channel_id)) {
52117
+ channelTypeByChannelId.set(p.channel_id, await this.getChannelType(p.channel_id));
52118
+ }
52119
+ }
52120
+ return posts.map((p) => toMcpPost(p, p.user_id ? usernameByUserId.get(p.user_id) ?? null : null, channelTypeByChannelId.get(p.channel_id)));
52001
52121
  }
52002
52122
  }
52003
52123
  function toMcpPost(post, username, channelType) {
@@ -52255,6 +52375,15 @@ class SlackMcpPlatformApi {
52255
52375
  return null;
52256
52376
  }
52257
52377
  }
52378
+ async addReaction(postId, emojiName) {
52379
+ const name = emojiName.replace(/:/g, "");
52380
+ mcpLogger.debug(`addReaction: :${name}: on ts ${postId}`);
52381
+ await slackApi("reactions.add", this.config.botToken, {
52382
+ channel: this.config.channelId,
52383
+ timestamp: postId,
52384
+ name
52385
+ });
52386
+ }
52258
52387
  async readThread(threadRootId, options) {
52259
52388
  mcpLogger.debug(`readThread: ts ${threadRootId}`);
52260
52389
  try {
@@ -52277,6 +52406,91 @@ class SlackMcpPlatformApi {
52277
52406
  return [];
52278
52407
  }
52279
52408
  }
52409
+ async readChannelHistory(channelId, options) {
52410
+ const limit = options?.limit ?? 20;
52411
+ mcpLogger.debug(`readChannelHistory: ${channelId} (limit=${limit})`);
52412
+ try {
52413
+ const response = await slackApi("conversations.history", this.config.botToken, {
52414
+ channel: channelId,
52415
+ limit
52416
+ });
52417
+ const messages = [...response.messages ?? []].sort((a, b) => parseFloat(a.ts) - parseFloat(b.ts));
52418
+ const usernameByUserId = new Map;
52419
+ for (const m of messages) {
52420
+ if (m.user && !usernameByUserId.has(m.user)) {
52421
+ usernameByUserId.set(m.user, await this.getUsername(m.user));
52422
+ }
52423
+ }
52424
+ return messages.map((m) => slackMessageToMcpPost(m, channelId, m.user ? usernameByUserId.get(m.user) ?? null : null));
52425
+ } catch (err) {
52426
+ mcpLogger.debug(`readChannelHistory ${channelId} failed: ${err}`);
52427
+ return null;
52428
+ }
52429
+ }
52430
+ async getChannelInfo(channelId) {
52431
+ mcpLogger.debug(`getChannelInfo: ${channelId}`);
52432
+ try {
52433
+ const response = await slackApi("conversations.info", this.config.botToken, { channel: channelId });
52434
+ const ch = response.channel;
52435
+ const isPrivate = ch.is_private || ch.is_im || ch.is_mpim || false;
52436
+ return {
52437
+ id: ch.id,
52438
+ channelType: isPrivate ? "private" : "public",
52439
+ name: ch.name
52440
+ };
52441
+ } catch (err) {
52442
+ mcpLogger.debug(`getChannelInfo ${channelId} failed: ${err}`);
52443
+ return null;
52444
+ }
52445
+ }
52446
+ async getChannelMembers(channelId) {
52447
+ const maxPages = 20;
52448
+ const all = [];
52449
+ let cursor = undefined;
52450
+ try {
52451
+ for (let i2 = 0;i2 < maxPages; i2++) {
52452
+ const params = {
52453
+ channel: channelId,
52454
+ limit: 1000
52455
+ };
52456
+ if (cursor)
52457
+ params.cursor = cursor;
52458
+ const response = await slackApi("conversations.members", this.config.botToken, params);
52459
+ all.push(...response.members ?? []);
52460
+ cursor = response.response_metadata?.next_cursor;
52461
+ if (!cursor)
52462
+ return all;
52463
+ }
52464
+ mcpLogger.warn(`getChannelMembers ${channelId} hit page cap of ${maxPages}`);
52465
+ return all;
52466
+ } catch (err) {
52467
+ mcpLogger.debug(`getChannelMembers ${channelId} failed: ${err}`);
52468
+ return null;
52469
+ }
52470
+ }
52471
+ async resolveRecipient(recipient) {
52472
+ const id = recipient.replace(/^<@/, "").replace(/>$/, "");
52473
+ if (!/^[UW][A-Z0-9]{8,}$/.test(id)) {
52474
+ return null;
52475
+ }
52476
+ try {
52477
+ const response = await slackApi("users.info", this.config.botToken, { user: id });
52478
+ return { id: response.user.id, username: response.user.name ?? null };
52479
+ } catch (err) {
52480
+ mcpLogger.debug(`resolveRecipient ${id} failed: ${err}`);
52481
+ return null;
52482
+ }
52483
+ }
52484
+ async sendDirectMessage(recipientUserId, message) {
52485
+ const opened = await slackApi("conversations.open", this.config.botToken, { users: recipientUserId });
52486
+ const dmChannelId = opened.channel.id;
52487
+ const post = await slackApi("chat.postMessage", this.config.botToken, {
52488
+ channel: dmChannelId,
52489
+ text: message,
52490
+ mrkdwn: true
52491
+ });
52492
+ return { postId: post.ts };
52493
+ }
52280
52494
  }
52281
52495
  function slackMessageToMcpPost(message, channelId, username) {
52282
52496
  const createAt = Math.floor(parseFloat(message.ts) * 1000);
@@ -53453,7 +53667,8 @@ function buildPermissionArgs(opts) {
53453
53667
  PLATFORM_THREAD_ID: opts.threadId || "",
53454
53668
  ALLOWED_USERS: opts.platformConfig.allowedUsers.join(","),
53455
53669
  DEBUG: opts.debug ? "1" : "",
53456
- PERMISSION_TIMEOUT_MS: String(opts.permissionTimeoutMs)
53670
+ PERMISSION_TIMEOUT_MS: String(opts.permissionTimeoutMs),
53671
+ SESSION_OWNER_USERNAME: opts.sessionOwnerUsername || ""
53457
53672
  };
53458
53673
  if (opts.platformConfig.appToken) {
53459
53674
  mcpEnv.PLATFORM_APP_TOKEN = opts.platformConfig.appToken;
@@ -53592,7 +53807,8 @@ class ClaudeCli extends EventEmitter2 {
53592
53807
  debug: this.debug,
53593
53808
  workingDir: this.options.workingDir,
53594
53809
  uploadDir: this.options.uploadDir,
53595
- outboundFiles: this.options.outboundFiles
53810
+ outboundFiles: this.options.outboundFiles,
53811
+ sessionOwnerUsername: this.options.sessionOwnerUsername
53596
53812
  });
53597
53813
  args.push(...permResult.args);
53598
53814
  this.mcpConfigTempFile = permResult.tempFile;
@@ -55233,7 +55449,8 @@ function buildRestartCliOptions(session, ctx) {
55233
55449
  permissionTimeoutMs: ctx.permissionTimeoutMs,
55234
55450
  account: ctx.account,
55235
55451
  uploadDir: getSessionUploadDir(session.platformId, session.threadId),
55236
- outboundFiles: platformMcpConfig.outboundFiles
55452
+ outboundFiles: platformMcpConfig.outboundFiles,
55453
+ sessionOwnerUsername: session.startedBy
55237
55454
  };
55238
55455
  }
55239
55456
 
@@ -66456,7 +66673,8 @@ async function startSession(options, username, displayName, replyToPostId, platf
66456
66673
  permissionTimeoutMs: ctx.config.permissionTimeoutMs,
66457
66674
  account: claudeAccount ? { id: claudeAccount.id, home: claudeAccount.home, apiKey: claudeAccount.apiKey } : undefined,
66458
66675
  uploadDir: getSessionUploadDir(platformId, actualThreadId),
66459
- outboundFiles: platformMcpConfig.outboundFiles
66676
+ outboundFiles: platformMcpConfig.outboundFiles,
66677
+ sessionOwnerUsername: username
66460
66678
  };
66461
66679
  const claude = new ClaudeCli(cliOptions);
66462
66680
  const session = {
@@ -66607,7 +66825,8 @@ Please start a new session.`), { action: "Post resume failure notification" });
66607
66825
  permissionTimeoutMs: ctx.config.permissionTimeoutMs,
66608
66826
  account: claudeAccount ? { id: claudeAccount.id, home: claudeAccount.home, apiKey: claudeAccount.apiKey } : undefined,
66609
66827
  uploadDir: getSessionUploadDir(platformId, state.threadId),
66610
- outboundFiles: platformMcpConfig.outboundFiles
66828
+ outboundFiles: platformMcpConfig.outboundFiles,
66829
+ sessionOwnerUsername: state.startedBy
66611
66830
  };
66612
66831
  const claude = new ClaudeCli(cliOptions);
66613
66832
  const session = {
@@ -78631,8 +78850,105 @@ class UpdateInstaller {
78631
78850
  }
78632
78851
  }
78633
78852
 
78853
+ // src/auto-update/respawn.ts
78854
+ init_logger();
78855
+ import { spawn as spawn5 } from "child_process";
78856
+ import { existsSync as existsSync15, statSync as statSync4 } from "fs";
78857
+ import { delimiter, join as join11 } from "path";
78858
+ var log38 = createLogger("respawn");
78859
+ function decideRespawn(env5 = process.env, isTTY = !!process.stdout.isTTY) {
78860
+ if (env5.CLAUDE_THREADS_BIN) {
78861
+ return { kind: "exit-for-supervisor", supervisor: "claude-threads-daemon" };
78862
+ }
78863
+ if (env5.pm_id !== undefined && env5.PM2_HOME) {
78864
+ return { kind: "exit-for-supervisor", supervisor: "pm2" };
78865
+ }
78866
+ if (env5.INVOCATION_ID) {
78867
+ return { kind: "exit-for-supervisor", supervisor: "systemd" };
78868
+ }
78869
+ if (env5.CLAUDE_THREADS_INTERACTIVE) {
78870
+ return { kind: "exit-for-supervisor", supervisor: "wrapped-tty" };
78871
+ }
78872
+ if (!isTTY) {
78873
+ return { kind: "exit-for-supervisor", supervisor: "none-headless" };
78874
+ }
78875
+ return { kind: "self-respawn" };
78876
+ }
78877
+ function resolveClaudeThreadsBin(_env = process.env, _existsSync = existsSync15, _isFileExecutable = isFileExecutable) {
78878
+ const isWin2 = process.platform === "win32";
78879
+ const names = isWin2 ? ["claude-threads.cmd", "claude-threads.exe", "claude-threads.bat"] : ["claude-threads"];
78880
+ const path10 = _env.PATH || _env.Path || "";
78881
+ const dirs = path10.split(delimiter).filter(Boolean);
78882
+ const home = _env.HOME || _env.USERPROFILE;
78883
+ const bunRoot = _env.BUN_INSTALL || (home ? join11(home, ".bun") : null);
78884
+ if (bunRoot) {
78885
+ const bunBin = join11(bunRoot, "bin");
78886
+ if (!dirs.includes(bunBin)) {
78887
+ dirs.push(bunBin);
78888
+ }
78889
+ }
78890
+ for (const dir of dirs) {
78891
+ for (const name of names) {
78892
+ const candidate = join11(dir, name);
78893
+ if (_existsSync(candidate) && _isFileExecutable(candidate)) {
78894
+ return candidate;
78895
+ }
78896
+ }
78897
+ }
78898
+ return null;
78899
+ }
78900
+ function isFileExecutable(path10) {
78901
+ try {
78902
+ const stat = statSync4(path10);
78903
+ if (!stat.isFile())
78904
+ return false;
78905
+ if (process.platform === "win32")
78906
+ return true;
78907
+ return (stat.mode & 73) !== 0;
78908
+ } catch {
78909
+ return false;
78910
+ }
78911
+ }
78912
+ function spawnReplacement(argv = process.argv.slice(2), binPath = resolveClaudeThreadsBin()) {
78913
+ if (!binPath) {
78914
+ log38.error("Could not resolve claude-threads on PATH; self-respawn aborted");
78915
+ return false;
78916
+ }
78917
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
78918
+ try {
78919
+ process.stdin.setRawMode(false);
78920
+ } catch {}
78921
+ }
78922
+ const childEnv = { ...process.env };
78923
+ delete childEnv.CLAUDE_THREADS_BIN;
78924
+ delete childEnv.CLAUDE_THREADS_INTERACTIVE;
78925
+ const useShell = process.platform === "win32";
78926
+ let child;
78927
+ try {
78928
+ child = spawn5(binPath, argv, {
78929
+ detached: true,
78930
+ stdio: "inherit",
78931
+ env: childEnv,
78932
+ shell: useShell
78933
+ });
78934
+ } catch (err) {
78935
+ log38.error(`spawn() threw: ${err instanceof Error ? err.message : String(err)}`);
78936
+ return false;
78937
+ }
78938
+ child.once("error", (err) => {
78939
+ log38.error(`Replacement process error: ${err.message}`);
78940
+ });
78941
+ if (child.pid === undefined) {
78942
+ log38.error("Spawn returned no pid (binary likely not executable)");
78943
+ return false;
78944
+ }
78945
+ child.unref();
78946
+ log38.info(`Spawned replacement pid=${child.pid} from ${binPath}`);
78947
+ return true;
78948
+ }
78949
+
78634
78950
  // src/auto-update/manager.ts
78635
- var log38 = createLogger("updater");
78951
+ var log39 = createLogger("updater");
78636
78952
 
78637
78953
  class AutoUpdateManager extends EventEmitter9 {
78638
78954
  config;
@@ -78655,23 +78971,23 @@ class AutoUpdateManager extends EventEmitter9 {
78655
78971
  }
78656
78972
  start() {
78657
78973
  if (!this.config.enabled) {
78658
- log38.info("Auto-update is disabled");
78974
+ log39.info("Auto-update is disabled");
78659
78975
  return;
78660
78976
  }
78661
78977
  const updateResult = this.installer.checkJustUpdated();
78662
78978
  if (updateResult) {
78663
- log38.info(`\uD83C\uDF89 Updated from v${updateResult.previousVersion} to v${updateResult.currentVersion}`);
78979
+ log39.info(`\uD83C\uDF89 Updated from v${updateResult.previousVersion} to v${updateResult.currentVersion}`);
78664
78980
  this.callbacks.broadcastUpdate((fmt) => `\uD83C\uDF89 ${fmt.formatBold("Bot updated")} from v${updateResult.previousVersion} to v${updateResult.currentVersion}`).catch((err) => {
78665
- log38.warn(`Failed to broadcast update notification: ${err}`);
78981
+ log39.warn(`Failed to broadcast update notification: ${err}`);
78666
78982
  });
78667
78983
  }
78668
78984
  this.checker.start();
78669
- log38.info(`\uD83D\uDD04 Auto-update manager started (mode: ${this.config.autoRestartMode})`);
78985
+ log39.info(`\uD83D\uDD04 Auto-update manager started (mode: ${this.config.autoRestartMode})`);
78670
78986
  }
78671
78987
  stop() {
78672
78988
  this.checker.stop();
78673
78989
  this.scheduler.stop();
78674
- log38.debug("Auto-update manager stopped");
78990
+ log39.debug("Auto-update manager stopped");
78675
78991
  }
78676
78992
  getState() {
78677
78993
  return { ...this.state };
@@ -78685,10 +79001,10 @@ class AutoUpdateManager extends EventEmitter9 {
78685
79001
  async forceUpdate() {
78686
79002
  const updateInfo = this.state.updateInfo || await this.checker.check();
78687
79003
  if (!updateInfo) {
78688
- log38.info("No update available");
79004
+ log39.info("No update available");
78689
79005
  return;
78690
79006
  }
78691
- log38.info("Forcing immediate update");
79007
+ log39.info("Forcing immediate update");
78692
79008
  await this.performUpdate(updateInfo);
78693
79009
  }
78694
79010
  deferUpdate(minutes = 60) {
@@ -78740,12 +79056,41 @@ class AutoUpdateManager extends EventEmitter9 {
78740
79056
  if (result.success) {
78741
79057
  this.updateStatus("pending_restart");
78742
79058
  this.emit("update:restart", updateInfo.latestVersion);
78743
- await this.callbacks.broadcastUpdate((fmt) => `✅ ${fmt.formatBold("Update installed")} - restarting now. ${fmt.formatItalic("Sessions will resume automatically.")}`).catch(() => {});
79059
+ const decision = decideRespawn();
79060
+ const binPath = decision.kind === "self-respawn" ? resolveClaudeThreadsBin() : null;
79061
+ const canSelfRespawn = decision.kind === "self-respawn" && binPath !== null;
79062
+ const willAutoRestart = decision.kind === "exit-for-supervisor" ? decision.supervisor !== "none-headless" : canSelfRespawn;
79063
+ if (willAutoRestart) {
79064
+ await this.callbacks.broadcastUpdate((fmt) => `✅ ${fmt.formatBold("Update installed")} to v${updateInfo.latestVersion}. Restarting now, sessions will resume automatically.`).catch(() => {});
79065
+ } else {
79066
+ await this.callbacks.broadcastUpdate((fmt) => `✅ ${fmt.formatBold("Update installed")} to v${updateInfo.latestVersion}. Could not auto-restart (no supervisor and no claude-threads on PATH); please run ${fmt.formatCode("claude-threads")} to bring the bot back. Sessions are persisted and will resume.`).catch(() => {});
79067
+ }
78744
79068
  await new Promise((resolve7) => setTimeout(resolve7, 1000));
78745
- await this.callbacks.prepareForRestart();
78746
- log38.info(`\uD83D\uDD04 Restarting for update to v${updateInfo.latestVersion}`);
79069
+ try {
79070
+ await this.callbacks.prepareForRestart();
79071
+ } catch (err) {
79072
+ const reason = err instanceof Error ? err.message : String(err);
79073
+ log39.error(`prepareForRestart failed: ${reason}`);
79074
+ await this.callbacks.broadcastUpdate((fmt) => `⚠️ ${fmt.formatBold("Restart aborted")}: shutdown sequence failed (${reason}). Sessions may be in an inconsistent state; please run ${fmt.formatCode("claude-threads")} manually.`).catch(() => {});
79075
+ process.exit(1);
79076
+ }
79077
+ log39.info(`\uD83D\uDD04 Restarting for update to v${updateInfo.latestVersion}`);
78747
79078
  process.stdout.write("\x1B[2J\x1B[H");
78748
79079
  process.stdout.write("\x1B[?25h");
79080
+ if (decision.kind === "self-respawn") {
79081
+ if (canSelfRespawn) {
79082
+ const ok = spawnReplacement(undefined, binPath);
79083
+ if (ok) {
79084
+ process.exit(0);
79085
+ }
79086
+ log39.error("Self-respawn launch failed after binary resolution succeeded");
79087
+ await this.callbacks.broadcastUpdate((fmt) => `⚠️ ${fmt.formatBold("Auto-restart failed")} after install: please run ${fmt.formatCode("claude-threads")} to bring the bot back. Sessions are persisted and will resume.`).catch(() => {});
79088
+ } else {
79089
+ log39.error("claude-threads not found on PATH; manual restart required");
79090
+ }
79091
+ process.exit(0);
79092
+ }
79093
+ log39.debug(`Restart handled by supervisor: ${decision.supervisor}`);
78749
79094
  process.exit(RESTART_EXIT_CODE);
78750
79095
  } else {
78751
79096
  const errorMsg = result.error ?? "Unknown error";
@@ -78835,7 +79180,7 @@ async function main() {
78835
79180
  return false;
78836
79181
  };
78837
79182
  if (await shouldUseAutoRestart()) {
78838
- const { spawn: spawn5 } = await import("child_process");
79183
+ const { spawn: spawn6 } = await import("child_process");
78839
79184
  const { dirname: dirname9, resolve: resolve7 } = await import("path");
78840
79185
  const { fileURLToPath: fileURLToPath7 } = await import("url");
78841
79186
  const __filename2 = fileURLToPath7(import.meta.url);
@@ -78847,7 +79192,7 @@ async function main() {
78847
79192
  const binPath = __filename2;
78848
79193
  let child;
78849
79194
  if (process.platform === "win32") {
78850
- child = spawn5("bash", [daemonPath, "--restart-on-error", ...args], {
79195
+ child = spawn6("bash", [daemonPath, "--restart-on-error", ...args], {
78851
79196
  stdio: "inherit",
78852
79197
  env: {
78853
79198
  ...process.env,
@@ -78856,7 +79201,7 @@ async function main() {
78856
79201
  }
78857
79202
  });
78858
79203
  } else {
78859
- child = spawn5(daemonPath, ["--restart-on-error", ...args], {
79204
+ child = spawn6(daemonPath, ["--restart-on-error", ...args], {
78860
79205
  stdio: "inherit",
78861
79206
  env: {
78862
79207
  ...process.env,
@@ -79135,9 +79480,9 @@ async function startWithoutDaemon() {
79135
79480
  await session.updateAllStickyMessages();
79136
79481
  await session.killAllSessions();
79137
79482
  autoUpdateManager?.stop();
79138
- for (const client of platforms.values()) {
79139
- client.disconnect();
79140
- }
79483
+ await Promise.all(Array.from(platforms.values()).map((client) => client.disconnect().catch((err) => {
79484
+ ui.addLog({ level: "warn", component: "shutdown", message: `disconnect failed: ${err}` });
79485
+ })));
79141
79486
  if (!isHeadless) {
79142
79487
  process.stdout.write("\x1B[2J\x1B[H");
79143
79488
  process.stdout.write("\x1B[?25h");