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/CHANGELOG.md +10 -0
- package/README.md +42 -31
- package/dist/index.js +370 -25
- package/dist/mcp/mcp-server.js +894 -38
- package/package.json +1 -1
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
|
|
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
|
|
52000
|
-
|
|
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
|
|
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
|
-
|
|
78974
|
+
log39.info("Auto-update is disabled");
|
|
78659
78975
|
return;
|
|
78660
78976
|
}
|
|
78661
78977
|
const updateResult = this.installer.checkJustUpdated();
|
|
78662
78978
|
if (updateResult) {
|
|
78663
|
-
|
|
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
|
-
|
|
78981
|
+
log39.warn(`Failed to broadcast update notification: ${err}`);
|
|
78666
78982
|
});
|
|
78667
78983
|
}
|
|
78668
78984
|
this.checker.start();
|
|
78669
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79004
|
+
log39.info("No update available");
|
|
78689
79005
|
return;
|
|
78690
79006
|
}
|
|
78691
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78746
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
79139
|
-
|
|
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");
|