claude-threads 0.17.0 → 0.18.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/dist/index.js +296 -12
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,7 +5,35 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.18.0] - 2026-01-01
9
+
10
+ ### Added
11
+ - **Keep-alive support** - Prevents system sleep while Claude sessions are active
12
+ - Automatically starts when first session begins, stops when all sessions end
13
+ - Cross-platform: macOS (`caffeinate`), Linux (`systemd-inhibit`), Windows (`SetThreadExecutionState`)
14
+ - Enabled by default, disable with `--no-keep-alive` CLI flag or `keepAlive: false` in config
15
+ - Shows `☕ Keep-alive enabled` in startup output
16
+ - **Resume timed-out sessions via emoji reaction** - React with 🔄 to the timeout message or session header to resume a timed-out session
17
+ - Timeout message now shows resume hint: "💡 React with 🔄 to resume, or send a new message to continue."
18
+ - Resume also works by sending a new message in the thread (existing behavior)
19
+ - Session header now displays truncated session ID for reference
20
+ - Supports multiple resume emojis: 🔄 (arrows_counterclockwise), ▶️ (arrow_forward), 🔁 (repeat)
21
+
22
+ ### Fixed
23
+ - **Sticky task list**: Task list now correctly stops being sticky when all tasks are completed
24
+ - Previously, the task list stayed at the bottom even after all tasks had `status: 'completed'`
25
+ - Now properly detects when all tasks are done using `todos.every(t => t.status === 'completed')`
26
+
27
+ ## [0.17.1] - 2025-12-31
28
+
29
+ ### Fixed
30
+ - **Sticky task list optimization**: Completed task lists no longer move to the bottom
31
+ - Once all tasks are done, the "~~Tasks~~ *(completed)*" message stays in place
32
+ - Reduces unnecessary message deletions and recreations
33
+ - Added `tasksCompleted` flag to session state for explicit tracking
34
+
35
+ ### Changed
36
+ - **Task list visual separator**: Added horizontal rule (`---`) above task list for better visibility
9
37
 
10
38
  ## [0.17.0] - 2025-12-31
11
39
 
package/dist/index.js CHANGED
@@ -13694,6 +13694,22 @@ class SessionStore {
13694
13694
  console.log(" [persist] Cleared all sessions");
13695
13695
  }
13696
13696
  }
