claude-threads 0.19.2 → 0.20.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 +18 -0
- package/dist/index.js +398 -30
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.20.0] - 2026-01-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Sticky message improvements** - Enhanced the channel sticky message with active sessions
|
|
14
|
+
- Shows display name in bold (e.g., **Anne**) instead of username
|
|
15
|
+
- Added session description below the title (generated by Claude)
|
|
16
|
+
- Added install hint: `npm i -g claude-threads` in footer
|
|
17
|
+
- Periodic refresh every 60 seconds to keep relative times current
|
|
18
|
+
- Auto-cleanup of old sticky messages from failed runs at startup
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Sticky message updates on session end** - Message now updates when sessions are:
|
|
22
|
+
- Canceled via `!stop` or ❌ reaction
|
|
23
|
+
- Paused/interrupted via `!escape` or ⏸️ reaction
|
|
24
|
+
- Killed due to timeout
|
|
25
|
+
- Failed to start or resume
|
|
26
|
+
- **Race condition in sticky updates** - Added mutex to prevent duplicate sticky posts when multiple updates happen concurrently
|
|
27
|
+
|
|
10
28
|
## [0.19.2] - 2026-01-01
|
|
11
29
|
|
|
12
30
|
### Added
|
package/dist/index.js
CHANGED
|
@@ -13289,9 +13289,11 @@ class MattermostClient extends EventEmitter {
|
|
|
13289
13289
|
this.allowedUsers = platformConfig.allowedUsers;
|
|
13290
13290
|
}
|
|
13291
13291
|
normalizePlatformUser(mattermostUser) {
|
|
13292
|
+
const displayName = mattermostUser.first_name || mattermostUser.nickname || mattermostUser.username;
|
|
13292
13293
|
return {
|
|
13293
13294
|
id: mattermostUser.id,
|
|
13294
13295
|
username: mattermostUser.username,
|
|
13296
|
+
displayName,
|
|
13295
13297
|
email: mattermostUser.email
|
|
13296
13298
|
};
|
|
13297
13299
|
}
|
|
@@ -13425,6 +13427,16 @@ class MattermostClient extends EventEmitter {
|
|
|
13425
13427
|
async deletePost(postId) {
|
|
13426
13428
|
await this.api("DELETE", `/posts/${postId}`);
|
|
13427
13429
|
}
|
|
13430
|
+
async pinPost(postId) {
|
|
13431
|
+
await this.api("POST", `/posts/${postId}/pin`);
|
|
13432
|
+
}
|
|
13433
|
+
async unpinPost(postId) {
|
|
13434
|
+
await this.api("POST", `/posts/${postId}/unpin`);
|
|
13435
|
+
}
|
|
13436
|
+
async getPinnedPosts() {
|
|
13437
|
+
const response = await this.api("GET", `/channels/${this.channelId}/pinned`);
|
|
13438
|
+
return response.order || [];
|
|
13439
|
+
}
|
|
13428
13440
|
async getThreadHistory(threadId, options) {
|
|
13429
13441
|
try {
|
|
13430
13442
|
const response = await this.api("GET", `/posts/${threadId}/thread`);
|
|
@@ -13513,7 +13525,11 @@ class MattermostClient extends EventEmitter {
|
|
|
13513
13525
|
if (post.channel_id !== this.channelId)
|
|
13514
13526
|
return;
|
|
13515
13527
|
this.getUser(post.user_id).then((user) => {
|
|
13516
|
-
|
|
13528
|
+
const normalizedPost = this.normalizePlatformPost(post);
|
|
13529
|
+
this.emit("message", normalizedPost, user);
|
|
13530
|
+
if (!post.root_id) {
|
|
13531
|
+
this.emit("channel_post", normalizedPost, user);
|
|
13532
|
+
}
|
|
13517
13533
|
});
|
|
13518
13534
|
} catch (err) {
|
|
13519
13535
|
wsLogger.debug(`Failed to parse post: ${err}`);
|
|
@@ -13716,11 +13732,37 @@ class SessionStore {
|
|
|
13716
13732
|
return staleIds;
|
|
13717
13733
|
}
|
|
13718
13734
|
clear() {
|
|
13719
|
-
this.
|
|
13735
|
+
const data = this.loadRaw();
|
|
13736
|
+
this.writeAtomic({ version: STORE_VERSION, sessions: {}, stickyPostIds: data.stickyPostIds });
|
|
13720
13737
|
if (this.debug) {
|
|
13721
13738
|
console.log(" [persist] Cleared all sessions");
|
|
13722
13739
|
}
|
|
13723
13740
|
}
|
|
13741
|
+
saveStickyPostId(platformId, postId) {
|
|
13742
|
+
const data = this.loadRaw();
|
|
13743
|
+
if (!data.stickyPostIds) {
|
|
13744
|
+
data.stickyPostIds = {};
|
|
13745
|
+
}
|
|
13746
|
+
data.stickyPostIds[platformId] = postId;
|
|
13747
|
+
this.writeAtomic(data);
|
|
13748
|
+
if (this.debug) {
|
|
13749
|
+
console.log(` [persist] Saved sticky post ID for ${platformId}: ${postId.substring(0, 8)}...`);
|
|
13750
|
+
}
|
|
13751
|
+
}
|
|
13752
|
+
getStickyPostIds() {
|
|
13753
|
+
const data = this.loadRaw();
|
|
13754
|
+
return new Map(Object.entries(data.stickyPostIds || {}));
|
|
13755
|
+
}
|
|
13756
|
+
removeStickyPostId(platformId) {
|
|
13757
|
+
const data = this.loadRaw();
|
|
13758
|
+
if (data.stickyPostIds && data.stickyPostIds[platformId]) {
|
|
13759
|
+
delete data.stickyPostIds[platformId];
|
|
13760
|
+
this.writeAtomic(data);
|
|
13761
|
+
if (this.debug) {
|
|
13762
|
+
console.log(` [persist] Removed sticky post ID for ${platformId}`);
|
|
13763
|
+
}
|
|
13764
|
+
}
|
|
13765
|
+
}
|
|
13724
13766
|
findByThread(platformId, threadId) {
|
|
13725
13767
|
const sessionId = `${platformId}:${threadId}`;
|
|
13726
13768
|
const data = this.loadRaw();
|
|
@@ -14822,7 +14864,7 @@ function handleEvent(session, event, ctx) {
|
|
|
14822
14864
|
handleExitPlanMode(session, block.id, ctx);
|
|
14823
14865
|
hasSpecialTool = true;
|
|
14824
14866
|
} else if (block.name === "TodoWrite") {
|
|
14825
|
-
handleTodoWrite(session, block.input);
|
|
14867
|
+
handleTodoWrite(session, block.input, ctx);
|
|
14826
14868
|
} else if (block.name === "Task") {
|
|
14827
14869
|
handleTaskStart(session, block.id, block.input, ctx);
|
|
14828
14870
|
} else if (block.name === "AskUserQuestion") {
|
|
@@ -14865,7 +14907,27 @@ function formatEvent(session, e, ctx) {
|
|
|
14865
14907
|
const parts = [];
|
|
14866
14908
|
for (const block of msg?.content || []) {
|
|
14867
14909
|
if (block.type === "text" && block.text) {
|
|
14868
|
-
|
|
14910
|
+
let text = block.text.replace(/<thinking>[\s\S]*?<\/thinking>/g, "").trim();
|
|
14911
|
+
const titleMatch = text.match(/\[SESSION_TITLE:\s*([^\]]+)\]/);
|
|
14912
|
+
if (titleMatch) {
|
|
14913
|
+
const newTitle = titleMatch[1].trim();
|
|
14914
|
+
if (newTitle !== session.sessionTitle) {
|
|
14915
|
+
session.sessionTitle = newTitle;
|
|
14916
|
+
ctx.persistSession(session);
|
|
14917
|
+
ctx.updateStickyMessage().catch(() => {});
|
|
14918
|
+
}
|
|
14919
|
+
text = text.replace(/\[SESSION_TITLE:\s*[^\]]+\]\s*/g, "").trim();
|
|
14920
|
+
}
|
|
14921
|
+
const descMatch = text.match(/\[SESSION_DESCRIPTION:\s*([^\]]+)\]/);
|
|
14922
|
+
if (descMatch) {
|
|
14923
|
+
const newDesc = descMatch[1].trim();
|
|
14924
|
+
if (newDesc !== session.sessionDescription) {
|
|
14925
|
+
session.sessionDescription = newDesc;
|
|
14926
|
+
ctx.persistSession(session);
|
|
14927
|
+
ctx.updateStickyMessage().catch(() => {});
|
|
14928
|
+
}
|
|
14929
|
+
text = text.replace(/\[SESSION_DESCRIPTION:\s*[^\]]+\]\s*/g, "").trim();
|
|
14930
|
+
}
|
|
14869
14931
|
if (text)
|
|
14870
14932
|
parts.push(text);
|
|
14871
14933
|
} else if (block.type === "tool_use" && block.name) {
|
|
@@ -14965,7 +15027,7 @@ async function handleExitPlanMode(session, toolUseId, ctx) {
|
|
|
14965
15027
|
session.pendingApproval = { postId: post.id, type: "plan", toolUseId };
|
|
14966
15028
|
ctx.stopTyping(session);
|
|
14967
15029
|
}
|
|
14968
|
-
async function handleTodoWrite(session, input) {
|
|
15030
|
+
async function handleTodoWrite(session, input, ctx) {
|
|
14969
15031
|
const todos = input.todos;
|
|
14970
15032
|
if (!todos || todos.length === 0) {
|
|
14971
15033
|
session.tasksCompleted = true;
|
|
@@ -15046,6 +15108,7 @@ async function handleTodoWrite(session, input) {
|
|
|
15046
15108
|
const post = await session.platform.createInteractivePost(displayMessage, [TASK_TOGGLE_EMOJIS[0]], session.threadId);
|
|
15047
15109
|
session.tasksPostId = post.id;
|
|
15048
15110
|
}
|
|
15111
|
+
ctx.updateStickyMessage().catch(() => {});
|
|
15049
15112
|
} catch (err) {
|
|
15050
15113
|
console.error(" \u26A0\uFE0F Failed to update tasks:", err);
|
|
15051
15114
|
}
|
|
@@ -15326,6 +15389,9 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
15326
15389
|
if (this.options.chrome) {
|
|
15327
15390
|
args.push("--chrome");
|
|
15328
15391
|
}
|
|
15392
|
+
if (this.options.appendSystemPrompt) {
|
|
15393
|
+
args.push("--append-system-prompt", this.options.appendSystemPrompt);
|
|
15394
|
+
}
|
|
15329
15395
|
if (this.debug) {
|
|
15330
15396
|
console.log(` [claude] Starting: ${claudePath} ${args.slice(0, 5).join(" ")}...`);
|
|
15331
15397
|
}
|
|
@@ -18917,7 +18983,7 @@ async function cancelSession(session, username, ctx) {
|
|
|
18917
18983
|
const shortId = session.threadId.substring(0, 8);
|
|
18918
18984
|
console.log(` \uD83D\uDED1 Session (${shortId}\u2026) cancelled by @${username}`);
|
|
18919
18985
|
await session.platform.createPost(`\uD83D\uDED1 **Session cancelled** by @${username}`, session.threadId);
|
|
18920
|
-
ctx.killSession(session.threadId);
|
|
18986
|
+
await ctx.killSession(session.threadId);
|
|
18921
18987
|
}
|
|
18922
18988
|
async function interruptSession(session, username) {
|
|
18923
18989
|
if (!session.claude.isRunning()) {
|
|
@@ -19336,7 +19402,34 @@ function findPersistedByThreadId(persisted, threadId) {
|
|
|
19336
19402
|
}
|
|
19337
19403
|
return;
|
|
19338
19404
|
}
|
|
19339
|
-
|
|
19405
|
+
var CHAT_PLATFORM_PROMPT = `
|
|
19406
|
+
You are running inside a chat platform (like Mattermost or Slack). Users interact with you through chat messages in a thread.
|
|
19407
|
+
|
|
19408
|
+
SESSION METADATA: At the START of your first response, include metadata about this session:
|
|
19409
|
+
|
|
19410
|
+
[SESSION_TITLE: <short title>]
|
|
19411
|
+
[SESSION_DESCRIPTION: <brief description>]
|
|
19412
|
+
|
|
19413
|
+
Title requirements:
|
|
19414
|
+
- 3-7 words maximum
|
|
19415
|
+
- Descriptive of the main task/topic
|
|
19416
|
+
- Written in imperative form (e.g., "Fix login bug", "Add dark mode")
|
|
19417
|
+
- Do NOT include quotes
|
|
19418
|
+
|
|
19419
|
+
Description requirements:
|
|
19420
|
+
- 1-2 sentences explaining what you're helping with
|
|
19421
|
+
- Summarize the current work or goal
|
|
19422
|
+
- Keep it under 100 characters
|
|
19423
|
+
|
|
19424
|
+
You can update both later if the session focus changes significantly.
|
|
19425
|
+
|
|
19426
|
+
Example: If the user asks "help me debug why the tests are failing", respond with:
|
|
19427
|
+
[SESSION_TITLE: Debug failing tests]
|
|
19428
|
+
[SESSION_DESCRIPTION: Investigating test failures and fixing broken assertions in the test suite.]
|
|
19429
|
+
|
|
19430
|
+
Then continue with your normal response.
|
|
19431
|
+
`.trim();
|
|
19432
|
+
async function startSession(options, username, displayName, replyToPostId, platformId, ctx) {
|
|
19340
19433
|
const threadId = replyToPostId || "";
|
|
19341
19434
|
const existingSessionId = ctx.getSessionId(platformId, threadId);
|
|
19342
19435
|
const existingSession = ctx.sessions.get(existingSessionId);
|
|
@@ -19372,7 +19465,8 @@ async function startSession(options, username, replyToPostId, platformId, ctx) {
|
|
|
19372
19465
|
sessionId: claudeSessionId,
|
|
19373
19466
|
resume: false,
|
|
19374
19467
|
chrome: ctx.chromeEnabled,
|
|
19375
|
-
platformConfig: platformMcpConfig
|
|
19468
|
+
platformConfig: platformMcpConfig,
|
|
19469
|
+
appendSystemPrompt: CHAT_PLATFORM_PROMPT
|
|
19376
19470
|
};
|
|
19377
19471
|
const claude = new ClaudeCli(cliOptions);
|
|
19378
19472
|
const session = {
|
|
@@ -19382,6 +19476,7 @@ async function startSession(options, username, replyToPostId, platformId, ctx) {
|
|
|
19382
19476
|
platform,
|
|
19383
19477
|
claudeSessionId,
|
|
19384
19478
|
startedBy: username,
|
|
19479
|
+
startedByDisplayName: displayName,
|
|
19385
19480
|
startedAt: new Date,
|
|
19386
19481
|
lastActivityAt: new Date,
|
|
19387
19482
|
sessionNumber: ctx.sessions.size + 1,
|
|
@@ -19408,7 +19503,8 @@ async function startSession(options, username, replyToPostId, platformId, ctx) {
|
|
|
19408
19503
|
isResumed: false,
|
|
19409
19504
|
wasInterrupted: false,
|
|
19410
19505
|
inProgressTaskStart: null,
|
|
19411
|
-
activeToolStarts: new Map
|
|
19506
|
+
activeToolStarts: new Map,
|
|
19507
|
+
firstPrompt: options.prompt
|
|
19412
19508
|
};
|
|
19413
19509
|
ctx.sessions.set(sessionId, session);
|
|
19414
19510
|
ctx.registerPost(post.id, actualThreadId);
|
|
@@ -19416,6 +19512,7 @@ async function startSession(options, username, replyToPostId, platformId, ctx) {
|
|
|
19416
19512
|
console.log(` \u25B6 Session #${ctx.sessions.size} started (${shortId}\u2026) by @${username}`);
|
|
19417
19513
|
keepAlive.sessionStarted();
|
|
19418
19514
|
await ctx.updateSessionHeader(session);
|
|
19515
|
+
await ctx.updateStickyMessage();
|
|
19419
19516
|
ctx.startTyping(session);
|
|
19420
19517
|
claude.on("event", (e) => ctx.handleEvent(sessionId, e));
|
|
19421
19518
|
claude.on("exit", (code) => ctx.handleExit(sessionId, code));
|
|
@@ -19426,9 +19523,9 @@ async function startSession(options, username, replyToPostId, platformId, ctx) {
|
|
|
19426
19523
|
ctx.stopTyping(session);
|
|
19427
19524
|
await session.platform.createPost(`\u274C ${err}`, actualThreadId);
|
|
19428
19525
|
ctx.sessions.delete(session.sessionId);
|
|
19526
|
+
await ctx.updateStickyMessage();
|
|
19429
19527
|
return;
|
|
19430
19528
|
}
|
|
19431
|
-
session.firstPrompt = options.prompt;
|
|
19432
19529
|
const shouldPrompt = await ctx.shouldPromptForWorktree(session);
|
|
19433
19530
|
if (shouldPrompt) {
|
|
19434
19531
|
session.queuedPrompt = options.prompt;
|
|
@@ -19480,6 +19577,7 @@ Please start a new session.`, state.threadId);
|
|
|
19480
19577
|
const sessionId = ctx.getSessionId(platformId, state.threadId);
|
|
19481
19578
|
const skipPerms = ctx.skipPermissions && !state.forceInteractivePermissions;
|
|
19482
19579
|
const platformMcpConfig = platform.getMcpConfig();
|
|
19580
|
+
const needsTitlePrompt = !state.sessionTitle;
|
|
19483
19581
|
const cliOptions = {
|
|
19484
19582
|
workingDir: state.workingDir,
|
|
19485
19583
|
threadId: state.threadId,
|
|
@@ -19487,7 +19585,8 @@ Please start a new session.`, state.threadId);
|
|
|
19487
19585
|
sessionId: state.claudeSessionId,
|
|
19488
19586
|
resume: true,
|
|
19489
19587
|
chrome: ctx.chromeEnabled,
|
|
19490
|
-
platformConfig: platformMcpConfig
|
|
19588
|
+
platformConfig: platformMcpConfig,
|
|
19589
|
+
appendSystemPrompt: needsTitlePrompt ? CHAT_PLATFORM_PROMPT : undefined
|
|
19491
19590
|
};
|
|
19492
19591
|
const claude = new ClaudeCli(cliOptions);
|
|
19493
19592
|
const session = {
|
|
@@ -19497,6 +19596,7 @@ Please start a new session.`, state.threadId);
|
|
|
19497
19596
|
platform,
|
|
19498
19597
|
claudeSessionId: state.claudeSessionId,
|
|
19499
19598
|
startedBy: state.startedBy,
|
|
19599
|
+
startedByDisplayName: state.startedByDisplayName,
|
|
19500
19600
|
startedAt: new Date(state.startedAt),
|
|
19501
19601
|
lastActivityAt: new Date,
|
|
19502
19602
|
sessionNumber: state.sessionNumber,
|
|
@@ -19529,7 +19629,9 @@ Please start a new session.`, state.threadId);
|
|
|
19529
19629
|
worktreePromptDisabled: state.worktreePromptDisabled,
|
|
19530
19630
|
queuedPrompt: state.queuedPrompt,
|
|
19531
19631
|
firstPrompt: state.firstPrompt,
|
|
19532
|
-
needsContextPromptOnNextMessage: state.needsContextPromptOnNextMessage
|
|
19632
|
+
needsContextPromptOnNextMessage: state.needsContextPromptOnNextMessage,
|
|
19633
|
+
sessionTitle: state.sessionTitle,
|
|
19634
|
+
sessionDescription: state.sessionDescription
|
|
19533
19635
|
};
|
|
19534
19636
|
ctx.sessions.set(sessionId, session);
|
|
19535
19637
|
if (state.sessionStartPostId) {
|
|
@@ -19544,6 +19646,7 @@ Please start a new session.`, state.threadId);
|
|
|
19544
19646
|
await session.platform.createPost(`\uD83D\uDD04 **Session resumed** after bot restart (v${VERSION})
|
|
19545
19647
|
*Reconnected to Claude session. You can continue where you left off.*`, state.threadId);
|
|
19546
19648
|
await ctx.updateSessionHeader(session);
|
|
19649
|
+
await ctx.updateStickyMessage();
|
|
19547
19650
|
ctx.persistSession(session);
|
|
19548
19651
|
} catch (err) {
|
|
19549
19652
|
console.error(` \u274C Failed to resume session ${shortId}...:`, err);
|
|
@@ -19553,6 +19656,7 @@ Please start a new session.`, state.threadId);
|
|
|
19553
19656
|
await session.platform.createPost(`\u26A0\uFE0F **Could not resume previous session.** Starting fresh.
|
|
19554
19657
|
*Your previous conversation context is preserved, but Claude needs to re-read it.*`, state.threadId);
|
|
19555
19658
|
} catch {}
|
|
19659
|
+
await ctx.updateStickyMessage();
|
|
19556
19660
|
}
|
|
19557
19661
|
}
|
|
19558
19662
|
async function sendFollowUp(session, message, files, ctx) {
|
|
@@ -19636,6 +19740,7 @@ async function handleExit(sessionId, code, ctx) {
|
|
|
19636
19740
|
await session.platform.createPost(`\u2139\uFE0F Session paused. Send a new message to continue.`, session.threadId);
|
|
19637
19741
|
} catch {}
|
|
19638
19742
|
console.log(` \u23F8\uFE0F Session paused (${shortId}\u2026) \u2014 ${ctx.sessions.size} active`);
|
|
19743
|
+
await ctx.updateStickyMessage();
|
|
19639
19744
|
return;
|
|
19640
19745
|
}
|
|
19641
19746
|
if (session.isResumed && code !== 0) {
|
|
@@ -19650,6 +19755,7 @@ async function handleExit(sessionId, code, ctx) {
|
|
|
19650
19755
|
try {
|
|
19651
19756
|
await session.platform.createPost(`\u26A0\uFE0F **Session resume failed** (exit code ${code}). The session data is preserved - try restarting the bot.`, session.threadId);
|
|
19652
19757
|
} catch {}
|
|
19758
|
+
await ctx.updateStickyMessage();
|
|
19653
19759
|
return;
|
|
19654
19760
|
}
|
|
19655
19761
|
console.log(` [exit] Session ${shortId}... normal exit, cleaning up`);
|
|
@@ -19675,8 +19781,9 @@ async function handleExit(sessionId, code, ctx) {
|
|
|
19675
19781
|
console.log(` [exit] Session ${shortId}... non-zero exit, preserving for potential retry`);
|
|
19676
19782
|
}
|
|
19677
19783
|
console.log(` \u25A0 Session ended (${shortId}\u2026) \u2014 ${ctx.sessions.size} active`);
|
|
19784
|
+
await ctx.updateStickyMessage();
|
|
19678
19785
|
}
|
|
19679
|
-
function killSession(session, unpersist, ctx) {
|
|
19786
|
+
async function killSession(session, unpersist, ctx) {
|
|
19680
19787
|
const shortId = session.threadId.substring(0, 8);
|
|
19681
19788
|
if (!unpersist) {
|
|
19682
19789
|
session.isRestarting = true;
|
|
@@ -19694,6 +19801,7 @@ function killSession(session, unpersist, ctx) {
|
|
|
19694
19801
|
ctx.unpersistSession(session.threadId);
|
|
19695
19802
|
}
|
|
19696
19803
|
console.log(` \u2716 Session killed (${shortId}\u2026) \u2014 ${ctx.sessions.size} active`);
|
|
19804
|
+
await ctx.updateStickyMessage();
|
|
19697
19805
|
}
|
|
19698
19806
|
function killAllSessions(ctx) {
|
|
19699
19807
|
for (const session of ctx.sessions.values()) {
|
|
@@ -19719,7 +19827,7 @@ async function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
|
|
|
19719
19827
|
ctx.persistSession(session);
|
|
19720
19828
|
ctx.registerPost(timeoutPost.id, session.threadId);
|
|
19721
19829
|
} catch {}
|
|
19722
|
-
killSession(session, false, ctx);
|
|
19830
|
+
await killSession(session, false, ctx);
|
|
19723
19831
|
continue;
|
|
19724
19832
|
}
|
|
19725
19833
|
const warningThresholdMs = timeoutMs - warningMs;
|
|
@@ -20250,6 +20358,239 @@ function clearContextPromptTimeout(pending) {
|
|
|
20250
20358
|
}
|
|
20251
20359
|
}
|
|
20252
20360
|
|
|
20361
|
+
// src/session/sticky-message.ts
|
|
20362
|
+
var stickyPostIds = new Map;
|
|
20363
|
+
var needsBump = new Map;
|
|
20364
|
+
var updateLocks = new Map;
|
|
20365
|
+
var sessionStore = null;
|
|
20366
|
+
function initialize(store) {
|
|
20367
|
+
sessionStore = store;
|
|
20368
|
+
const persistedIds = store.getStickyPostIds();
|
|
20369
|
+
for (const [platformId, postId] of persistedIds) {
|
|
20370
|
+
stickyPostIds.set(platformId, postId);
|
|
20371
|
+
}
|
|
20372
|
+
if (persistedIds.size > 0) {
|
|
20373
|
+
console.log(` \uD83D\uDCCC Restored ${persistedIds.size} sticky post ID(s) from persistence`);
|
|
20374
|
+
}
|
|
20375
|
+
}
|
|
20376
|
+
function formatRelativeTime(date) {
|
|
20377
|
+
const now = Date.now();
|
|
20378
|
+
const diff = now - date.getTime();
|
|
20379
|
+
const minutes = Math.floor(diff / 60000);
|
|
20380
|
+
const hours = Math.floor(minutes / 60);
|
|
20381
|
+
if (minutes < 1)
|
|
20382
|
+
return "just now";
|
|
20383
|
+
if (minutes < 60)
|
|
20384
|
+
return `${minutes}m ago`;
|
|
20385
|
+
if (hours < 24)
|
|
20386
|
+
return `${hours}h ago`;
|
|
20387
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
20388
|
+
}
|
|
20389
|
+
function getTaskProgress(session) {
|
|
20390
|
+
if (!session.lastTasksContent)
|
|
20391
|
+
return null;
|
|
20392
|
+
const match = session.lastTasksContent.match(/\((\d+)\/(\d+)/);
|
|
20393
|
+
if (match) {
|
|
20394
|
+
return `${match[1]}/${match[2]}`;
|
|
20395
|
+
}
|
|
20396
|
+
return null;
|
|
20397
|
+
}
|
|
20398
|
+
function getSessionTopic(session) {
|
|
20399
|
+
if (session.sessionTitle) {
|
|
20400
|
+
return session.sessionTitle;
|
|
20401
|
+
}
|
|
20402
|
+
return formatTopicFromPrompt(session.firstPrompt);
|
|
20403
|
+
}
|
|
20404
|
+
function formatTopicFromPrompt(prompt) {
|
|
20405
|
+
if (!prompt)
|
|
20406
|
+
return "_No topic_";
|
|
20407
|
+
let cleaned = prompt.replace(/^@[\w-]+\s*/g, "").trim();
|
|
20408
|
+
if (cleaned.startsWith("!")) {
|
|
20409
|
+
return "_No topic_";
|
|
20410
|
+
}
|
|
20411
|
+
cleaned = cleaned.replace(/\s+/g, " ");
|
|
20412
|
+
if (cleaned.length > 50) {
|
|
20413
|
+
cleaned = cleaned.substring(0, 47) + "\u2026";
|
|
20414
|
+
}
|
|
20415
|
+
return cleaned || "_No topic_";
|
|
20416
|
+
}
|
|
20417
|
+
function buildStickyMessage(sessions, platformId) {
|
|
20418
|
+
const platformSessions = [...sessions.values()].filter((s) => s.platformId === platformId);
|
|
20419
|
+
if (platformSessions.length === 0) {
|
|
20420
|
+
return [
|
|
20421
|
+
"---",
|
|
20422
|
+
"**Active Claude Threads**",
|
|
20423
|
+
"",
|
|
20424
|
+
"_No active sessions_",
|
|
20425
|
+
"",
|
|
20426
|
+
"_Mention me to start a session_ \xB7 `npm i -g claude-threads`"
|
|
20427
|
+
].join(`
|
|
20428
|
+
`);
|
|
20429
|
+
}
|
|
20430
|
+
platformSessions.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime());
|
|
20431
|
+
const count = platformSessions.length;
|
|
20432
|
+
const lines = [
|
|
20433
|
+
"---",
|
|
20434
|
+
`**Active Claude Threads** (${count})`,
|
|
20435
|
+
""
|
|
20436
|
+
];
|
|
20437
|
+
for (const session of platformSessions) {
|
|
20438
|
+
const topic = getSessionTopic(session);
|
|
20439
|
+
const threadLink = `[${topic}](/_redirect/pl/${session.threadId})`;
|
|
20440
|
+
const displayName = session.startedByDisplayName || session.startedBy;
|
|
20441
|
+
const time = formatRelativeTime(session.startedAt);
|
|
20442
|
+
const taskProgress = getTaskProgress(session);
|
|
20443
|
+
const progressStr = taskProgress ? ` \xB7 ${taskProgress}` : "";
|
|
20444
|
+
lines.push(`\u25B8 ${threadLink} \xB7 **${displayName}**${progressStr} \xB7 ${time}`);
|
|
20445
|
+
if (session.sessionDescription) {
|
|
20446
|
+
lines.push(` _${session.sessionDescription}_`);
|
|
20447
|
+
}
|
|
20448
|
+
}
|
|
20449
|
+
lines.push("");
|
|
20450
|
+
lines.push("_Mention me to start a session_ \xB7 `npm i -g claude-threads`");
|
|
20451
|
+
return lines.join(`
|
|
20452
|
+
`);
|
|
20453
|
+
}
|
|
20454
|
+
var DEBUG = process.env.DEBUG === "1";
|
|
20455
|
+
async function updateStickyMessage(platform, sessions) {
|
|
20456
|
+
const platformId = platform.platformId;
|
|
20457
|
+
const pendingUpdate = updateLocks.get(platformId);
|
|
20458
|
+
if (pendingUpdate) {
|
|
20459
|
+
await pendingUpdate;
|
|
20460
|
+
}
|
|
20461
|
+
let releaseLock;
|
|
20462
|
+
const lock = new Promise((resolve6) => {
|
|
20463
|
+
releaseLock = resolve6;
|
|
20464
|
+
});
|
|
20465
|
+
updateLocks.set(platformId, lock);
|
|
20466
|
+
try {
|
|
20467
|
+
await updateStickyMessageImpl(platform, sessions);
|
|
20468
|
+
} finally {
|
|
20469
|
+
releaseLock();
|
|
20470
|
+
updateLocks.delete(platformId);
|
|
20471
|
+
}
|
|
20472
|
+
}
|
|
20473
|
+
async function updateStickyMessageImpl(platform, sessions) {
|
|
20474
|
+
if (DEBUG) {
|
|
20475
|
+
const platformSessions = [...sessions.values()].filter((s) => s.platformId === platform.platformId);
|
|
20476
|
+
console.log(`[sticky] updateStickyMessage called for ${platform.platformId}, ${platformSessions.length} sessions`);
|
|
20477
|
+
for (const s of platformSessions) {
|
|
20478
|
+
console.log(`[sticky] - ${s.sessionId}: title="${s.sessionTitle}" firstPrompt="${s.firstPrompt?.substring(0, 30)}..."`);
|
|
20479
|
+
}
|
|
20480
|
+
}
|
|
20481
|
+
const content = buildStickyMessage(sessions, platform.platformId);
|
|
20482
|
+
const existingPostId = stickyPostIds.get(platform.platformId);
|
|
20483
|
+
const shouldBump = needsBump.get(platform.platformId) ?? false;
|
|
20484
|
+
if (DEBUG) {
|
|
20485
|
+
console.log(`[sticky] existingPostId: ${existingPostId || "(none)"}, needsBump: ${shouldBump}`);
|
|
20486
|
+
console.log(`[sticky] content preview: ${content.substring(0, 100).replace(/\n/g, "\\n")}...`);
|
|
20487
|
+
}
|
|
20488
|
+
try {
|
|
20489
|
+
if (existingPostId && !shouldBump) {
|
|
20490
|
+
if (DEBUG)
|
|
20491
|
+
console.log(`[sticky] Updating existing post in place...`);
|
|
20492
|
+
try {
|
|
20493
|
+
await platform.updatePost(existingPostId, content);
|
|
20494
|
+
try {
|
|
20495
|
+
await platform.pinPost(existingPostId);
|
|
20496
|
+
if (DEBUG)
|
|
20497
|
+
console.log(`[sticky] Re-pinned post`);
|
|
20498
|
+
} catch (pinErr) {
|
|
20499
|
+
if (DEBUG)
|
|
20500
|
+
console.log(`[sticky] Re-pin failed (might already be pinned):`, pinErr);
|
|
20501
|
+
}
|
|
20502
|
+
if (DEBUG)
|
|
20503
|
+
console.log(`[sticky] Updated successfully`);
|
|
20504
|
+
return;
|
|
20505
|
+
} catch (err) {
|
|
20506
|
+
if (DEBUG)
|
|
20507
|
+
console.log(`[sticky] Update failed, will create new:`, err);
|
|
20508
|
+
}
|
|
20509
|
+
}
|
|
20510
|
+
needsBump.set(platform.platformId, false);
|
|
20511
|
+
if (existingPostId) {
|
|
20512
|
+
if (DEBUG)
|
|
20513
|
+
console.log(`[sticky] Unpinning and deleting existing post ${existingPostId.substring(0, 8)}...`);
|
|
20514
|
+
try {
|
|
20515
|
+
await platform.unpinPost(existingPostId);
|
|
20516
|
+
if (DEBUG)
|
|
20517
|
+
console.log(`[sticky] Unpinned successfully`);
|
|
20518
|
+
} catch (err) {
|
|
20519
|
+
if (DEBUG)
|
|
20520
|
+
console.log(`[sticky] Unpin failed (probably already unpinned):`, err);
|
|
20521
|
+
}
|
|
20522
|
+
try {
|
|
20523
|
+
await platform.deletePost(existingPostId);
|
|
20524
|
+
if (DEBUG)
|
|
20525
|
+
console.log(`[sticky] Deleted successfully`);
|
|
20526
|
+
} catch (err) {
|
|
20527
|
+
if (DEBUG)
|
|
20528
|
+
console.log(`[sticky] Delete failed (probably already deleted):`, err);
|
|
20529
|
+
}
|
|
20530
|
+
stickyPostIds.delete(platform.platformId);
|
|
20531
|
+
}
|
|
20532
|
+
if (DEBUG)
|
|
20533
|
+
console.log(`[sticky] Creating new post...`);
|
|
20534
|
+
const post = await platform.createPost(content);
|
|
20535
|
+
stickyPostIds.set(platform.platformId, post.id);
|
|
20536
|
+
try {
|
|
20537
|
+
await platform.pinPost(post.id);
|
|
20538
|
+
if (DEBUG)
|
|
20539
|
+
console.log(`[sticky] Pinned post successfully`);
|
|
20540
|
+
} catch (err) {
|
|
20541
|
+
if (DEBUG)
|
|
20542
|
+
console.log(`[sticky] Failed to pin post:`, err);
|
|
20543
|
+
}
|
|
20544
|
+
if (sessionStore) {
|
|
20545
|
+
sessionStore.saveStickyPostId(platform.platformId, post.id);
|
|
20546
|
+
}
|
|
20547
|
+
console.log(` \uD83D\uDCCC Created sticky message for ${platform.platformId}: ${post.id.substring(0, 8)}...`);
|
|
20548
|
+
} catch (err) {
|
|
20549
|
+
console.error(` \u26A0\uFE0F Failed to update sticky message for ${platform.platformId}:`, err);
|
|
20550
|
+
}
|
|
20551
|
+
}
|
|
20552
|
+
async function updateAllStickyMessages(platforms, sessions) {
|
|
20553
|
+
const updates = [...platforms.values()].map((platform) => updateStickyMessage(platform, sessions));
|
|
20554
|
+
await Promise.all(updates);
|
|
20555
|
+
}
|
|
20556
|
+
function markNeedsBump(platformId) {
|
|
20557
|
+
needsBump.set(platformId, true);
|
|
20558
|
+
}
|
|
20559
|
+
async function cleanupOldStickyMessages(platform, botUserId) {
|
|
20560
|
+
const currentStickyId = stickyPostIds.get(platform.platformId);
|
|
20561
|
+
try {
|
|
20562
|
+
const pinnedPostIds = await platform.getPinnedPosts();
|
|
20563
|
+
if (DEBUG)
|
|
20564
|
+
console.log(`[sticky] Found ${pinnedPostIds.length} pinned posts, current sticky: ${currentStickyId?.substring(0, 8) || "(none)"}`);
|
|
20565
|
+
for (const postId of pinnedPostIds) {
|
|
20566
|
+
if (postId === currentStickyId)
|
|
20567
|
+
continue;
|
|
20568
|
+
try {
|
|
20569
|
+
const post = await platform.getPost(postId);
|
|
20570
|
+
if (!post)
|
|
20571
|
+
continue;
|
|
20572
|
+
if (post.userId === botUserId) {
|
|
20573
|
+
if (DEBUG)
|
|
20574
|
+
console.log(`[sticky] Cleaning up old sticky: ${postId.substring(0, 8)}...`);
|
|
20575
|
+
try {
|
|
20576
|
+
await platform.unpinPost(postId);
|
|
20577
|
+
await platform.deletePost(postId);
|
|
20578
|
+
console.log(` \uD83E\uDDF9 Cleaned up old sticky message: ${postId.substring(0, 8)}...`);
|
|
20579
|
+
} catch (err) {
|
|
20580
|
+
if (DEBUG)
|
|
20581
|
+
console.log(`[sticky] Failed to cleanup ${postId}:`, err);
|
|
20582
|
+
}
|
|
20583
|
+
}
|
|
20584
|
+
} catch (err) {
|
|
20585
|
+
if (DEBUG)
|
|
20586
|
+
console.log(`[sticky] Could not check post ${postId}:`, err);
|
|
20587
|
+
}
|
|
20588
|
+
}
|
|
20589
|
+
} catch (err) {
|
|
20590
|
+
console.error(` \u26A0\uFE0F Failed to cleanup old sticky messages:`, err);
|
|
20591
|
+
}
|
|
20592
|
+
}
|
|
20593
|
+
|
|
20253
20594
|
// src/session/types.ts
|
|
20254
20595
|
var MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || "5", 10);
|
|
20255
20596
|
var SESSION_TIMEOUT_MS = parseInt(process.env.SESSION_TIMEOUT_MS || "1800000", 10);
|
|
@@ -20275,6 +20616,9 @@ class SessionManager {
|
|
|
20275
20616
|
this.worktreeMode = worktreeMode;
|
|
20276
20617
|
this.cleanupTimer = setInterval(() => {
|
|
20277
20618
|
cleanupIdleSessions(SESSION_TIMEOUT_MS, SESSION_WARNING_MS, this.getLifecycleContext()).catch((err) => console.error(" [cleanup] Error during idle session cleanup:", err));
|
|
20619
|
+
if (this.sessions.size > 0) {
|
|
20620
|
+
this.updateStickyMessage().catch((err) => console.error(" [sticky] Error during periodic refresh:", err));
|
|
20621
|
+
}
|
|
20278
20622
|
}, 60000);
|
|
20279
20623
|
}
|
|
20280
20624
|
addPlatform(platformId, client) {
|
|
@@ -20285,6 +20629,10 @@ class SessionManager {
|
|
|
20285
20629
|
this.handleReaction(platformId, reaction.postId, reaction.emojiName, user.username);
|
|
20286
20630
|
}
|
|
20287
20631
|
});
|
|
20632
|
+
client.on("channel_post", () => {
|
|
20633
|
+
markNeedsBump(platformId);
|
|
20634
|
+
this.updateStickyMessage();
|
|
20635
|
+
});
|
|
20288
20636
|
console.log(` \uD83D\uDCE1 Platform "${platformId}" registered`);
|
|
20289
20637
|
}
|
|
20290
20638
|
removePlatform(platformId) {
|
|
@@ -20317,7 +20665,8 @@ class SessionManager {
|
|
|
20317
20665
|
postWorktreePrompt: (s, r) => this.postWorktreePrompt(s, r),
|
|
20318
20666
|
buildMessageContent: (t, p, f) => this.buildMessageContent(t, p, f),
|
|
20319
20667
|
offerContextPrompt: (s, q, e) => this.offerContextPrompt(s, q, e),
|
|
20320
|
-
bumpTasksToBottom: (s) => this.bumpTasksToBottom(s)
|
|
20668
|
+
bumpTasksToBottom: (s) => this.bumpTasksToBottom(s),
|
|
20669
|
+
updateStickyMessage: () => this.updateStickyMessage()
|
|
20321
20670
|
};
|
|
20322
20671
|
}
|
|
20323
20672
|
getEventContext() {
|
|
@@ -20328,7 +20677,9 @@ class SessionManager {
|
|
|
20328
20677
|
startTyping: (s) => this.startTyping(s),
|
|
20329
20678
|
stopTyping: (s) => this.stopTyping(s),
|
|
20330
20679
|
appendContent: (s, t) => this.appendContent(s, t),
|
|
20331
|
-
bumpTasksToBottom: (s) => this.bumpTasksToBottom(s)
|
|
20680
|
+
bumpTasksToBottom: (s) => this.bumpTasksToBottom(s),
|
|
20681
|
+
updateStickyMessage: () => this.updateStickyMessage(),
|
|
20682
|
+
persistSession: (s) => this.persistSession(s)
|
|
20332
20683
|
};
|
|
20333
20684
|
}
|
|
20334
20685
|
getReactionContext() {
|
|
@@ -20584,6 +20935,7 @@ class SessionManager {
|
|
|
20584
20935
|
threadId: session.threadId,
|
|
20585
20936
|
claudeSessionId: session.claudeSessionId,
|
|
20586
20937
|
startedBy: session.startedBy,
|
|
20938
|
+
startedByDisplayName: session.startedByDisplayName,
|
|
20587
20939
|
startedAt: session.startedAt.toISOString(),
|
|
20588
20940
|
lastActivityAt: session.lastActivityAt.toISOString(),
|
|
20589
20941
|
sessionNumber: session.sessionNumber,
|
|
@@ -20603,7 +20955,9 @@ class SessionManager {
|
|
|
20603
20955
|
firstPrompt: session.firstPrompt,
|
|
20604
20956
|
pendingContextPrompt: persistedContextPrompt,
|
|
20605
20957
|
needsContextPromptOnNextMessage: session.needsContextPromptOnNextMessage,
|
|
20606
|
-
timeoutPostId: session.timeoutPostId
|
|
20958
|
+
timeoutPostId: session.timeoutPostId,
|
|
20959
|
+
sessionTitle: session.sessionTitle,
|
|
20960
|
+
sessionDescription: session.sessionDescription
|
|
20607
20961
|
};
|
|
20608
20962
|
this.sessionStore.save(session.sessionId, state);
|
|
20609
20963
|
}
|
|
@@ -20613,21 +20967,35 @@ class SessionManager {
|
|
|
20613
20967
|
async updateSessionHeader(session) {
|
|
20614
20968
|
await updateSessionHeader(session, this.getCommandContext());
|
|
20615
20969
|
}
|
|
20970
|
+
async updateStickyMessage() {
|
|
20971
|
+
await updateAllStickyMessages(this.platforms, this.sessions);
|
|
20972
|
+
}
|
|
20616
20973
|
async initialize() {
|
|
20974
|
+
initialize(this.sessionStore);
|
|
20975
|
+
for (const platform of this.platforms.values()) {
|
|
20976
|
+
try {
|
|
20977
|
+
const botUser = await platform.getBotUser();
|
|
20978
|
+
await cleanupOldStickyMessages(platform, botUser.id);
|
|
20979
|
+
} catch (err) {
|
|
20980
|
+
console.error(` \u26A0\uFE0F Failed to cleanup old sticky messages for ${platform.platformId}:`, err);
|
|
20981
|
+
}
|
|
20982
|
+
}
|
|
20617
20983
|
const staleIds = this.sessionStore.cleanStale(SESSION_TIMEOUT_MS * 2);
|
|
20618
20984
|
if (staleIds.length > 0) {
|
|
20619
20985
|
console.log(` \uD83E\uDDF9 Cleaned ${staleIds.length} stale session(s) from persistence`);
|
|
20620
20986
|
}
|
|
20621
20987
|
const persisted = this.sessionStore.load();
|
|
20622
|
-
|
|
20623
|
-
|
|
20624
|
-
|
|
20625
|
-
|
|
20626
|
-
|
|
20988
|
+
console.log(` [persist] Loaded ${persisted.size} session(s)`);
|
|
20989
|
+
if (persisted.size > 0) {
|
|
20990
|
+
console.log(` \uD83D\uDD04 Attempting to resume ${persisted.size} persisted session(s)...`);
|
|
20991
|
+
for (const state of persisted.values()) {
|
|
20992
|
+
await resumeSession(state, this.getLifecycleContext());
|
|
20993
|
+
}
|
|
20627
20994
|
}
|
|
20995
|
+
await this.updateStickyMessage();
|
|
20628
20996
|
}
|
|
20629
|
-
async startSession(options, username, replyToPostId, platformId = "default") {
|
|
20630
|
-
await startSession(options, username, replyToPostId, platformId, this.getLifecycleContext());
|
|
20997
|
+
async startSession(options, username, replyToPostId, platformId = "default", displayName) {
|
|
20998
|
+
await startSession(options, username, displayName, replyToPostId, platformId, this.getLifecycleContext());
|
|
20631
20999
|
}
|
|
20632
21000
|
findSessionByThreadId(threadId) {
|
|
20633
21001
|
for (const session of this.sessions.values()) {
|
|
@@ -20670,11 +21038,11 @@ class SessionManager {
|
|
|
20670
21038
|
getPersistedSession(threadId) {
|
|
20671
21039
|
return this.findPersistedByThreadId(threadId);
|
|
20672
21040
|
}
|
|
20673
|
-
killSession(threadId, unpersist = true) {
|
|
21041
|
+
async killSession(threadId, unpersist = true) {
|
|
20674
21042
|
const session = this.findSessionByThreadId(threadId);
|
|
20675
21043
|
if (!session)
|
|
20676
21044
|
return;
|
|
20677
|
-
killSession(session, unpersist, this.getLifecycleContext());
|
|
21045
|
+
await killSession(session, unpersist, this.getLifecycleContext());
|
|
20678
21046
|
}
|
|
20679
21047
|
killAllSessions() {
|
|
20680
21048
|
killAllSessions(this.getLifecycleContext());
|
|
@@ -20809,8 +21177,8 @@ class SessionManager {
|
|
|
20809
21177
|
}
|
|
20810
21178
|
return session.sessionAllowedUsers.has(username) || session.platform.isUserAllowed(username);
|
|
20811
21179
|
}
|
|
20812
|
-
async startSessionWithWorktree(options, branch, username, replyToPostId, platformId = "default") {
|
|
20813
|
-
await this.startSession(options, username, replyToPostId, platformId);
|
|
21180
|
+
async startSessionWithWorktree(options, branch, username, replyToPostId, platformId = "default", displayName) {
|
|
21181
|
+
await this.startSession(options, username, replyToPostId, platformId, displayName);
|
|
20814
21182
|
const threadId = replyToPostId || "";
|
|
20815
21183
|
const session = this.sessions.get(this.getSessionId(platformId, threadId));
|
|
20816
21184
|
if (session) {
|
|
@@ -21105,10 +21473,10 @@ Release notes not available. See [GitHub releases](https://github.com/anneschuth
|
|
|
21105
21473
|
if (branchMatch) {
|
|
21106
21474
|
const branch = branchMatch[1];
|
|
21107
21475
|
const cleanedPrompt = prompt.replace(/(?:on branch|!worktree)\s+\S+/i, "").trim();
|
|
21108
|
-
await session.startSessionWithWorktree({ prompt: cleanedPrompt || prompt, files }, branch, username, threadRoot);
|
|
21476
|
+
await session.startSessionWithWorktree({ prompt: cleanedPrompt || prompt, files }, branch, username, threadRoot, platformConfig.id, user?.displayName);
|
|
21109
21477
|
return;
|
|
21110
21478
|
}
|
|
21111
|
-
await session.startSession({ prompt, files }, username, threadRoot);
|
|
21479
|
+
await session.startSession({ prompt, files }, username, threadRoot, platformConfig.id, user?.displayName);
|
|
21112
21480
|
} catch (err) {
|
|
21113
21481
|
console.error(" \u274C Error handling message:", err);
|
|
21114
21482
|
try {
|