claude-threads 0.42.0 → 0.43.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 CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.43.0] - 2026-01-07
11
+
12
+ ### Added
13
+ - **Auto-restart on updates** - Bot automatically restarts after installing updates when running with daemon wrapper
14
+ - **`!update` command family** - Check update status, force immediate update (`!update now`), or defer (`!update defer`)
15
+ - **`--auto-restart` / `--no-auto-restart` CLI flags** - Control auto-restart behavior (enabled by default when `autoUpdate.enabled`)
16
+
17
+ ### Changed
18
+ - **Platform-specific formatting for update messages** - Update notifications now use proper bold/italic formatting per platform (Mattermost vs Slack)
19
+ - **Improved daemon wrapper** - Now correctly uses local binary instead of global installation
20
+
10
21
  ## [0.42.0] - 2026-01-07
11
22
 
12
23
  ### Fixed
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # claude-threads-daemon - Auto-restart wrapper for claude-threads
4
+ #
5
+ # This script runs claude-threads and automatically restarts it when
6
+ # it exits with code 42 (the special "update restart" signal).
7
+ #
8
+ # For other exit codes:
9
+ # - 0: Clean exit, don't restart
10
+ # - 1+: Error, optional restart based on flags
11
+ #
12
+ # Usage:
13
+ # claude-threads-daemon [options]
14
+ # claude-threads-daemon --restart-on-error # Also restart on errors
15
+ # claude-threads-daemon --max-restarts 10 # Limit restart count
16
+ #
17
+ # Environment variables:
18
+ # CLAUDE_THREADS_MAX_RESTARTS - Maximum restart attempts (default: unlimited)
19
+ # CLAUDE_THREADS_RESTART_DELAY - Delay between restarts in seconds (default: 2)
20
+ #
21
+
22
+ set -e
23
+
24
+ # Exit code that signals "restart for update"
25
+ RESTART_EXIT_CODE=42
26
+
27
+ # Configuration
28
+ MAX_RESTARTS=${CLAUDE_THREADS_MAX_RESTARTS:-0} # 0 = unlimited
29
+ RESTART_DELAY=${CLAUDE_THREADS_RESTART_DELAY:-2}
30
+ RESTART_ON_ERROR=false
31
+ VERBOSE=false
32
+
33
+ # Path to claude-threads binary (set by index.ts when spawning daemon)
34
+ # Falls back to global 'claude-threads' command if not set
35
+ # If CLAUDE_THREADS_BIN is a .js file, run it with bun
36
+ if [ -n "$CLAUDE_THREADS_BIN" ]; then
37
+ if [[ "$CLAUDE_THREADS_BIN" == *.js ]]; then
38
+ CLAUDE_THREADS_CMD="bun $CLAUDE_THREADS_BIN"
39
+ else
40
+ CLAUDE_THREADS_CMD="$CLAUDE_THREADS_BIN"
41
+ fi
42
+ else
43
+ CLAUDE_THREADS_CMD="claude-threads"
44
+ fi
45
+
46
+ # Track restart count
47
+ restart_count=0
48
+
49
+ # Parse arguments
50
+ while [[ $# -gt 0 ]]; do
51
+ case $1 in
52
+ --restart-on-error)
53
+ RESTART_ON_ERROR=true
54
+ shift
55
+ ;;
56
+ --max-restarts)
57
+ MAX_RESTARTS="$2"
58
+ shift 2
59
+ ;;
60
+ --restart-delay)
61
+ RESTART_DELAY="$2"
62
+ shift 2
63
+ ;;
64
+ --verbose|-v)
65
+ VERBOSE=true
66
+ shift
67
+ ;;
68
+ --help|-h)
69
+ echo "claude-threads-daemon - Auto-restart wrapper for claude-threads"
70
+ echo ""
71
+ echo "Usage: claude-threads-daemon [options]"
72
+ echo ""
73
+ echo "Options:"
74
+ echo " --restart-on-error Also restart on non-zero exit codes"
75
+ echo " --max-restarts N Maximum number of restarts (default: unlimited)"
76
+ echo " --restart-delay N Seconds to wait between restarts (default: 2)"
77
+ echo " --verbose, -v Print debug information"
78
+ echo " --help, -h Show this help message"
79
+ echo ""
80
+ echo "Exit codes:"
81
+ echo " 0 Clean exit, no restart"
82
+ echo " 42 Update restart signal (always restarts)"
83
+ echo " other Error (restarts only with --restart-on-error)"
84
+ exit 0
85
+ ;;
86
+ *)
87
+ # Pass through to claude-threads
88
+ break
89
+ ;;
90
+ esac
91
+ done
92
+
93
+ log() {
94
+ if [ "$VERBOSE" = true ]; then
95
+ echo "[daemon] $*" >&2
96
+ fi
97
+ }
98
+
99
+ log "Starting claude-threads daemon"
100
+ log "Binary: $CLAUDE_THREADS_CMD"
101
+ log "Max restarts: ${MAX_RESTARTS:-unlimited}"
102
+ log "Restart delay: ${RESTART_DELAY}s"
103
+ log "Restart on error: $RESTART_ON_ERROR"
104
+
105
+ while true; do
106
+ log "Starting claude-threads (restart #$restart_count)..."
107
+
108
+ # Run claude-threads with any remaining arguments
109
+ # Note: CLAUDE_THREADS_CMD may be "bun /path/to/file.js" so we use eval
110
+ set +e
111
+ eval $CLAUDE_THREADS_CMD '"$@"'
112
+ exit_code=$?
113
+ set -e
114
+
115
+ log "claude-threads exited with code $exit_code"
116
+
117
+ # Check if we should restart
118
+ should_restart=false
119
+
120
+ if [ $exit_code -eq $RESTART_EXIT_CODE ]; then
121
+ # Exit code 42 = restart for update
122
+ log "Received update restart signal"
123
+ should_restart=true
124
+ elif [ $exit_code -eq 0 ]; then
125
+ # Clean exit
126
+ log "Clean exit, not restarting"
127
+ should_restart=false
128
+ elif [ "$RESTART_ON_ERROR" = true ]; then
129
+ # Error exit with restart-on-error enabled
130
+ log "Error exit, restarting due to --restart-on-error"
131
+ should_restart=true
132
+ fi
133
+
134
+ if [ "$should_restart" = false ]; then
135
+ log "Exiting daemon with code $exit_code"
136
+ exit $exit_code
137
+ fi
138
+
139
+ # Check max restarts
140
+ if [ $MAX_RESTARTS -gt 0 ]; then
141
+ restart_count=$((restart_count + 1))
142
+ if [ $restart_count -ge $MAX_RESTARTS ]; then
143
+ echo "[daemon] Maximum restart count ($MAX_RESTARTS) reached, exiting" >&2
144
+ exit 1
145
+ fi
146
+ fi
147
+
148
+ # Wait before restart
149
+ log "Waiting ${RESTART_DELAY}s before restart..."
150
+ sleep "$RESTART_DELAY"
151
+ done
package/dist/index.js CHANGED
@@ -51460,6 +51460,74 @@ async function updateSessionHeader(session, ctx) {
51460
51460
  const postId = session.sessionStartPostId;
51461
51461
  await withErrorHandling(() => session.platform.updatePost(postId, msg), { action: "Update session header", session });
51462
51462
  }
