claude-threads 0.51.0 → 0.52.1
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 +13 -0
- package/dist/index.js +98 -22
- package/package.json +1 -1
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.52.1] - 2026-01-09
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Bug report image upload error** - Fixed `downloadFile` method losing `this` context when passed as callback, causing "undefined is not an object" error (#163)
|
|
14
|
+
|
|
15
|
+
## [0.52.0] - 2026-01-09
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Image attachments in bug reports** - Fixed image attachments not appearing in bug reports by uploading to Catbox before generating the report (#158)
|
|
19
|
+
- **Sticky message install command** - Fixed npm/bun string issue in sticky message and added website link (#159)
|
|
20
|
+
- **Paused sessions auto-resuming** - Fixed paused sessions incorrectly auto-resuming on bot restart by persisting paused state (#160)
|
|
21
|
+
- **403 permission errors** - Fixed 403 errors when unpinning/updating stale task posts by handling channel post deletion gracefully (#161)
|
|
22
|
+
|
|
10
23
|
## [0.51.0] - 2026-01-09
|
|
11
24
|
|
|
12
25
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -42852,9 +42852,42 @@ class MattermostClient extends EventEmitter {
|
|
|
42852
42852
|
extension: mattermostFile.extension
|
|
42853
42853
|
};
|
|
42854
42854
|
}
|
|
42855
|
+
async processAndEmitPost(post) {
|
|
42856
|
+
const hasFileIds = post.file_ids && post.file_ids.length > 0;
|
|
42857
|
+
const hasFileMetadata = post.metadata?.files && post.metadata.files.length > 0;
|
|
42858
|
+
if (hasFileIds && !hasFileMetadata) {
|
|
42859
|
+
log.debug(`Post ${post.id.substring(0, 8)} has ${post.file_ids.length} file(s), fetching metadata`);
|
|
42860
|
+
try {
|
|
42861
|
+
const files = [];
|
|
42862
|
+
for (const fileId of post.file_ids) {
|
|
42863
|
+
try {
|
|
42864
|
+
const file = await this.api("GET", `/files/${fileId}/info`);
|
|
42865
|
+
files.push(file);
|
|
42866
|
+
} catch (err) {
|
|
42867
|
+
log.warn(`Failed to fetch file info for ${fileId}: ${err}`);
|
|
42868
|
+
}
|
|
42869
|
+
}
|
|
42870
|
+
if (files.length > 0) {
|
|
42871
|
+
post.metadata = {
|
|
42872
|
+
...post.metadata,
|
|
42873
|
+
files
|
|
42874
|
+
};
|
|
42875
|
+
log.debug(`Enriched post ${post.id.substring(0, 8)} with ${files.length} file(s)`);
|
|
42876
|
+
}
|
|
42877
|
+
} catch (err) {
|
|
42878
|
+
log.warn(`Failed to fetch file metadata for post ${post.id.substring(0, 8)}: ${err}`);
|
|
42879
|
+
}
|
|
42880
|
+
}
|
|
42881
|
+
const user = await this.getUser(post.user_id);
|
|
42882
|
+
const normalizedPost = this.normalizePlatformPost(post);
|
|
42883
|
+
this.emit("message", normalizedPost, user);
|
|
42884
|
+
if (!post.root_id) {
|
|
42885
|
+
this.emit("channel_post", normalizedPost, user);
|
|
42886
|
+
}
|
|
42887
|
+
}
|
|
42855
42888
|
MAX_RETRIES = 3;
|
|
42856
42889
|
RETRY_DELAY_MS = 500;
|
|
42857
|
-
async api(method, path, body, retryCount = 0) {
|
|
42890
|
+
async api(method, path, body, retryCount = 0, options) {
|
|
42858
42891
|
const url = `${this.url}/api/v4${path}`;
|
|
42859
42892
|
log.debug(`API ${method} ${path}`);
|
|
42860
42893
|
const response = await fetch(url, {
|
|
@@ -42871,9 +42904,14 @@ class MattermostClient extends EventEmitter {
|
|
|
42871
42904
|
const delay = this.RETRY_DELAY_MS * Math.pow(2, retryCount);
|
|
42872
42905
|
log.warn(`API ${method} ${path} failed with 500, retrying in ${delay}ms (attempt ${retryCount + 1}/${this.MAX_RETRIES})`);
|
|
42873
42906
|
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
42874
|
-
return this.api(method, path, body, retryCount + 1);
|
|
42907
|
+
return this.api(method, path, body, retryCount + 1, options);
|
|
42908
|
+
}
|
|
42909
|
+
const isSilent = options?.silent?.includes(response.status);
|
|
42910
|
+
if (isSilent) {
|
|
42911
|
+
log.debug(`API ${method} ${path} failed: ${response.status} (expected)`);
|
|
42912
|
+
} else {
|
|
42913
|
+
log.warn(`API ${method} ${path} failed: ${response.status} ${text.substring(0, 100)}`);
|
|
42875
42914
|
}
|
|
42876
|
-
log.warn(`API ${method} ${path} failed: ${response.status} ${text.substring(0, 100)}`);
|
|
42877
42915
|
throw new Error(`Mattermost API error ${response.status}: ${text}`);
|
|
42878
42916
|
}
|
|
42879
42917
|
log.debug(`API ${method} ${path} \u2192 ${response.status}`);
|
|
@@ -42992,7 +43030,14 @@ class MattermostClient extends EventEmitter {
|
|
|
42992
43030
|
}
|
|
42993
43031
|
async unpinPost(postId) {
|
|
42994
43032
|
log.debug(`Unpinning post ${postId.substring(0, 8)}`);
|
|
42995
|
-
|
|
43033
|
+
try {
|
|
43034
|
+
await this.api("POST", `/posts/${postId}/unpin`, undefined, 0, { silent: [403, 404] });
|
|
43035
|
+
} catch (err) {
|
|
43036
|
+
if (err instanceof Error && (err.message.includes("403") || err.message.includes("404"))) {
|
|
43037
|
+
return;
|
|
43038
|
+
}
|
|
43039
|
+
throw err;
|
|
43040
|
+
}
|
|
42996
43041
|
}
|
|
42997
43042
|
async getPinnedPosts() {
|
|
42998
43043
|
const response = await this.api("GET", `/channels/${this.channelId}/pinned`);
|
|
@@ -43120,13 +43165,7 @@ class MattermostClient extends EventEmitter {
|
|
|
43120
43165
|
if (post.channel_id !== this.channelId)
|
|
43121
43166
|
return;
|
|
43122
43167
|
this.lastProcessedPostId = post.id;
|
|
43123
|
-
this.
|
|
43124
|
-
const normalizedPost = this.normalizePlatformPost(post);
|
|
43125
|
-
this.emit("message", normalizedPost, user);
|
|
43126
|
-
if (!post.root_id) {
|
|
43127
|
-
this.emit("channel_post", normalizedPost, user);
|
|
43128
|
-
}
|
|
43129
|
-
});
|
|
43168
|
+
this.processAndEmitPost(post);
|
|
43130
43169
|
} catch (err) {
|
|
43131
43170
|
wsLogger.warn(`Failed to parse post: ${err}`);
|
|
43132
43171
|
}
|
|
@@ -45330,6 +45369,7 @@ function resetSessionActivity(session) {
|
|
|
45330
45369
|
session.lastActivityAt = new Date;
|
|
45331
45370
|
session.timeoutWarningPosted = false;
|
|
45332
45371
|
session.lifecyclePostId = undefined;
|
|
45372
|
+
session.isPaused = undefined;
|
|
45333
45373
|
}
|
|
45334
45374
|
function updateLastMessage(session, post) {
|
|
45335
45375
|
session.lastMessageId = post.id;
|
|
@@ -45545,8 +45585,18 @@ async function bumpTasksToBottomWithContent(session, newContent, registerPost) {
|
|
|
45545
45585
|
sessionLog(session).debug(`Could not remove toggle emoji: ${err}`);
|
|
45546
45586
|
}
|
|
45547
45587
|
await session.platform.unpinPost(oldTasksPostId).catch(() => {});
|
|
45548
|
-
|
|
45549
|
-
|
|
45588
|
+
let repurposedPostId = null;
|
|
45589
|
+
try {
|
|
45590
|
+
await session.platform.updatePost(oldTasksPostId, contentToPost);
|
|
45591
|
+
repurposedPostId = oldTasksPostId;
|
|
45592
|
+
registerPost(oldTasksPostId, session.threadId);
|
|
45593
|
+
} catch (err) {
|
|
45594
|
+
sessionLog(session).debug(`Could not repurpose task post (creating new): ${err}`);
|
|
45595
|
+
const newPost = await session.platform.createPost(contentToPost, session.threadId);
|
|
45596
|
+
repurposedPostId = newPost.id;
|
|
45597
|
+
registerPost(newPost.id, session.threadId);
|
|
45598
|
+
updateLastMessage(session, newPost);
|
|
45599
|
+
}
|
|
45550
45600
|
if (oldTasksContent) {
|
|
45551
45601
|
const displayContent = getTaskDisplayContent(session);
|
|
45552
45602
|
const newTasksPost = await session.platform.createInteractivePost(displayContent, [TASK_TOGGLE_EMOJIS[0]], session.threadId);
|
|
@@ -45558,7 +45608,7 @@ async function bumpTasksToBottomWithContent(session, newContent, registerPost) {
|
|
|
45558
45608
|
} else {
|
|
45559
45609
|
session.tasksPostId = null;
|
|
45560
45610
|
}
|
|
45561
|
-
return oldTasksPostId;
|
|
45611
|
+
return repurposedPostId || oldTasksPostId;
|
|
45562
45612
|
} finally {
|
|
45563
45613
|
releaseLock();
|
|
45564
45614
|
}
|
|
@@ -51548,10 +51598,10 @@ async function reportBug(session, description, username, ctx, errorContext, atta
|
|
|
51548
51598
|
const context = await collectBugReportContext(session, errorContext);
|
|
51549
51599
|
let imageUrls = [];
|
|
51550
51600
|
let imageErrors = [];
|
|
51551
|
-
const downloadFile = session.platform.downloadFile;
|
|
51601
|
+
const downloadFile = session.platform.downloadFile?.bind(session.platform);
|
|
51552
51602
|
if (attachedFiles && attachedFiles.length > 0 && downloadFile) {
|
|
51553
51603
|
await postInfo(session, `\uD83D\uDCE4 Uploading ${attachedFiles.length} image(s)...`);
|
|
51554
|
-
const uploadResults = await uploadImages(attachedFiles,
|
|
51604
|
+
const uploadResults = await uploadImages(attachedFiles, downloadFile);
|
|
51555
51605
|
imageUrls = uploadResults.filter((r) => r.success && typeof r.url === "string").map((r) => r.url);
|
|
51556
51606
|
imageErrors = uploadResults.filter((r) => !r.success).map((r) => `${r.originalFile.name}: ${r.error}`);
|
|
51557
51607
|
}
|
|
@@ -52377,12 +52427,16 @@ async function handleExitPlanMode(session, toolUseId, ctx) {
|
|
|
52377
52427
|
}
|
|
52378
52428
|
async function cleanupOrphanedTaskPosts(session, currentTaskPostId) {
|
|
52379
52429
|
try {
|
|
52430
|
+
const botUser = await session.platform.getBotUser();
|
|
52431
|
+
const botUserId = botUser.id;
|
|
52380
52432
|
const history = await session.platform.getThreadHistory(session.threadId, { limit: 50 });
|
|
52381
52433
|
const taskPostPattern = /^(?:(?:---|___|\*\*\*|\u2014+)\s*\n)?\uD83D\uDCCB/;
|
|
52382
52434
|
let cleanedCount = 0;
|
|
52383
52435
|
for (const msg of history) {
|
|
52384
52436
|
if (msg.id === currentTaskPostId)
|
|
52385
52437
|
continue;
|
|
52438
|
+
if (msg.userId !== botUserId)
|
|
52439
|
+
continue;
|
|
52386
52440
|
if (!taskPostPattern.test(msg.message))
|
|
52387
52441
|
continue;
|
|
52388
52442
|
sessionLog4(session).info(`Cleaning up orphaned task post ${msg.id.substring(0, 8)}`);
|
|
@@ -53326,6 +53380,7 @@ ${CHAT_PLATFORM_PROMPT}`;
|
|
|
53326
53380
|
${sessionFormatter.formatItalic("Reconnected to Claude session. You can continue where you left off.")}`;
|
|
53327
53381
|
await withErrorHandling(() => session.platform.updatePost(postId, resumeMsg), { action: "Update timeout/shutdown post for resume", session });
|
|
53328
53382
|
session.lifecyclePostId = undefined;
|
|
53383
|
+
session.isPaused = undefined;
|
|
53329
53384
|
} else {
|
|
53330
53385
|
const restartMsg = `${sessionFormatter.formatBold("Session resumed")} after bot restart (v${VERSION})
|
|
53331
53386
|
${sessionFormatter.formatItalic("Reconnected to Claude session. You can continue where you left off.")}`;
|
|
@@ -53418,15 +53473,20 @@ async function handleExit(sessionId, code, ctx) {
|
|
|
53418
53473
|
sessionLog6(session).debug(`Exited after interrupt, preserving for resume`);
|
|
53419
53474
|
ctx.ops.stopTyping(session);
|
|
53420
53475
|
cleanupSessionTimers(session);
|
|
53476
|
+
const message = session.hasClaudeResponded ? `\u2139\uFE0F Session paused. Send a new message to continue.` : `\u2139\uFE0F Session ended before Claude could respond. Send a new message to start fresh.`;
|
|
53477
|
+
const pausePost = await withErrorHandling(() => postInfo(session, message), { action: "Post session pause notification", session });
|
|
53421
53478
|
if (session.hasClaudeResponded) {
|
|
53479
|
+
session.isPaused = true;
|
|
53480
|
+
if (pausePost) {
|
|
53481
|
+
session.lifecyclePostId = pausePost.id;
|
|
53482
|
+
ctx.ops.registerPost(pausePost.id, session.threadId);
|
|
53483
|
+
}
|
|
53422
53484
|
ctx.ops.persistSession(session);
|
|
53423
53485
|
}
|
|
53424
53486
|
ctx.ops.emitSessionRemove(session.sessionId);
|
|
53425
53487
|
mutableSessions(ctx).delete(session.sessionId);
|
|
53426
53488
|
cleanupPostIndex(ctx, session.threadId);
|
|
53427
53489
|
keepAlive.sessionEnded();
|
|
53428
|
-
const message = session.hasClaudeResponded ? `\u2139\uFE0F Session paused. Send a new message to continue.` : `\u2139\uFE0F Session ended before Claude could respond. Send a new message to start fresh.`;
|
|
53429
|
-
await withErrorHandling(() => postInfo(session, message), { action: "Post session pause notification", session });
|
|
53430
53490
|
sessionLog6(session).info(`\u23F8 Session paused`);
|
|
53431
53491
|
await ctx.ops.updateStickyMessage();
|
|
53432
53492
|
return;
|
|
@@ -53565,6 +53625,7 @@ async function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
|
|
|
53565
53625
|
ctx.ops.registerPost(timeoutPost.id, session.threadId);
|
|
53566
53626
|
}
|
|
53567
53627
|
}
|
|
53628
|
+
session.isPaused = true;
|
|
53568
53629
|
ctx.ops.persistSession(session);
|
|
53569
53630
|
await killSession(session, false, ctx);
|
|
53570
53631
|
continue;
|
|
@@ -54051,7 +54112,7 @@ async function buildStickyMessage(sessions, platformId, config, formatter, getTh
|
|
|
54051
54112
|
}
|
|
54052
54113
|
}
|
|
54053
54114
|
lines2.push("");
|
|
54054
|
-
lines2.push(`${formatter.formatItalic("Mention me to start a session")} \xB7 ${formatter.formatCode("
|
|
54115
|
+
lines2.push(`${formatter.formatItalic("Mention me to start a session")} \xB7 ${formatter.formatCode("bun install -g claude-threads")} \xB7 ${formatter.formatLink("claude-threads.run", "https://claude-threads.run/")}`);
|
|
54055
54116
|
return lines2.join(`
|
|
54056
54117
|
`);
|
|
54057
54118
|
}
|
|
@@ -54103,7 +54164,7 @@ async function buildStickyMessage(sessions, platformId, config, formatter, getTh
|
|
|
54103
54164
|
}
|
|
54104
54165
|
}
|
|
54105
54166
|
lines.push("");
|
|
54106
|
-
lines.push(`${formatter.formatItalic("Mention me to start a session")} \xB7 ${formatter.formatCode("
|
|
54167
|
+
lines.push(`${formatter.formatItalic("Mention me to start a session")} \xB7 ${formatter.formatCode("bun install -g claude-threads")} \xB7 ${formatter.formatLink("claude-threads.run", "https://claude-threads.run/")}`);
|
|
54107
54168
|
return lines.join(`
|
|
54108
54169
|
`);
|
|
54109
54170
|
}
|
|
@@ -54651,6 +54712,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
54651
54712
|
pendingContextPrompt: persistedContextPrompt,
|
|
54652
54713
|
needsContextPromptOnNextMessage: session.needsContextPromptOnNextMessage,
|
|
54653
54714
|
lifecyclePostId: session.lifecyclePostId,
|
|
54715
|
+
isPaused: session.isPaused,
|
|
54654
54716
|
sessionTitle: session.sessionTitle,
|
|
54655
54717
|
sessionDescription: session.sessionDescription,
|
|
54656
54718
|
pullRequestUrl: session.pullRequestUrl,
|
|
@@ -54858,9 +54920,23 @@ class SessionManager extends EventEmitter4 {
|
|
|
54858
54920
|
const persisted = this.sessionStore.load();
|
|
54859
54921
|
log18.info(`\uD83D\uDCC2 Loaded ${persisted.size} session(s) from persistence`);
|
|
54860
54922
|
if (persisted.size > 0) {
|
|
54861
|
-
|
|
54923
|
+
const activeToResume = [];
|
|
54924
|
+
const pausedToSkip = [];
|
|
54862
54925
|
for (const state of persisted.values()) {
|
|
54863
|
-
|
|
54926
|
+
if (state.isPaused) {
|
|
54927
|
+
pausedToSkip.push(state);
|
|
54928
|
+
} else {
|
|
54929
|
+
activeToResume.push(state);
|
|
54930
|
+
}
|
|
54931
|
+
}
|
|
54932
|
+
if (pausedToSkip.length > 0) {
|
|
54933
|
+
log18.info(`\u23F8\uFE0F ${pausedToSkip.length} session(s) remain paused (waiting for user message)`);
|
|
54934
|
+
}
|
|
54935
|
+
if (activeToResume.length > 0) {
|
|
54936
|
+
log18.info(`\uD83D\uDD04 Attempting to resume ${activeToResume.length} active session(s)...`);
|
|
54937
|
+
for (const state of activeToResume) {
|
|
54938
|
+
await resumeSession(state, this.getContext());
|
|
54939
|
+
}
|
|
54864
54940
|
}
|
|
54865
54941
|
}
|
|
54866
54942
|
await this.updateStickyMessage();
|