13697
+ findByThread(platformId, threadId) {
13698
+ const sessionId = `${platformId}:${threadId}`;
13699
+ const data = this.loadRaw();
13700
+ return data.sessions[sessionId];
13701
+ }
13702
+ findByPostId(platformId, postId) {
13703
+ const data = this.loadRaw();
13704
+ for (const session of Object.values(data.sessions)) {
13705
+ if (session.platformId !== platformId)
13706
+ continue;
13707
+ if (session.timeoutPostId === postId || session.sessionStartPostId === postId) {
13708
+ return session;
13709
+ }
13710
+ }
13711
+ return;
13712
+ }
13697
13713
  loadRaw() {
13698
13714
  if (!existsSync3(SESSIONS_FILE)) {
13699
13715
  return { version: STORE_VERSION, sessions: {} };
@@ -13718,6 +13734,7 @@ var ALLOW_ALL_EMOJIS = ["white_check_mark", "heavy_check_mark"];
13718
13734
  var NUMBER_EMOJIS = ["one", "two", "three", "four"];
13719
13735
  var CANCEL_EMOJIS = ["x", "octagonal_sign", "stop_sign"];
13720
13736
  var ESCAPE_EMOJIS = ["double_vertical_bar", "pause_button"];
13737
+ var RESUME_EMOJIS = ["arrows_counterclockwise", "arrow_forward", "repeat"];
13721
13738
  function isApprovalEmoji(emoji) {
13722
13739
  return APPROVAL_EMOJIS.includes(emoji);
13723
13740
  }
@@ -13733,6 +13750,9 @@ function isCancelEmoji(emoji) {
13733
13750
  function isEscapeEmoji(emoji) {
13734
13751
  return ESCAPE_EMOJIS.includes(emoji);
13735
13752
  }
13753
+ function isResumeEmoji(emoji) {
13754
+ return RESUME_EMOJIS.includes(emoji);
13755
+ }
13736
13756
  var UNICODE_NUMBER_EMOJIS = {
13737
13757
  "1\uFE0F\u20E3": 0,
13738
13758
  "2\uFE0F\u20E3": 1,
@@ -13823,6 +13843,9 @@ async function bumpTasksToBottom(session) {
13823
13843
  if (!session.tasksPostId || !session.lastTasksContent) {
13824
13844
  return;
13825
13845
  }
13846
+ if (session.tasksCompleted) {
13847
+ return;
13848
+ }
13826
13849
  try {
13827
13850
  await session.platform.deletePost(session.tasksPostId);
13828
13851
  const newPost = await session.platform.createPost(session.lastTasksContent, session.threadId);
@@ -13853,7 +13876,8 @@ async function flush(session, registerPost) {
13853
13876
  session.currentPostId = null;
13854
13877
  session.pendingContent = remainder;
13855
13878
  if (remainder) {
13856
- if (session.tasksPostId && session.lastTasksContent) {
13879
+ const hasActiveTasks = session.tasksPostId && session.lastTasksContent && !session.tasksCompleted;
13880
+ if (hasActiveTasks) {
13857
13881
  const postId = await bumpTasksToBottomWithContent(session, `*(continued)*
13858
13882
 
13859
13883
  ` + remainder, registerPost);
@@ -13876,7 +13900,8 @@ async function flush(session, registerPost) {
13876
13900
  if (session.currentPostId) {
13877
13901
  await session.platform.updatePost(session.currentPostId, content);
13878
13902
  } else {
13879
- if (session.tasksPostId && session.lastTasksContent) {
13903
+ const hasActiveTasks = session.tasksPostId && session.lastTasksContent && !session.tasksCompleted;
13904
+ if (hasActiveTasks) {
13880
13905
  const postId = await bumpTasksToBottomWithContent(session, content, registerPost);
13881
13906
  session.currentPostId = postId;
13882
13907
  } else {
@@ -14843,9 +14868,11 @@ async function handleExitPlanMode(session, toolUseId, ctx) {
14843
14868
  async function handleTodoWrite(session, input) {
14844
14869
  const todos = input.todos;
14845
14870
  if (!todos || todos.length === 0) {
14871
+ session.tasksCompleted = true;
14846
14872
  if (session.tasksPostId) {
14847
14873
  try {
14848
- const completedMsg = "\uD83D\uDCCB ~~Tasks~~ *(completed)*";
14874
+ const completedMsg = `---
14875
+ \uD83D\uDCCB ~~Tasks~~ *(completed)*`;
14849
14876
  await session.platform.updatePost(session.tasksPostId, completedMsg);
14850
14877
  session.lastTasksContent = completedMsg;
14851
14878
  } catch (err) {
@@ -14854,6 +14881,8 @@ async function handleTodoWrite(session, input) {
14854
14881
  }
14855
14882
  return;
14856
14883
  }
14884
+ const allCompleted = todos.every((t) => t.status === "completed");
14885
+ session.tasksCompleted = allCompleted;
14857
14886
  const completed = todos.filter((t) => t.status === "completed").length;
14858
14887
  const total = todos.length;
14859
14888
  const pct = Math.round(completed / total * 100);
@@ -14863,7 +14892,8 @@ async function handleTodoWrite(session, input) {
14863
14892
  } else if (!hasInProgress) {
14864
14893
  session.inProgressTaskStart = null;
14865
14894
  }
14866
- let message = `\uD83D\uDCCB **Tasks** (${completed}/${total} \xB7 ${pct}%)
14895
+ let message = `---
14896
+ \uD83D\uDCCB **Tasks** (${completed}/${total} \xB7 ${pct}%)
14867
14897
 
14868
14898
  `;
14869
14899
  for (const todo of todos) {
@@ -18939,6 +18969,7 @@ async function updateSessionHeader(session, ctx) {
18939
18969
  rows.push(`| \uD83D\uDC65 **Participants** | ${otherParticipants} |`);
18940
18970
  }
18941
18971
  rows.push(`| \uD83D\uDD22 **Session** | #${session.sessionNumber} of ${ctx.maxSessions} max |`);
18972
+ rows.push(`| \uD83C\uDD94 **Session ID** | \`${session.claudeSessionId.substring(0, 8)}\` |`);
18942
18973
  rows.push(`| ${permMode.split(" ")[0]} **Permissions** | ${permMode.split(" ")[1]} |`);
18943
18974
  if (ctx.chromeEnabled) {
18944
18975
  rows.push(`| \uD83C\uDF10 **Chrome** | Enabled |`);
@@ -18971,6 +19002,196 @@ async function updateSessionHeader(session, ctx) {
18971
19002
  // src/session/lifecycle.ts
18972
19003
  import { randomUUID as randomUUID2 } from "crypto";
18973
19004
  import { existsSync as existsSync7 } from "fs";
19005
+
19006
+ // src/utils/keep-alive.ts
19007
+ import { spawn as spawn3 } from "child_process";
19008
+ var logger = createLogger("[keep-alive]");
19009
+
19010
+ class KeepAliveManager {
19011
+ activeSessionCount = 0;
19012
+ keepAliveProcess = null;
19013
+ enabled = true;
19014
+ platform;
19015
+ constructor() {
19016
+ this.platform = process.platform;
19017
+ }
19018
+ setEnabled(enabled) {
19019
+ this.enabled = enabled;
19020
+ if (!enabled && this.keepAliveProcess) {
19021
+ this.stopKeepAlive();
19022
+ }
19023
+ logger.debug(`Keep-alive ${enabled ? "enabled" : "disabled"}`);
19024
+ }
19025
+ isEnabled() {
19026
+ return this.enabled;
19027
+ }
19028
+ isActive() {
19029
+ return this.keepAliveProcess !== null;
19030
+ }
19031
+ sessionStarted() {
19032
+ this.activeSessionCount++;
19033
+ logger.debug(`Session started (${this.activeSessionCount} active)`);
19034
+ if (this.activeSessionCount === 1) {
19035
+ this.startKeepAlive();
19036
+ }
19037
+ }
19038
+ sessionEnded() {
19039
+ if (this.activeSessionCount > 0) {
19040
+ this.activeSessionCount--;
19041
+ }
19042
+ logger.debug(`Session ended (${this.activeSessionCount} active)`);
19043
+ if (this.activeSessionCount === 0) {
19044
+ this.stopKeepAlive();
19045
+ }
19046
+ }
19047
+ forceStop() {
19048
+ this.stopKeepAlive();
19049
+ this.activeSessionCount = 0;
19050
+ }
19051
+ getSessionCount() {
19052
+ return this.activeSessionCount;
19053
+ }
19054
+ startKeepAlive() {
19055
+ if (!this.enabled) {
19056
+ logger.debug("Keep-alive disabled, skipping");
19057
+ return;
19058
+ }
19059
+ if (this.keepAliveProcess) {
19060
+ logger.debug("Keep-alive already running");
19061
+ return;
19062
+ }
19063
+ switch (this.platform) {
19064
+ case "darwin":
19065
+ this.startMacOSKeepAlive();
19066
+ break;
19067
+ case "linux":
19068
+ this.startLinuxKeepAlive();
19069
+ break;
19070
+ case "win32":
19071
+ this.startWindowsKeepAlive();
19072
+ break;
19073
+ default:
19074
+ logger.info(`Keep-alive not supported on ${this.platform}`);
19075
+ }
19076
+ }
19077
+ stopKeepAlive() {
19078
+ if (this.keepAliveProcess) {
19079
+ logger.debug("Stopping keep-alive");
19080
+ this.keepAliveProcess.kill();
19081
+ this.keepAliveProcess = null;
19082
+ }
19083
+ }
19084
+ startMacOSKeepAlive() {
19085
+ try {
19086
+ this.keepAliveProcess = spawn3("caffeinate", ["-s", "-i"], {
19087
+ stdio: "ignore",
19088
+ detached: false
19089
+ });
19090
+ this.keepAliveProcess.on("error", (err) => {
19091
+ logger.error(`Failed to start caffeinate: ${err.message}`);
19092
+ this.keepAliveProcess = null;
19093
+ });
19094
+ this.keepAliveProcess.on("exit", (code) => {
19095
+ if (code !== null && code !== 0 && this.activeSessionCount > 0) {
19096
+ logger.debug(`caffeinate exited with code ${code}`);
19097
+ }
19098
+ this.keepAliveProcess = null;
19099
+ });
19100
+ logger.info("System sleep prevention started (caffeinate)");
19101
+ } catch (err) {
19102
+ logger.error(`Failed to start caffeinate: ${err}`);
19103
+ }
19104
+ }
19105
+ startLinuxKeepAlive() {
19106
+ try {
19107
+ this.keepAliveProcess = spawn3("systemd-inhibit", [
19108
+ "--what=sleep:idle:handle-lid-switch",
19109
+ "--why=Claude Code session active",
19110
+ "--mode=block",
19111
+ "sleep",
19112
+ "infinity"
19113
+ ], {
19114
+ stdio: "ignore",
19115
+ detached: false
19116
+ });
19117
+ this.keepAliveProcess.on("error", (err) => {
19118
+ logger.debug(`systemd-inhibit not available: ${err.message}`);
19119
+ this.keepAliveProcess = null;
19120
+ this.startLinuxKeepAliveFallback();
19121
+ });
19122
+ this.keepAliveProcess.on("exit", (code) => {
19123
+ if (code !== null && code !== 0 && this.activeSessionCount > 0) {
19124
+ logger.debug(`systemd-inhibit exited with code ${code}`);
19125
+ }
19126
+ this.keepAliveProcess = null;
19127
+ });
19128
+ logger.info("System sleep prevention started (systemd-inhibit)");
19129
+ } catch (err) {
19130
+ logger.debug(`Failed to start systemd-inhibit: ${err}`);
19131
+ this.startLinuxKeepAliveFallback();
19132
+ }
19133
+ }
19134
+ startLinuxKeepAliveFallback() {
19135
+ try {
19136
+ this.keepAliveProcess = spawn3("bash", [
19137
+ "-c",
19138
+ `while true; do xdg-screensaver reset 2>/dev/null || true; sleep 60; done`
19139
+ ], {
19140
+ stdio: "ignore",
19141
+ detached: false
19142
+ });
19143
+ this.keepAliveProcess.on("error", (err) => {
19144
+ logger.info(`Linux keep-alive fallback not available: ${err.message}`);
19145
+ this.keepAliveProcess = null;
19146
+ });
19147
+ this.keepAliveProcess.on("exit", () => {
19148
+ this.keepAliveProcess = null;
19149
+ });
19150
+ logger.info("System sleep prevention started (xdg-screensaver reset loop)");
19151
+ } catch (err) {
19152
+ logger.info(`Linux keep-alive not available: ${err}`);
19153
+ }
19154
+ }
19155
+ startWindowsKeepAlive() {
19156
+ try {
19157
+ const script = `
19158
+ Add-Type -TypeDefinition @"
19159
+ using System;
19160
+ using System.Runtime.InteropServices;
19161
+ public class PowerState {
19162
+ [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
19163
+ public static extern uint SetThreadExecutionState(uint esFlags);
19164
+ }
19165
+ "@
19166
+ # ES_CONTINUOUS | ES_SYSTEM_REQUIRED
19167
+ [PowerState]::SetThreadExecutionState(0x80000001) | Out-Null
19168
+ # Keep running until killed
19169
+ while ($true) { Start-Sleep -Seconds 60 }
19170
+ `;
19171
+ this.keepAliveProcess = spawn3("powershell", ["-NoProfile", "-Command", script], {
19172
+ stdio: "ignore",
19173
+ detached: false,
19174
+ windowsHide: true
19175
+ });
19176
+ this.keepAliveProcess.on("error", (err) => {
19177
+ logger.info(`Windows keep-alive not available: ${err.message}`);
19178
+ this.keepAliveProcess = null;
19179
+ });
19180
+ this.keepAliveProcess.on("exit", (code) => {
19181
+ if (code !== null && code !== 0 && this.activeSessionCount > 0) {
19182
+ logger.debug(`PowerShell keep-alive exited with code ${code}`);
19183
+ }
19184
+ this.keepAliveProcess = null;
19185
+ });
19186
+ logger.info("System sleep prevention started (SetThreadExecutionState)");
19187
+ } catch (err) {
19188
+ logger.info(`Windows keep-alive not available: ${err}`);
19189
+ }
19190
+ }
19191
+ }
19192
+ var keepAlive = new KeepAliveManager;
19193
+
19194
+ // src/session/lifecycle.ts
18974
19195
  function findPersistedByThreadId(persisted, threadId) {
18975
19196
  for (const session of persisted.values()) {
18976
19197
  if (session.threadId === threadId) {
@@ -19041,6 +19262,7 @@ async function startSession(options, username, replyToPostId, platformId, ctx) {
19041
19262
  sessionStartPostId: post.id,
19042
19263
  tasksPostId: null,
19043
19264
  lastTasksContent: null,
19265
+ tasksCompleted: false,
19044
19266
  activeSubagents: new Map,
19045
19267
  updateTimer: null,
19046
19268
  typingTimer: null,
@@ -19055,6 +19277,7 @@ async function startSession(options, username, replyToPostId, platformId, ctx) {
19055
19277
  ctx.registerPost(post.id, actualThreadId);
19056
19278
  const shortId = actualThreadId.substring(0, 8);
19057
19279
  console.log(` \u25B6 Session #${ctx.sessions.size} started (${shortId}\u2026) by @${username}`);
19280
+ keepAlive.sessionStarted();
19058
19281
  await ctx.updateSessionHeader(session);
19059
19282
  ctx.startTyping(session);
19060
19283
  claude.on("event", (e) => ctx.handleEvent(sessionId, e));
@@ -19153,6 +19376,7 @@ Please start a new session.`, state.threadId);
19153
19376
  sessionStartPostId: state.sessionStartPostId,
19154
19377
  tasksPostId: state.tasksPostId,
19155
19378
  lastTasksContent: state.lastTasksContent ?? null,
19379
+ tasksCompleted: state.tasksCompleted ?? false,
19156
19380
  activeSubagents: new Map,
19157
19381
  updateTimer: null,
19158
19382
  typingTimer: null,
@@ -19173,6 +19397,7 @@ Please start a new session.`, state.threadId);
19173
19397
  if (state.sessionStartPostId) {
19174
19398
  ctx.registerPost(state.sessionStartPostId, state.threadId);
19175
19399
  }
19400
+ keepAlive.sessionStarted();
19176
19401
  claude.on("event", (e) => ctx.handleEvent(sessionId, e));
19177
19402
  claude.on("exit", (code) => ctx.handleExit(sessionId, code));
19178
19403
  try {
@@ -19251,6 +19476,7 @@ async function handleExit(sessionId, code, ctx) {
19251
19476
  session.updateTimer = null;
19252
19477
  }
19253
19478
  ctx.sessions.delete(session.sessionId);
19479
+ keepAlive.sessionEnded();
19254
19480
  return;
19255
19481
  }
19256
19482
  if (session.wasInterrupted) {
@@ -19267,6 +19493,7 @@ async function handleExit(sessionId, code, ctx) {
19267
19493
  ctx.postIndex.delete(postId);
19268
19494
  }
19269
19495
  }
19496
+ keepAlive.sessionEnded();
19270
19497
  try {
19271
19498
  await session.platform.createPost(`\u2139\uFE0F Session paused. Send a new message to continue.`, session.threadId);
19272
19499
  } catch {}
@@ -19281,6 +19508,7 @@ async function handleExit(sessionId, code, ctx) {
19281
19508
  session.updateTimer = null;
19282
19509
  }
19283
19510
  ctx.sessions.delete(session.sessionId);
19511
+ keepAlive.sessionEnded();
19284
19512
  try {
19285
19513
  await session.platform.createPost(`\u26A0\uFE0F **Session resume failed** (exit code ${code}). The session data is preserved - try restarting the bot.`, session.threadId);
19286
19514
  } catch {}
@@ -19302,6 +19530,7 @@ async function handleExit(sessionId, code, ctx) {
19302
19530
  ctx.postIndex.delete(postId);
19303
19531
  }
19304
19532
  }
19533
+ keepAlive.sessionEnded();
19305
19534
  if (code === 0 || code === null) {
19306
19535
  ctx.unpersistSession(session.sessionId);
19307
19536
  } else {
@@ -19322,6 +19551,7 @@ function killSession(session, unpersist, ctx) {
19322
19551
  ctx.postIndex.delete(postId);
19323
19552
  }
19324
19553
  }
19554
+ keepAlive.sessionEnded();
19325
19555
  if (unpersist) {
19326
19556
  ctx.unpersistSession(session.threadId);
19327
19557
  }
@@ -19334,15 +19564,23 @@ function killAllSessions(ctx) {
19334
19564
  }
19335
19565
  ctx.sessions.clear();
19336
19566
  ctx.postIndex.clear();
19567
+ keepAlive.forceStop();
19337
19568
  }
19338
- function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
19569
+ async function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
19339
19570
  const now = Date.now();
19340
19571
  for (const [_sessionId, session] of ctx.sessions) {
19341
19572
  const idleMs = now - session.lastActivityAt.getTime();
19342
19573
  const shortId = session.threadId.substring(0, 8);
19343
19574
  if (idleMs > timeoutMs) {
19344
19575
  console.log(` \u23F0 Session (${shortId}\u2026) timed out after ${Math.round(idleMs / 60000)}min idle`);
19345
- session.platform.createPost(`\u23F0 **Session timed out** after ${Math.round(idleMs / 60000)} minutes of inactivity`, session.threadId).catch(() => {});
19576
+ try {
19577
+ const timeoutPost = await session.platform.createPost(`\u23F0 **Session timed out** after ${Math.round(idleMs / 60000)} minutes of inactivity
19578
+
19579
+ ` + `\uD83D\uDCA1 React with \uD83D\uDD04 to resume, or send a new message to continue.`, session.threadId);
19580
+ session.timeoutPostId = timeoutPost.id;
19581
+ ctx.persistSession(session);
19582
+ ctx.registerPost(timeoutPost.id, session.threadId);
19583
+ } catch {}
19346
19584
  killSession(session, false, ctx);
19347
19585
  continue;
19348
19586
  }
@@ -19357,13 +19595,13 @@ function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
19357
19595
  }
19358
19596
 
19359
19597
  // src/git/worktree.ts
19360
- import { spawn as spawn3 } from "child_process";
19598
+ import { spawn as spawn4 } from "child_process";
19361
19599
  import { randomUUID as randomUUID3 } from "crypto";
19362
19600
  import * as path9 from "path";
19363
19601
  import * as fs5 from "fs/promises";
19364
19602
  async function execGit(args, cwd) {
19365
19603
  return new Promise((resolve6, reject) => {
19366
- const proc = spawn3("git", args, { cwd });
19604
+ const proc = spawn4("git", args, { cwd });
19367
19605
  let stdout = "";
19368
19606
  let stderr = "";
19369
19607
  proc.stdout.on("data", (data) => {
@@ -19898,7 +20136,7 @@ class SessionManager {
19898
20136
  this.chromeEnabled = chromeEnabled;
19899
20137
  this.worktreeMode = worktreeMode;
19900
20138
  this.cleanupTimer = setInterval(() => {
19901
- cleanupIdleSessions(SESSION_TIMEOUT_MS, SESSION_WARNING_MS, this.getLifecycleContext());
20139
+ cleanupIdleSessions(SESSION_TIMEOUT_MS, SESSION_WARNING_MS, this.getLifecycleContext()).catch((err) => console.error(" [cleanup] Error during idle session cleanup:", err));
19902
20140
  }, 60000);
19903
20141
  }
19904
20142
  addPlatform(platformId, client) {
@@ -19993,6 +20231,11 @@ class SessionManager {
19993
20231
  }
19994
20232
  async handleMessage(_platformId, _post, _user) {}
19995
20233
  async handleReaction(platformId, postId, emojiName, username) {
20234
+ if (isResumeEmoji(emojiName)) {
20235
+ const resumed = await this.tryResumeFromReaction(platformId, postId, username);
20236
+ if (resumed)
20237
+ return;
20238
+ }
19996
20239
  const session = this.getSessionByPost(postId);
19997
20240
  if (!session)
19998
20241
  return;
@@ -20003,6 +20246,36 @@ class SessionManager {
20003
20246
  }
20004
20247
  await this.handleSessionReaction(session, postId, emojiName, username);
20005
20248
  }
20249
+ async tryResumeFromReaction(platformId, postId, username) {
20250
+ const persistedSession = this.sessionStore.findByPostId(platformId, postId);
20251
+ if (!persistedSession)
20252
+ return false;
20253
+ const sessionId = `${platformId}:${persistedSession.threadId}`;
20254
+ if (this.sessions.has(sessionId)) {
20255
+ if (this.debug) {
20256
+ console.log(` [resume] Session already active for ${persistedSession.threadId.substring(0, 8)}...`);
20257
+ }
20258
+ return false;
20259
+ }
20260
+ const allowedUsers = new Set(persistedSession.sessionAllowedUsers);
20261
+ const platform = this.platforms.get(platformId);
20262
+ if (!allowedUsers.has(username) && !platform?.isUserAllowed(username)) {
20263
+ if (platform) {
20264
+ await platform.createPost(`\u26A0\uFE0F @${username} is not authorized to resume this session`, persistedSession.threadId);
20265
+ }
20266
+ return false;
20267
+ }
20268
+ if (this.sessions.size >= MAX_SESSIONS) {
20269
+ if (platform) {
20270
+ await platform.createPost(`\u26A0\uFE0F **Too busy** - ${this.sessions.size} sessions active. Please try again later.`, persistedSession.threadId);
20271
+ }
20272
+ return false;
20273
+ }
20274
+ const shortId = persistedSession.threadId.substring(0, 8);
20275
+ console.log(` \uD83D\uDD04 Resuming session ${shortId}... via emoji reaction by @${username}`);
20276
+ await resumeSession(persistedSession, this.getLifecycleContext());
20277
+ return true;
20278
+ }
20006
20279
  async handleSessionReaction(session, postId, emojiName, username) {
20007
20280
  if (session.worktreePromptPostId === postId && emojiName === "x") {
20008
20281
  await handleWorktreeSkip(session, username, (s) => this.persistSession(s), (s, q) => this.offerContextPrompt(s, q));
@@ -20178,13 +20451,15 @@ class SessionManager {
20178
20451
  sessionStartPostId: session.sessionStartPostId,
20179
20452
  tasksPostId: session.tasksPostId,
20180
20453
  lastTasksContent: session.lastTasksContent,
20454
+ tasksCompleted: session.tasksCompleted,
20181
20455
  worktreeInfo: session.worktreeInfo,
20182
20456
  pendingWorktreePrompt: session.pendingWorktreePrompt,
20183
20457
  worktreePromptDisabled: session.worktreePromptDisabled,
20184
20458
  queuedPrompt: session.queuedPrompt,
20185
20459
  firstPrompt: session.firstPrompt,
20186
20460
  pendingContextPrompt: persistedContextPrompt,
20187
- needsContextPromptOnNextMessage: session.needsContextPromptOnNextMessage
20461
+ needsContextPromptOnNextMessage: session.needsContextPromptOnNextMessage,
20462
+ timeoutPostId: session.timeoutPostId
20188
20463
  };
20189
20464
  this.sessionStore.save(session.sessionId, state);
20190
20465
  }
@@ -20426,7 +20701,7 @@ class SessionManager {
20426
20701
  var dim2 = (s) => `\x1B[2m${s}\x1B[0m`;
20427
20702
  var bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
20428
20703
  var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
20429
- program.name("claude-threads").version(VERSION).description("Share Claude Code sessions in Mattermost").option("--url <url>", "Mattermost server URL").option("--token <token>", "Mattermost bot token").option("--channel <id>", "Mattermost channel ID").option("--bot-name <name>", "Bot mention name (default: claude-code)").option("--allowed-users <users>", "Comma-separated allowed usernames").option("--skip-permissions", "Skip interactive permission prompts").option("--no-skip-permissions", "Enable interactive permission prompts (override env)").option("--chrome", "Enable Claude in Chrome integration").option("--no-chrome", "Disable Claude in Chrome integration").option("--worktree-mode <mode>", "Git worktree mode: off, prompt, require (default: prompt)").option("--setup", "Run interactive setup wizard (reconfigure existing settings)").option("--debug", "Enable debug logging").parse();
20704
+ program.name("claude-threads").version(VERSION).description("Share Claude Code sessions in Mattermost").option("--url <url>", "Mattermost server URL").option("--token <token>", "Mattermost bot token").option("--channel <id>", "Mattermost channel ID").option("--bot-name <name>", "Bot mention name (default: claude-code)").option("--allowed-users <users>", "Comma-separated allowed usernames").option("--skip-permissions", "Skip interactive permission prompts").option("--no-skip-permissions", "Enable interactive permission prompts (override env)").option("--chrome", "Enable Claude in Chrome integration").option("--no-chrome", "Disable Claude in Chrome integration").option("--worktree-mode <mode>", "Git worktree mode: off, prompt, require (default: prompt)").option("--keep-alive", "Enable system sleep prevention (default: enabled)").option("--no-keep-alive", "Disable system sleep prevention").option("--setup", "Run interactive setup wizard (reconfigure existing settings)").option("--debug", "Enable debug logging").parse();
20430
20705
  var opts = program.opts();
20431
20706
  function hasRequiredCliArgs(args) {
20432
20707
  return !!(args.url && args.token && args.channel);
@@ -20444,7 +20719,8 @@ async function main() {
20444
20719
  allowedUsers: opts.allowedUsers,
20445
20720
  skipPermissions: opts.skipPermissions,
20446
20721
  chrome: opts.chrome,
20447
- worktreeMode: opts.worktreeMode
20722
+ worktreeMode: opts.worktreeMode,
20723
+ keepAlive: opts.keepAlive
20448
20724
  };
20449
20725
  if (opts.setup) {
20450
20726
  await runOnboarding(true);
@@ -20462,6 +20738,11 @@ async function main() {
20462
20738
  if (cliArgs.worktreeMode !== undefined) {
20463
20739
  newConfig.worktreeMode = cliArgs.worktreeMode;
20464
20740
  }
20741
+ if (cliArgs.keepAlive !== undefined) {
20742
+ newConfig.keepAlive = cliArgs.keepAlive;
20743
+ }
20744
+ const keepAliveEnabled = newConfig.keepAlive !== false;
20745
+ keepAlive.setEnabled(keepAliveEnabled);
20465
20746
  const platformConfig = newConfig.platforms.find((p) => p.type === "mattermost");
20466
20747
  if (!platformConfig) {
20467
20748
  throw new Error("No Mattermost platform configured.");
@@ -20481,6 +20762,9 @@ async function main() {
20481
20762
  if (config.chrome) {
20482
20763
  console.log(` \uD83C\uDF10 ${dim2("Chrome integration enabled")}`);
20483
20764
  }
20765
+ if (keepAliveEnabled) {
20766
+ console.log(` \u2615 ${dim2("Keep-alive enabled")}`);
20767
+ }
20484
20768
  console.log("");
20485
20769
  const mattermost = new MattermostClient(platformConfig);
20486
20770
  const session = new SessionManager(workingDir, platformConfig.skipPermissions, config.chrome, config.worktreeMode);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Share Claude Code sessions live in a Mattermost channel with interactive features",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",