51463
+ async function showUpdateStatus(session, updateManager) {
51464
+ const formatter = session.platform.getFormatter();
51465
+ if (!updateManager) {
51466
+ await postInfo(session, `Auto-update is not available`);
51467
+ return;
51468
+ }
51469
+ if (!updateManager.isEnabled()) {
51470
+ await postInfo(session, `Auto-update is disabled in configuration`);
51471
+ return;
51472
+ }
51473
+ const updateInfo = await updateManager.checkNow();
51474
+ if (!updateInfo || !updateInfo.available) {
51475
+ await postSuccess(session, `${formatter.formatBold("Up to date")} - no updates available`);
51476
+ return;
51477
+ }
51478
+ const scheduledAt = updateManager.getScheduledRestartAt();
51479
+ const config = updateManager.getConfig();
51480
+ let statusLine;
51481
+ if (scheduledAt) {
51482
+ const secondsRemaining = Math.max(0, Math.round((scheduledAt.getTime() - Date.now()) / 1000));
51483
+ statusLine = `Restarting in ${secondsRemaining} seconds`;
51484
+ } else {
51485
+ statusLine = `Mode: ${config.autoRestartMode}`;
51486
+ }
51487
+ await postInfo(session, `\uD83D\uDD04 ${formatter.formatBold("Update available")}
51488
+
51489
+ ` + `Current: v${updateInfo.currentVersion}
51490
+ ` + `Latest: v${updateInfo.latestVersion}
51491
+ ` + `${statusLine}
51492
+
51493
+ ` + `Commands:
51494
+ ` + `\u2022 ${formatter.formatCode("!update now")} - Update immediately
51495
+ ` + `\u2022 ${formatter.formatCode("!update defer")} - Defer for 1 hour`);
51496
+ }
51497
+ async function forceUpdateNow(session, username, updateManager) {
51498
+ if (!await requireSessionOwner(session, username, "force updates")) {
51499
+ return;
51500
+ }
51501
+ const formatter = session.platform.getFormatter();
51502
+ if (!updateManager) {
51503
+ await postWarning(session, `Auto-update is not available`);
51504
+ return;
51505
+ }
51506
+ if (!updateManager.hasUpdate()) {
51507
+ await postInfo(session, `No update available to install`);
51508
+ return;
51509
+ }
51510
+ await postInfo(session, `\uD83D\uDD04 ${formatter.formatBold("Forcing update")} - restarting shortly...
51511
+ ` + formatter.formatItalic("Sessions will resume automatically"));
51512
+ await updateManager.forceUpdate();
51513
+ }
51514
+ async function deferUpdate(session, username, updateManager) {
51515
+ if (!await requireSessionOwner(session, username, "defer updates")) {
51516
+ return;
51517
+ }
51518
+ const formatter = session.platform.getFormatter();
51519
+ if (!updateManager) {
51520
+ await postWarning(session, `Auto-update is not available`);
51521
+ return;
51522
+ }
51523
+ if (!updateManager.hasUpdate()) {
51524
+ await postInfo(session, `No pending update to defer`);
51525
+ return;
51526
+ }
51527
+ updateManager.deferUpdate(60);
51528
+ await postSuccess(session, `\u23F8\uFE0F ${formatter.formatBold("Update deferred")} for 1 hour
51529
+ ` + formatter.formatItalic("Use !update now to apply earlier"));
51530
+ }
51463
51531
 
51464
51532
  // src/session/lifecycle.ts
51465
51533
  import { randomUUID as randomUUID3 } from "crypto";
@@ -51529,6 +51597,9 @@ Users can control sessions with these commands:
51529
51597
  - \`!kick @user\`: Remove a user from the session
51530
51598
  - \`!cd /path\`: Change working directory (restarts the session)
51531
51599
  - \`!permissions interactive|skip\`: Toggle permission prompts
51600
+ - \`!update\`: Show auto-update status
51601
+ - \`!update now\`: Apply pending update immediately
51602
+ - \`!update defer\`: Defer pending update for 1 hour
51532
51603
 
51533
51604
  SESSION METADATA: At the START of your first response, include metadata about this session:
51534
51605
 
@@ -52848,6 +52919,18 @@ async function buildStatusBar(sessionCount, config, formatter, platformId) {
52848
52919
  if (pausedPlatforms.get(platformId)) {
52849
52920
  items.push(formatter.formatCode("\u23F8\uFE0F Platform paused"));
52850
52921
  }
52922
+ if (config.updateStatus?.available) {
52923
+ const status = config.updateStatus;
52924
+ if (status.countdownSeconds !== undefined && status.countdownSeconds > 0) {
52925
+ items.push(formatter.formatCode(`\uD83D\uDD04 Restarting in ${status.countdownSeconds}s`));
52926
+ } else if (status.status === "installing") {
52927
+ items.push(formatter.formatCode(`\uD83D\uDCE6 Installing v${status.latestVersion}...`));
52928
+ } else if (status.status === "available") {
52929
+ items.push(formatter.formatCode(`\uD83C\uDD95 Update: v${status.latestVersion}`));
52930
+ } else if (status.status === "deferred") {
52931
+ items.push(formatter.formatCode(`\u23F8\uFE0F Update deferred`));
52932
+ }
52933
+ }
52851
52934
  const claudeVersion = getClaudeCliVersion();
52852
52935
  const versionStr = claudeVersion ? `v${VERSION} \xB7 CLI ${claudeVersion}` : `v${VERSION}`;
52853
52936
  items.push(formatter.formatCode(versionStr));
@@ -53143,6 +53226,7 @@ class SessionManager extends EventEmitter4 {
53143
53226
  sessionStore;
53144
53227
  cleanupTimer = null;
53145
53228
  isShuttingDown = false;
53229
+ autoUpdateManager = null;
53146
53230
  constructor(workingDir, skipPermissions = false, chromeEnabled = false, worktreeMode = "prompt", sessionsPath) {
53147
53231
  super();
53148
53232
  this.workingDir = workingDir;
@@ -53179,6 +53263,9 @@ class SessionManager extends EventEmitter4 {
53179
53263
  removePlatform(platformId) {
53180
53264
  this.platforms.delete(platformId);
53181
53265
  }
53266
+ setAutoUpdateManager(manager) {
53267
+ this.autoUpdateManager = manager;
53268
+ }
53182
53269
  registerWorktreeUser(worktreePath, sessionId) {
53183
53270
  if (!this.worktreeUsers.has(worktreePath)) {
53184
53271
  this.worktreeUsers.set(worktreePath, new Set);
@@ -53789,6 +53876,24 @@ class SessionManager extends EventEmitter4 {
53789
53876
  return;
53790
53877
  await enableInteractivePermissions(session, username, this.getContext());
53791
53878
  }
53879
+ async showUpdateStatus(threadId, _username) {
53880
+ const session = this.findSessionByThreadId(threadId);
53881
+ if (!session)
53882
+ return;
53883
+ await showUpdateStatus(session, this.autoUpdateManager);
53884
+ }
53885
+ async forceUpdateNow(threadId, username) {
53886
+ const session = this.findSessionByThreadId(threadId);
53887
+ if (!session)
53888
+ return;
53889
+ await forceUpdateNow(session, username, this.autoUpdateManager);
53890
+ }
53891
+ async deferUpdate(threadId, username) {
53892
+ const session = this.findSessionByThreadId(threadId);
53893
+ if (!session)
53894
+ return;
53895
+ await deferUpdate(session, username, this.autoUpdateManager);
53896
+ }
53792
53897
  isSessionInteractive(threadId) {
53793
53898
  const session = this.findSessionByThreadId(threadId);
53794
53899
  if (!session)
@@ -53928,6 +54033,60 @@ class SessionManager extends EventEmitter4 {
53928
54033
  this.isShuttingDown = true;
53929
54034
  setShuttingDown(true);
53930
54035
  }
54036
+ getActivityInfo() {
54037
+ const sessions = [...this.sessions.values()];
54038
+ if (sessions.length === 0) {
54039
+ return {
54040
+ activeSessionCount: 0,
54041
+ lastActivityAt: null,
54042
+ anySessionBusy: false
54043
+ };
54044
+ }
54045
+ let lastActivity = null;
54046
+ let anyBusy = false;
54047
+ for (const session of sessions) {
54048
+ if (!lastActivity || session.lastActivityAt > lastActivity) {
54049
+ lastActivity = session.lastActivityAt;
54050
+ }
54051
+ if (session.typingTimer !== null) {
54052
+ anyBusy = true;
54053
+ }
54054
+ }
54055
+ return {
54056
+ activeSessionCount: sessions.length,
54057
+ lastActivityAt: lastActivity,
54058
+ anySessionBusy: anyBusy
54059
+ };
54060
+ }
54061
+ async broadcastToAll(messageBuilder) {
54062
+ for (const session of this.sessions.values()) {
54063
+ try {
54064
+ const formatter = session.platform.getFormatter();
54065
+ const message = messageBuilder(formatter);
54066
+ await postInfo(session, message);
54067
+ } catch (err) {
54068
+ log18.warn(`Failed to broadcast to session ${session.threadId}: ${err}`);
54069
+ }
54070
+ }
54071
+ }
54072
+ async postUpdateAskMessage(threadIds, version) {
54073
+ for (const threadId of threadIds) {
54074
+ const session = this.findSessionByThreadId(threadId);
54075
+ if (!session)
54076
+ continue;
54077
+ try {
54078
+ const fmt = session.platform.getFormatter();
54079
+ const message = `\uD83D\uDD04 ${fmt.formatBold("Update available:")} v${version}
54080
+
54081
+ ` + `React: \uD83D\uDC4D to update now | \uD83D\uDC4E to defer for 1 hour
54082
+ ` + fmt.formatItalic("Update will proceed automatically after timeout if no response");
54083
+ const post = await session.platform.createInteractivePost(message, ["\uD83D\uDC4D", "\uD83D\uDC4E"], session.threadId);
54084
+ this.registerPost(post.id, session.threadId);
54085
+ } catch (err) {
54086
+ log18.warn(`Failed to post ask message to ${threadId}: ${err}`);
54087
+ }
54088
+ }
54089
+ }
53931
54090
  async shutdown(message) {
53932
54091
  this.isShuttingDown = true;
53933
54092
  if (this.cleanupTimer) {
@@ -63380,6 +63539,9 @@ async function handleMessage(client, session, post, user, options) {
63380
63539
  [code("!kick @user"), "Remove an invited user"],
63381
63540
  [code("!permissions interactive"), "Enable interactive permissions"],
63382
63541
  [code("!approve"), "Approve pending plan (alternative to \uD83D\uDC4D reaction)"],
63542
+ [code("!update"), "Show auto-update status"],
63543
+ [code("!update now"), "Apply pending update immediately"],
63544
+ [code("!update defer"), "Defer pending update for 1 hour"],
63383
63545
  [code("!escape"), "Interrupt current task (session stays active)"],
63384
63546
  [code("!stop"), "Stop this session"],
63385
63547
  [code("!kill"), "Emergency shutdown (kills ALL sessions, exits bot)"]
@@ -63430,6 +63592,18 @@ Release notes not available. See ${formatter.formatLink("GitHub releases", "http
63430
63592
  await session.changeDirectory(threadRoot, cdMatch[1].trim(), username);
63431
63593
  return;
63432
63594
  }
63595
+ const updateMatch = content.trim().match(/^!update(?:\s+(now|defer))?$/i);
63596
+ if (updateMatch) {
63597
+ const subcommand = updateMatch[1]?.toLowerCase();
63598
+ if (subcommand === "now") {
63599
+ await session.forceUpdateNow(threadRoot, username);
63600
+ } else if (subcommand === "defer") {
63601
+ await session.deferUpdate(threadRoot, username);
63602
+ } else {
63603
+ await session.showUpdateStatus(threadRoot, username);
63604
+ }
63605
+ return;
63606
+ }
63433
63607
  const worktreeMatch = content.match(/^!worktree\s+(\S+)(?:\s+(.*))?$/i);
63434
63608
  if (worktreeMatch) {
63435
63609
  const subcommand = worktreeMatch[1].toLowerCase();
@@ -63535,6 +63709,681 @@ Release notes not available. See ${formatter.formatLink("GitHub releases", "http
63535
63709
  }
63536
63710
  }
63537
63711
 
63712
+ // src/auto-update/manager.ts
63713
+ import { EventEmitter as EventEmitter9 } from "events";
63714
+
63715
+ // src/auto-update/checker.ts
63716
+ import { EventEmitter as EventEmitter7 } from "events";
63717
+ var log19 = createLogger("checker");
63718
+ var PACKAGE_NAME2 = "claude-threads";
63719
+ function compareVersions(a, b) {
63720
+ const partsA = a.replace(/^v/, "").split(".").map(Number);
63721
+ const partsB = b.replace(/^v/, "").split(".").map(Number);
63722
+ for (let i = 0;i < 3; i++) {
63723
+ const numA = partsA[i] || 0;
63724
+ const numB = partsB[i] || 0;
63725
+ if (numA > numB)
63726
+ return 1;
63727
+ if (numA < numB)
63728
+ return -1;
63729
+ }
63730
+ return 0;
63731
+ }
63732
+ async function fetchLatestVersion() {
63733
+ try {
63734
+ const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME2}/latest`, {
63735
+ headers: {
63736
+ Accept: "application/json"
63737
+ }
63738
+ });
63739
+ if (!response.ok) {
63740
+ log19.warn(`Failed to fetch latest version: HTTP ${response.status}`);
63741
+ return null;
63742
+ }
63743
+ const data = await response.json();
63744
+ return data.version ?? null;
63745
+ } catch (err) {
63746
+ log19.warn(`Failed to fetch latest version: ${err}`);
63747
+ return null;
63748
+ }
63749
+ }
63750
+
63751
+ class UpdateChecker extends EventEmitter7 {
63752
+ config;
63753
+ checkInterval = null;
63754
+ lastCheck = null;
63755
+ lastUpdateInfo = null;
63756
+ isChecking = false;
63757
+ constructor(config) {
63758
+ super();
63759
+ this.config = config;
63760
+ }
63761
+ start() {
63762
+ if (!this.config.enabled) {
63763
+ log19.debug("Auto-update disabled, not starting checker");
63764
+ return;
63765
+ }
63766
+ setTimeout(() => {
63767
+ this.check().catch((err) => {
63768
+ log19.warn(`Initial update check failed: ${err}`);
63769
+ });
63770
+ }, 5000);
63771
+ const intervalMs = this.config.checkIntervalMinutes * 60 * 1000;
63772
+ this.checkInterval = setInterval(() => {
63773
+ this.check().catch((err) => {
63774
+ log19.warn(`Periodic update check failed: ${err}`);
63775
+ });
63776
+ }, intervalMs);
63777
+ log19.info(`\uD83D\uDD04 Update checker started (every ${this.config.checkIntervalMinutes} minutes)`);
63778
+ }
63779
+ stop() {
63780
+ if (this.checkInterval) {
63781
+ clearInterval(this.checkInterval);
63782
+ this.checkInterval = null;
63783
+ }
63784
+ log19.debug("Update checker stopped");
63785
+ }
63786
+ async check() {
63787
+ if (this.isChecking) {
63788
+ log19.debug("Check already in progress, skipping");
63789
+ return this.lastUpdateInfo;
63790
+ }
63791
+ this.isChecking = true;
63792
+ this.emit("check:start");
63793
+ try {
63794
+ log19.debug("Checking for updates...");
63795
+ const latestVersion2 = await fetchLatestVersion();
63796
+ if (!latestVersion2) {
63797
+ this.emit("check:complete", false);
63798
+ return null;
63799
+ }
63800
+ this.lastCheck = new Date;
63801
+ const currentVersion = VERSION;
63802
+ const hasUpdate = compareVersions(latestVersion2, currentVersion) > 0;
63803
+ if (hasUpdate) {
63804
+ const updateInfo = {
63805
+ available: true,
63806
+ currentVersion,
63807
+ latestVersion: latestVersion2,
63808
+ detectedAt: new Date
63809
+ };
63810
+ if (!this.lastUpdateInfo || this.lastUpdateInfo.latestVersion !== latestVersion2) {
63811
+ log19.info(`\uD83C\uDD95 Update available: v${currentVersion} \u2192 v${latestVersion2}`);
63812
+ this.lastUpdateInfo = updateInfo;
63813
+ this.emit("update", updateInfo);
63814
+ }
63815
+ this.emit("check:complete", true);
63816
+ return updateInfo;
63817
+ }
63818
+ log19.debug(`Up to date (v${currentVersion})`);
63819
+ this.emit("check:complete", false);
63820
+ return null;
63821
+ } catch (err) {
63822
+ log19.warn(`Update check failed: ${err}`);
63823
+ this.emit("check:error", err);
63824
+ return null;
63825
+ } finally {
63826
+ this.isChecking = false;
63827
+ }
63828
+ }
63829
+ getLastUpdateInfo() {
63830
+ return this.lastUpdateInfo;
63831
+ }
63832
+ getLastCheckTime() {
63833
+ return this.lastCheck;
63834
+ }
63835
+ updateConfig(config) {
63836
+ const oldInterval = this.config.checkIntervalMinutes;
63837
+ this.config = config;
63838
+ if (config.checkIntervalMinutes !== oldInterval && this.checkInterval) {
63839
+ this.stop();
63840
+ this.start();
63841
+ }
63842
+ }
63843
+ }
63844
+
63845
+ // src/auto-update/scheduler.ts
63846
+ import { EventEmitter as EventEmitter8 } from "events";
63847
+
63848
+ // src/auto-update/types.ts
63849
+ var RESTART_EXIT_CODE = 42;
63850
+ var DEFAULT_CHECK_INTERVAL_MINUTES = 60;
63851
+ var DEFAULT_IDLE_TIMEOUT_MINUTES = 5;
63852
+ var DEFAULT_QUIET_TIMEOUT_MINUTES = 10;
63853
+ var DEFAULT_ASK_TIMEOUT_MINUTES = 30;
63854
+ var MIN_CHECK_INTERVAL_MINUTES = 5;
63855
+ var UPDATE_STATE_FILENAME = "update-state.json";
63856
+ var DEFAULT_AUTO_UPDATE_CONFIG = {
63857
+ enabled: true,
63858
+ checkIntervalMinutes: DEFAULT_CHECK_INTERVAL_MINUTES,
63859
+ autoRestartMode: "idle",
63860
+ idleTimeoutMinutes: DEFAULT_IDLE_TIMEOUT_MINUTES,
63861
+ quietTimeoutMinutes: DEFAULT_QUIET_TIMEOUT_MINUTES,
63862
+ scheduledWindow: {
63863
+ startHour: 2,
63864
+ endHour: 5
63865
+ },
63866
+ askTimeoutMinutes: DEFAULT_ASK_TIMEOUT_MINUTES
63867
+ };
63868
+ function mergeAutoUpdateConfig(userConfig) {
63869
+ if (!userConfig) {
63870
+ return { ...DEFAULT_AUTO_UPDATE_CONFIG };
63871
+ }
63872
+ return {
63873
+ enabled: userConfig.enabled ?? DEFAULT_AUTO_UPDATE_CONFIG.enabled,
63874
+ checkIntervalMinutes: Math.max(MIN_CHECK_INTERVAL_MINUTES, userConfig.checkIntervalMinutes ?? DEFAULT_AUTO_UPDATE_CONFIG.checkIntervalMinutes),
63875
+ autoRestartMode: userConfig.autoRestartMode ?? DEFAULT_AUTO_UPDATE_CONFIG.autoRestartMode,
63876
+ idleTimeoutMinutes: userConfig.idleTimeoutMinutes ?? DEFAULT_AUTO_UPDATE_CONFIG.idleTimeoutMinutes,
63877
+ quietTimeoutMinutes: userConfig.quietTimeoutMinutes ?? DEFAULT_AUTO_UPDATE_CONFIG.quietTimeoutMinutes,
63878
+ scheduledWindow: userConfig.scheduledWindow ?? DEFAULT_AUTO_UPDATE_CONFIG.scheduledWindow,
63879
+ askTimeoutMinutes: userConfig.askTimeoutMinutes ?? DEFAULT_AUTO_UPDATE_CONFIG.askTimeoutMinutes
63880
+ };
63881
+ }
63882
+ function isInScheduledWindow(window2) {
63883
+ const now = new Date;
63884
+ const hour = now.getHours();
63885
+ if (window2.startHour > window2.endHour) {
63886
+ return hour >= window2.startHour || hour < window2.endHour;
63887
+ }
63888
+ return hour >= window2.startHour && hour < window2.endHour;
63889
+ }
63890
+
63891
+ // src/auto-update/scheduler.ts
63892
+ var log20 = createLogger("scheduler");
63893
+
63894
+ class UpdateScheduler extends EventEmitter8 {
63895
+ config;
63896
+ getSessionActivity;
63897
+ getActiveThreadIds;
63898
+ postAskMessage;
63899
+ pendingUpdate = null;
63900
+ checkTimer = null;
63901
+ countdownTimer = null;
63902
+ idleStartTime = null;
63903
+ scheduledRestartAt = null;
63904
+ askApprovals = new Map;
63905
+ askStartTime = null;
63906
+ constructor(config, getSessionActivity, getActiveThreadIds, postAskMessage) {
63907
+ super();
63908
+ this.config = config;
63909
+ this.getSessionActivity = getSessionActivity;
63910
+ this.getActiveThreadIds = getActiveThreadIds;
63911
+ this.postAskMessage = postAskMessage;
63912
+ }
63913
+ scheduleUpdate(updateInfo) {
63914
+ this.pendingUpdate = updateInfo;
63915
+ if (this.config.autoRestartMode === "immediate") {
63916
+ log20.info("Immediate mode: triggering update now");
63917
+ this.emit("ready", updateInfo);
63918
+ return;
63919
+ }
63920
+ this.startChecking();
63921
+ }
63922
+ cancelSchedule() {
63923
+ this.stopChecking();
63924
+ this.pendingUpdate = null;
63925
+ this.idleStartTime = null;
63926
+ this.scheduledRestartAt = null;
63927
+ this.askApprovals.clear();
63928
+ this.askStartTime = null;
63929
+ log20.debug("Update schedule cancelled");
63930
+ }
63931
+ deferUpdate(minutes) {
63932
+ const deferUntil = new Date(Date.now() + minutes * 60 * 1000);
63933
+ this.scheduledRestartAt = null;
63934
+ this.idleStartTime = null;
63935
+ this.emit("deferred", deferUntil);
63936
+ log20.info(`Update deferred until ${deferUntil.toLocaleTimeString()}`);
63937
+ return deferUntil;
63938
+ }
63939
+ recordAskResponse(threadId, approved) {
63940
+ this.askApprovals.set(threadId, approved);
63941
+ log20.debug(`Thread ${threadId.substring(0, 8)} ${approved ? "approved" : "denied"} update`);
63942
+ this.checkAskCondition();
63943
+ }
63944
+ getScheduledRestartAt() {
63945
+ return this.scheduledRestartAt;
63946
+ }
63947
+ getPendingUpdate() {
63948
+ return this.pendingUpdate;
63949
+ }
63950
+ updateConfig(config) {
63951
+ this.config = config;
63952
+ }
63953
+ stop() {
63954
+ this.stopChecking();
63955
+ this.stopCountdown();
63956
+ }
63957
+ startChecking() {
63958
+ if (this.checkTimer)
63959
+ return;
63960
+ this.checkCondition();
63961
+ this.checkTimer = setInterval(() => this.checkCondition(), 1e4);
63962
+ log20.debug(`Started checking for ${this.config.autoRestartMode} condition`);
63963
+ }
63964
+ stopChecking() {
63965
+ if (this.checkTimer) {
63966
+ clearInterval(this.checkTimer);
63967
+ this.checkTimer = null;
63968
+ }
63969
+ }
63970
+ checkCondition() {
63971
+ if (!this.pendingUpdate)
63972
+ return;
63973
+ switch (this.config.autoRestartMode) {
63974
+ case "idle":
63975
+ this.checkIdleCondition();
63976
+ break;
63977
+ case "quiet":
63978
+ this.checkQuietCondition();
63979
+ break;
63980
+ case "scheduled":
63981
+ this.checkScheduledCondition();
63982
+ break;
63983
+ case "ask":
63984
+ this.checkAskCondition();
63985
+ break;
63986
+ }
63987
+ }
63988
+ checkIdleCondition() {
63989
+ const activity = this.getSessionActivity();
63990
+ if (activity.activeSessionCount === 0) {
63991
+ if (!this.idleStartTime) {
63992
+ this.idleStartTime = new Date;
63993
+ log20.debug("No active sessions, starting idle timer");
63994
+ }
63995
+ const idleMs = Date.now() - this.idleStartTime.getTime();
63996
+ const requiredMs = this.config.idleTimeoutMinutes * 60 * 1000;
63997
+ if (idleMs >= requiredMs) {
63998
+ log20.info(`Idle for ${this.config.idleTimeoutMinutes} minutes, triggering update`);
63999
+ this.triggerCountdown();
64000
+ }
64001
+ } else {
64002
+ if (this.idleStartTime) {
64003
+ log20.debug("Sessions became active, resetting idle timer");
64004
+ this.idleStartTime = null;
64005
+ }
64006
+ }
64007
+ }
64008
+ checkQuietCondition() {
64009
+ const activity = this.getSessionActivity();
64010
+ if (activity.lastActivityAt) {
64011
+ const quietMs = Date.now() - activity.lastActivityAt.getTime();
64012
+ const requiredMs = this.config.quietTimeoutMinutes * 60 * 1000;
64013
+ if (quietMs >= requiredMs && !activity.anySessionBusy) {
64014
+ log20.info(`Sessions quiet for ${this.config.quietTimeoutMinutes} minutes, triggering update`);
64015
+ this.triggerCountdown();
64016
+ }
64017
+ } else if (activity.activeSessionCount === 0) {
64018
+ if (!this.idleStartTime) {
64019
+ this.idleStartTime = new Date;
64020
+ }
64021
+ const idleMs = Date.now() - this.idleStartTime.getTime();
64022
+ const requiredMs = this.config.quietTimeoutMinutes * 60 * 1000;
64023
+ if (idleMs >= requiredMs) {
64024
+ log20.info("No sessions and quiet timeout reached, triggering update");
64025
+ this.triggerCountdown();
64026
+ }
64027
+ }
64028
+ }
64029
+ checkScheduledCondition() {
64030
+ if (!isInScheduledWindow(this.config.scheduledWindow)) {
64031
+ return;
64032
+ }
64033
+ const activity = this.getSessionActivity();
64034
+ if (activity.activeSessionCount === 0) {
64035
+ log20.info("Within scheduled window and no active sessions, triggering update");
64036
+ this.triggerCountdown();
64037
+ } else if (activity.lastActivityAt) {
64038
+ const quietMs = Date.now() - activity.lastActivityAt.getTime();
64039
+ const requiredMs = this.config.idleTimeoutMinutes * 60 * 1000;
64040
+ if (quietMs >= requiredMs && !activity.anySessionBusy) {
64041
+ log20.info("Within scheduled window and sessions quiet, triggering update");
64042
+ this.triggerCountdown();
64043
+ }
64044
+ }
64045
+ }
64046
+ checkAskCondition() {
64047
+ const threadIds = this.getActiveThreadIds();
64048
+ if (threadIds.length === 0) {
64049
+ log20.info("No active threads, proceeding with update");
64050
+ this.triggerCountdown();
64051
+ return;
64052
+ }
64053
+ if (!this.askStartTime && this.pendingUpdate) {
64054
+ this.askStartTime = new Date;
64055
+ this.postAskMessage(threadIds, this.pendingUpdate.latestVersion).catch((err) => {
64056
+ log20.warn(`Failed to post ask message: ${err}`);
64057
+ });
64058
+ return;
64059
+ }
64060
+ let approvals = 0;
64061
+ let denials = 0;
64062
+ for (const approved of this.askApprovals.values()) {
64063
+ if (approved)
64064
+ approvals++;
64065
+ else
64066
+ denials++;
64067
+ }
64068
+ if (approvals > threadIds.length / 2) {
64069
+ log20.info(`Majority approved (${approvals}/${threadIds.length}), triggering update`);
64070
+ this.triggerCountdown();
64071
+ return;
64072
+ }
64073
+ if (denials > threadIds.length / 2) {
64074
+ log20.info(`Majority denied (${denials}/${threadIds.length}), deferring update`);
64075
+ this.deferUpdate(60);
64076
+ return;
64077
+ }
64078
+ if (this.askStartTime) {
64079
+ const elapsedMs = Date.now() - this.askStartTime.getTime();
64080
+ const timeoutMs = this.config.askTimeoutMinutes * 60 * 1000;
64081
+ if (elapsedMs >= timeoutMs) {
64082
+ log20.info(`Ask timeout reached (${this.config.askTimeoutMinutes} min), triggering update`);
64083
+ this.triggerCountdown();
64084
+ }
64085
+ }
64086
+ }
64087
+ triggerCountdown() {
64088
+ if (!this.pendingUpdate)
64089
+ return;
64090
+ this.stopChecking();
64091
+ this.scheduledRestartAt = new Date(Date.now() + 60000);
64092
+ let secondsRemaining = 60;
64093
+ this.emit("countdown", secondsRemaining);
64094
+ this.countdownTimer = setInterval(() => {
64095
+ secondsRemaining--;
64096
+ this.emit("countdown", secondsRemaining);
64097
+ if (secondsRemaining <= 0) {
64098
+ this.stopCountdown();
64099
+ this.emit("ready", this.pendingUpdate);
64100
+ }
64101
+ }, 1000);
64102
+ log20.info("Update countdown started (60 seconds)");
64103
+ }
64104
+ stopCountdown() {
64105
+ if (this.countdownTimer) {
64106
+ clearInterval(this.countdownTimer);
64107
+ this.countdownTimer = null;
64108
+ }
64109
+ }
64110
+ }
64111
+
64112
+ // src/auto-update/installer.ts
64113
+ import { spawn as spawn5 } from "child_process";
64114
+ import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
64115
+ import { dirname as dirname6, resolve as resolve6 } from "path";
64116
+ import { homedir as homedir4 } from "os";
64117
+ var log21 = createLogger("installer");
64118
+ var STATE_PATH = resolve6(homedir4(), ".config", "claude-threads", UPDATE_STATE_FILENAME);
64119
+ var PACKAGE_NAME3 = "claude-threads";
64120
+ function loadUpdateState() {
64121
+ try {
64122
+ if (existsSync11(STATE_PATH)) {
64123
+ const content = readFileSync8(STATE_PATH, "utf-8");
64124
+ return JSON.parse(content);
64125
+ }
64126
+ } catch (err) {
64127
+ log21.warn(`Failed to load update state: ${err}`);
64128
+ }
64129
+ return {};
64130
+ }
64131
+ function saveUpdateState(state) {
64132
+ try {
64133
+ const dir = dirname6(STATE_PATH);
64134
+ if (!existsSync11(dir)) {
64135
+ mkdirSync3(dir, { recursive: true });
64136
+ }
64137
+ writeFileSync4(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
64138
+ log21.debug("Update state saved");
64139
+ } catch (err) {
64140
+ log21.warn(`Failed to save update state: ${err}`);
64141
+ }
64142
+ }
64143
+ function clearUpdateState() {
64144
+ try {
64145
+ if (existsSync11(STATE_PATH)) {
64146
+ writeFileSync4(STATE_PATH, "{}", "utf-8");
64147
+ }
64148
+ } catch (err) {
64149
+ log21.warn(`Failed to clear update state: ${err}`);
64150
+ }
64151
+ }
64152
+ function checkJustUpdated() {
64153
+ const state = loadUpdateState();
64154
+ if (state.justUpdated && state.previousVersion) {
64155
+ saveUpdateState({
64156
+ ...state,
64157
+ justUpdated: false
64158
+ });
64159
+ return {
64160
+ previousVersion: state.previousVersion,
64161
+ currentVersion: VERSION
64162
+ };
64163
+ }
64164
+ return null;
64165
+ }
64166
+ async function installVersion(version) {
64167
+ log21.info(`\uD83D\uDCE6 Installing ${PACKAGE_NAME3}@${version}...`);
64168
+ saveUpdateState({
64169
+ previousVersion: VERSION,
64170
+ targetVersion: version,
64171
+ startedAt: new Date().toISOString(),
64172
+ justUpdated: false
64173
+ });
64174
+ return new Promise((resolve7) => {
64175
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
64176
+ const child = spawn5(npmCmd, ["install", "-g", `${PACKAGE_NAME3}@${version}`], {
64177
+ stdio: ["ignore", "pipe", "pipe"],
64178
+ env: {
64179
+ ...process.env,
64180
+ npm_config_progress: "false"
64181
+ }
64182
+ });
64183
+ let stdout = "";
64184
+ let stderr = "";
64185
+ child.stdout?.on("data", (data) => {
64186
+ stdout += data.toString();
64187
+ });
64188
+ child.stderr?.on("data", (data) => {
64189
+ stderr += data.toString();
64190
+ });
64191
+ child.on("close", (code) => {
64192
+ if (code === 0) {
64193
+ log21.info(`\u2705 Successfully installed ${PACKAGE_NAME3}@${version}`);
64194
+ saveUpdateState({
64195
+ previousVersion: VERSION,
64196
+ targetVersion: version,
64197
+ startedAt: new Date().toISOString(),
64198
+ justUpdated: true
64199
+ });
64200
+ resolve7({ success: true });
64201
+ } else {
64202
+ const errorMsg = stderr || stdout || `Exit code: ${code}`;
64203
+ log21.error(`\u274C Installation failed: ${errorMsg}`);
64204
+ clearUpdateState();
64205
+ resolve7({ success: false, error: errorMsg });
64206
+ }
64207
+ });
64208
+ child.on("error", (err) => {
64209
+ log21.error(`\u274C Failed to spawn npm: ${err}`);
64210
+ clearUpdateState();
64211
+ resolve7({ success: false, error: err.message });
64212
+ });
64213
+ setTimeout(() => {
64214
+ if (child.exitCode === null) {
64215
+ child.kill();
64216
+ log21.error("\u274C Installation timed out");
64217
+ clearUpdateState();
64218
+ resolve7({ success: false, error: "Installation timed out" });
64219
+ }
64220
+ }, 5 * 60 * 1000);
64221
+ });
64222
+ }
64223
+ function getRollbackInstructions(previousVersion) {
64224
+ return `To rollback to the previous version, run:
64225
+ npm install -g ${PACKAGE_NAME3}@${previousVersion}`;
64226
+ }
64227
+
64228
+ class UpdateInstaller {
64229
+ isInstalling = false;
64230
+ async install(updateInfo) {
64231
+ if (this.isInstalling) {
64232
+ return { success: false, error: "Installation already in progress" };
64233
+ }
64234
+ this.isInstalling = true;
64235
+ try {
64236
+ return await installVersion(updateInfo.latestVersion);
64237
+ } finally {
64238
+ this.isInstalling = false;
64239
+ }
64240
+ }
64241
+ isInProgress() {
64242
+ return this.isInstalling;
64243
+ }
64244
+ checkJustUpdated() {
64245
+ return checkJustUpdated();
64246
+ }
64247
+ getState() {
64248
+ return loadUpdateState();
64249
+ }
64250
+ clearState() {
64251
+ clearUpdateState();
64252
+ }
64253
+ }
64254
+
64255
+ // src/auto-update/manager.ts
64256
+ var log22 = createLogger("auto-update");
64257
+
64258
+ class AutoUpdateManager extends EventEmitter9 {
64259
+ config;
64260
+ callbacks;
64261
+ checker;
64262
+ scheduler;
64263
+ installer;
64264
+ state = {
64265
+ status: "idle"
64266
+ };
64267
+ deferredUntil = null;
64268
+ constructor(configOverride, callbacks) {
64269
+ super();
64270
+ this.config = mergeAutoUpdateConfig(configOverride);
64271
+ this.callbacks = callbacks;
64272
+ this.checker = new UpdateChecker(this.config);
64273
+ this.scheduler = new UpdateScheduler(this.config, callbacks.getSessionActivity, callbacks.getActiveThreadIds, callbacks.postAskMessage);
64274
+ this.installer = new UpdateInstaller;
64275
+ this.setupEventHandlers();
64276
+ }
64277
+ start() {
64278
+ if (!this.config.enabled) {
64279
+ log22.info("Auto-update is disabled");
64280
+ return;
64281
+ }
64282
+ const updateResult = this.installer.checkJustUpdated();
64283
+ if (updateResult) {
64284
+ log22.info(`\uD83C\uDF89 Updated from v${updateResult.previousVersion} to v${updateResult.currentVersion}`);
64285
+ this.callbacks.broadcastUpdate((fmt) => `\uD83C\uDF89 ${fmt.formatBold("Bot updated")} from v${updateResult.previousVersion} to v${updateResult.currentVersion}`).catch((err) => {
64286
+ log22.warn(`Failed to broadcast update notification: ${err}`);
64287
+ });
64288
+ }
64289
+ this.checker.start();
64290
+ log22.info(`\uD83D\uDD04 Auto-update manager started (mode: ${this.config.autoRestartMode})`);
64291
+ }
64292
+ stop() {
64293
+ this.checker.stop();
64294
+ this.scheduler.stop();
64295
+ log22.debug("Auto-update manager stopped");
64296
+ }
64297
+ getState() {
64298
+ return { ...this.state };
64299
+ }
64300
+ getConfig() {
64301
+ return { ...this.config };
64302
+ }
64303
+ async checkNow() {
64304
+ return this.checker.check();
64305
+ }
64306
+ async forceUpdate() {
64307
+ const updateInfo = this.state.updateInfo || await this.checker.check();
64308
+ if (!updateInfo) {
64309
+ log22.info("No update available");
64310
+ return;
64311
+ }
64312
+ log22.info("Forcing immediate update");
64313
+ await this.performUpdate(updateInfo);
64314
+ }
64315
+ deferUpdate(minutes = 60) {
64316
+ this.deferredUntil = this.scheduler.deferUpdate(minutes);
64317
+ this.updateStatus("deferred", `Deferred until ${this.deferredUntil.toLocaleTimeString()}`);
64318
+ }
64319
+ recordAskResponse(threadId, approved) {
64320
+ this.scheduler.recordAskResponse(threadId, approved);
64321
+ }
64322
+ isEnabled() {
64323
+ return this.config.enabled;
64324
+ }
64325
+ hasUpdate() {
64326
+ return this.state.updateInfo?.available ?? false;
64327
+ }
64328
+ getUpdateInfo() {
64329
+ return this.state.updateInfo;
64330
+ }
64331
+ getScheduledRestartAt() {
64332
+ return this.scheduler.getScheduledRestartAt();
64333
+ }
64334
+ setupEventHandlers() {
64335
+ this.checker.on("update", (info) => {
64336
+ this.state.updateInfo = info;
64337
+ this.updateStatus("available");
64338
+ this.emit("update:available", info);
64339
+ this.callbacks.refreshUI().catch(() => {});
64340
+ this.scheduler.scheduleUpdate(info);
64341
+ });
64342
+ this.scheduler.on("countdown", (seconds) => {
64343
+ this.emit("update:countdown", seconds);
64344
+ if (seconds === 60 || seconds === 30 || seconds === 10) {
64345
+ const latestVersion2 = this.state.updateInfo?.latestVersion;
64346
+ this.callbacks.broadcastUpdate((fmt) => `\u23F3 ${fmt.formatBold(`Restarting in ${seconds} seconds`)} for update to v${latestVersion2}`).catch(() => {});
64347
+ }
64348
+ });
64349
+ this.scheduler.on("ready", async (info) => {
64350
+ await this.performUpdate(info);
64351
+ });
64352
+ this.scheduler.on("deferred", (until) => {
64353
+ this.deferredUntil = until;
64354
+ this.updateStatus("deferred");
64355
+ });
64356
+ }
64357
+ async performUpdate(updateInfo) {
64358
+ this.updateStatus("installing");
64359
+ await this.callbacks.broadcastUpdate((fmt) => `\uD83D\uDCE6 ${fmt.formatBold("Installing update")} v${updateInfo.latestVersion}...`).catch(() => {});
64360
+ const result = await this.installer.install(updateInfo);
64361
+ if (result.success) {
64362
+ this.updateStatus("pending_restart");
64363
+ this.emit("update:restart", updateInfo.latestVersion);
64364
+ await this.callbacks.broadcastUpdate((fmt) => `\u2705 ${fmt.formatBold("Update installed")} - restarting now. ${fmt.formatItalic("Sessions will resume automatically.")}`).catch(() => {});
64365
+ await new Promise((resolve7) => setTimeout(resolve7, 1000));
64366
+ log22.info(`\uD83D\uDD04 Restarting for update to v${updateInfo.latestVersion}`);
64367
+ process.exit(RESTART_EXIT_CODE);
64368
+ } else {
64369
+ const errorMsg = result.error ?? "Unknown error";
64370
+ this.state.errorMessage = errorMsg;
64371
+ this.updateStatus("failed", errorMsg);
64372
+ this.emit("update:failed", errorMsg);
64373
+ const errorText = result.error;
64374
+ await this.callbacks.broadcastUpdate((fmt) => `\u274C ${fmt.formatBold("Update failed")}: ${errorText}
64375
+ ${getRollbackInstructions(VERSION)}`).catch(() => {});
64376
+ }
64377
+ }
64378
+ updateStatus(status, message) {
64379
+ this.state.status = status;
64380
+ if (message) {
64381
+ this.state.errorMessage = status === "failed" ? message : undefined;
64382
+ }
64383
+ this.emit("update:status", status, message);
64384
+ this.callbacks.refreshUI().catch(() => {});
64385
+ }
64386
+ }
63538
64387
  // src/index.ts
63539
64388
  function createPlatformClient(config) {
63540
64389
  switch (config.type) {
@@ -63572,12 +64421,56 @@ function wirePlatformEvents(platformId, client, session, ui) {
63572
64421
  ui.addLog({ level: "error", component: platformId, message: String(e) });
63573
64422
  });
63574
64423
  }
63575
- 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").option("--skip-version-check", "Skip Claude CLI version compatibility check").parse();
64424
+ 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").option("--skip-version-check", "Skip Claude CLI version compatibility check").option("--auto-restart", "Enable auto-restart on updates (default when autoUpdate enabled)").option("--no-auto-restart", "Disable auto-restart on updates").parse();
63576
64425
  var opts = program.opts();
63577
64426
  function hasRequiredCliArgs(args) {
63578
64427
  return !!(args.url && args.token && args.channel);
63579
64428
  }
63580
64429
  async function main() {
64430
+ const shouldUseAutoRestart = async () => {
64431
+ if (opts.autoRestart === false)
64432
+ return false;
64433
+ if (opts.autoRestart === true)
64434
+ return true;
64435
+ if (await configExists()) {
64436
+ try {
64437
+ const config2 = loadConfigWithMigration();
64438
+ if (!config2)
64439
+ return false;
64440
+ return config2.autoUpdate?.enabled !== false;
64441
+ } catch {
64442
+ return false;
64443
+ }
64444
+ }
64445
+ return false;
64446
+ };
64447
+ if (await shouldUseAutoRestart()) {
64448
+ const { spawn: spawn6 } = await import("child_process");
64449
+ const { dirname: dirname7, resolve: resolve7 } = await import("path");
64450
+ const { fileURLToPath: fileURLToPath6 } = await import("url");
64451
+ const __filename2 = fileURLToPath6(import.meta.url);
64452
+ const __dirname6 = dirname7(__filename2);
64453
+ const daemonPath = resolve7(__dirname6, "..", "bin", "claude-threads-daemon");
64454
+ const args = process.argv.slice(2).filter((arg) => arg !== "--auto-restart" && arg !== "--no-auto-restart").concat("--no-auto-restart");
64455
+ console.log("\uD83D\uDD04 Starting with auto-restart enabled...");
64456
+ console.log("");
64457
+ const binPath = __filename2;
64458
+ const child = spawn6(daemonPath, ["--restart-on-error", ...args], {
64459
+ stdio: "inherit",
64460
+ env: {
64461
+ ...process.env,
64462
+ CLAUDE_THREADS_BIN: binPath
64463
+ }
64464
+ });
64465
+ child.on("error", (err) => {
64466
+ console.error(`Failed to start daemon: ${err.message}`);
64467
+ process.exit(1);
64468
+ });
64469
+ child.on("exit", (code) => {
64470
+ process.exit(code ?? 0);
64471
+ });
64472
+ return;
64473
+ }
63581
64474
  checkForUpdates();
63582
64475
  if (opts.debug) {
63583
64476
  process.env.DEBUG = "1";
@@ -63634,6 +64527,7 @@ async function main() {
63634
64527
  keepAliveEnabled
63635
64528
  };
63636
64529
  let sessionManager = null;
64530
+ let autoUpdateManager = null;
63637
64531
  const ui = await startUI({
63638
64532
  config: {
63639
64533
  version: VERSION,
@@ -63748,6 +64642,31 @@ async function main() {
63748
64642
  }
63749
64643
  }));
63750
64644
  await session.initialize();
64645
+ autoUpdateManager = new AutoUpdateManager(config.autoUpdate, {
64646
+ getSessionActivity: () => session.getActivityInfo(),
64647
+ getActiveThreadIds: () => session.getActiveThreadIds(),
64648
+ broadcastUpdate: (msg) => session.broadcastToAll(msg),
64649
+ postAskMessage: (ids, ver) => session.postUpdateAskMessage(ids, ver),
64650
+ refreshUI: () => session.updateAllStickyMessages()
64651
+ });
64652
+ session.setAutoUpdateManager(autoUpdateManager);
64653
+ autoUpdateManager.on("update:available", (info) => {
64654
+ ui.addLog({ level: "info", component: "update", message: `\uD83C\uDD95 Update available: v${info.currentVersion} \u2192 v${info.latestVersion}` });
64655
+ });
64656
+ autoUpdateManager.on("update:countdown", (seconds) => {
64657
+ if (seconds === 60 || seconds === 30 || seconds === 10 || seconds <= 5) {
64658
+ ui.addLog({ level: "info", component: "update", message: `\uD83D\uDD04 Restarting in ${seconds} seconds...` });
64659
+ }
64660
+ });
64661
+ autoUpdateManager.on("update:status", (status, message) => {
64662
+ if (message) {
64663
+ ui.addLog({ level: "info", component: "update", message: `\uD83D\uDD04 ${status}: ${message}` });
64664
+ }
64665
+ });
64666
+ autoUpdateManager.on("update:failed", (error) => {
64667
+ ui.addLog({ level: "error", component: "update", message: `\u274C Update failed: ${error}` });
64668
+ });
64669
+ autoUpdateManager.start();
63751
64670
  ui.setReady();
63752
64671
  let isShuttingDown2 = false;
63753
64672
  const shutdown = async (_signal) => {
@@ -63755,7 +64674,7 @@ async function main() {
63755
64674
  return;
63756
64675
  isShuttingDown2 = true;
63757
64676
  ui.setShuttingDown();
63758
- await new Promise((resolve6) => setTimeout(resolve6, 50));
64677
+ await new Promise((resolve7) => setTimeout(resolve7, 50));
63759
64678
  session.setShuttingDown();
63760
64679
  await session.updateAllStickyMessages();
63761
64680
  const activeCount = session.getActiveThreadIds().length;
@@ -63764,6 +64683,7 @@ async function main() {
63764
64683
  await session.postShutdownMessages();
63765
64684
  }
63766
64685
  await session.killAllSessions();
64686
+ autoUpdateManager?.stop();
63767
64687
  for (const client of platforms.values()) {
63768
64688
  client.disconnect();
63769
64689
  }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "0.42.0",
3
+ "version": "0.43.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",
7
7
  "bin": {
8
8
  "claude-threads": "./dist/index.js",
9
- "claude-threads-mcp": "./dist/mcp/permission-server.js"
9
+ "claude-threads-mcp": "./dist/mcp/permission-server.js",
10
+ "claude-threads-daemon": "./bin/claude-threads-daemon"
10
11
  },
11
12
  "scripts": {
12
13
  "dev": "bun --watch src/index.ts",
@@ -53,6 +54,7 @@
53
54
  "homepage": "https://github.com/anneschuth/claude-threads#readme",
54
55
  "files": [
55
56
  "dist",
57
+ "bin",
56
58
  "README.md",
57
59
  "CHANGELOG.md",
58
60
  "LICENSE",