claude-threads 1.2.1 → 1.3.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
@@ -5,6 +5,22 @@ 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
+ ## [1.3.0] - 2026-01-17
9
+
10
+ ### Added
11
+ - **Dynamic slash command passthrough** - Unknown `!commands` are now checked against Claude CLI's available slash commands and passed through automatically (#229)
12
+ - Captures `slash_commands` from Claude CLI's `init` event
13
+ - `!foo` passes through to `/foo` if it's an available slash command
14
+ - **`!plugin` command** - New command for managing Claude Code plugins (#229)
15
+ - `!plugin list` - Shows installed plugins
16
+ - `!plugin install <name>` - Installs a plugin and restarts Claude CLI
17
+ - `!plugin uninstall <name>` - Uninstalls a plugin and restarts Claude CLI
18
+
19
+ ### Fixed
20
+ - **Package manager detection for updates** - Updates now use the same package manager (bun/npm) that was used to install claude-threads (#230)
21
+ - Prevents duplicate global installations when updating
22
+ - Respects `BUN_INSTALL` env var for custom bun install locations
23
+
8
24
  ## [1.2.1] - 2026-01-17
9
25
 
10
26
  ### Fixed
@@ -9,6 +9,12 @@
9
9
  # - 0: Clean exit, don't restart
10
10
  # - 1+: Error, optional restart based on flags
11
11
  #
12
+ # PLATFORM SUPPORT:
13
+ # - Linux: Full support
14
+ # - macOS: Full support
15
+ # - Windows: Requires Git Bash, WSL, or similar bash environment
16
+ # Without bash, use claude-threads directly (no auto-restart)
17
+ #
12
18
  # Usage:
13
19
  # claude-threads-daemon [options]
14
20
  # claude-threads-daemon --restart-on-error # Also restart on errors
@@ -32,9 +38,13 @@ VERBOSE=false
32
38
 
33
39
  # Path to claude-threads binary (set by index.ts when spawning daemon)
34
40
  # Falls back to global 'claude-threads' command if not set
35
- # If CLAUDE_THREADS_BIN is a .js file, run it with node
41
+ # If CLAUDE_THREADS_BIN is a .js file, run it with node (matches the shebang)
36
42
  if [ -n "$CLAUDE_THREADS_BIN" ]; then
37
43
  if [[ "$CLAUDE_THREADS_BIN" == *.js ]]; then
44
+ if ! command -v node &> /dev/null; then
45
+ echo "[daemon] Error: node not found in PATH" >&2
46
+ exit 1
47
+ fi
38
48
  CLAUDE_THREADS_CMD="node $CLAUDE_THREADS_BIN"
39
49
  else
40
50
  CLAUDE_THREADS_CMD="$CLAUDE_THREADS_BIN"
package/dist/index.js CHANGED
@@ -16227,7 +16227,7 @@ var require_react_reconciler_development = __commonJS((exports, module) => {
16227
16227
  return hook.checkDCE ? true : false;
16228
16228
  }
16229
16229
  function setIsStrictModeForDevtools(newIsStrictMode) {
16230
- typeof log29 === "function" && unstable_setDisableYieldValue2(newIsStrictMode);
16230
+ typeof log30 === "function" && unstable_setDisableYieldValue2(newIsStrictMode);
16231
16231
  if (injectedHook && typeof injectedHook.setStrictMode === "function")
16232
16232
  try {
16233
16233
  injectedHook.setStrictMode(rendererID, newIsStrictMode);
@@ -24311,7 +24311,7 @@ Check the render method of %s.`, getComponentNameFromFiber(current) || "Unknown"
24311
24311
  var fiberStack = [];
24312
24312
  var index$jscomp$0 = -1, emptyContextObject = {};
24313
24313
  Object.freeze(emptyContextObject);
24314
- var clz32 = Math.clz32 ? Math.clz32 : clz32Fallback, log$1 = Math.log, LN2 = Math.LN2, nextTransitionUpdateLane = 256, nextTransitionDeferredLane = 262144, nextRetryLane = 4194304, scheduleCallback$3 = Scheduler.unstable_scheduleCallback, cancelCallback$1 = Scheduler.unstable_cancelCallback, shouldYield = Scheduler.unstable_shouldYield, requestPaint = Scheduler.unstable_requestPaint, now$1 = Scheduler.unstable_now, ImmediatePriority = Scheduler.unstable_ImmediatePriority, UserBlockingPriority = Scheduler.unstable_UserBlockingPriority, NormalPriority$1 = Scheduler.unstable_NormalPriority, IdlePriority = Scheduler.unstable_IdlePriority, log29 = Scheduler.log, unstable_setDisableYieldValue2 = Scheduler.unstable_setDisableYieldValue, rendererID = null, injectedHook = null, hasLoggedError = false, isDevToolsPresent = typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== "undefined", lastResetTime = 0;
24314
+ var clz32 = Math.clz32 ? Math.clz32 : clz32Fallback, log$1 = Math.log, LN2 = Math.LN2, nextTransitionUpdateLane = 256, nextTransitionDeferredLane = 262144, nextRetryLane = 4194304, scheduleCallback$3 = Scheduler.unstable_scheduleCallback, cancelCallback$1 = Scheduler.unstable_cancelCallback, shouldYield = Scheduler.unstable_shouldYield, requestPaint = Scheduler.unstable_requestPaint, now$1 = Scheduler.unstable_now, ImmediatePriority = Scheduler.unstable_ImmediatePriority, UserBlockingPriority = Scheduler.unstable_UserBlockingPriority, NormalPriority$1 = Scheduler.unstable_NormalPriority, IdlePriority = Scheduler.unstable_IdlePriority, log30 = Scheduler.log, unstable_setDisableYieldValue2 = Scheduler.unstable_setDisableYieldValue, rendererID = null, injectedHook = null, hasLoggedError = false, isDevToolsPresent = typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== "undefined", lastResetTime = 0;
24315
24315
  if (typeof performance === "object" && typeof performance.now === "function") {
24316
24316
  var localPerformance = performance;
24317
24317
  var getCurrentTime = function() {
@@ -54370,6 +54370,19 @@ var COMMAND_REGISTRY = [
54370
54370
  claudeCanExecute: true,
54371
54371
  returnsResultToClaude: false
54372
54372
  },
54373
+ {
54374
+ command: "plugin",
54375
+ description: "Manage Claude Code plugins",
54376
+ args: "<subcommand> [name]",
54377
+ category: "system",
54378
+ audience: "user",
54379
+ claudeNotes: "User manages plugins themselves",
54380
+ subcommands: [
54381
+ { name: "list", description: "List installed plugins" },
54382
+ { name: "install", description: "Install a plugin (restarts Claude)", args: "<name>" },
54383
+ { name: "uninstall", description: "Uninstall a plugin (restarts Claude)", args: "<name>" }
54384
+ ]
54385
+ },
54373
54386
  {
54374
54387
  command: "context",
54375
54388
  description: "Show context usage",
@@ -54443,13 +54456,22 @@ var COMMAND_PATTERNS = [
54443
54456
  ["context", /^!context\s*$/i],
54444
54457
  ["cost", /^!cost\s*$/i],
54445
54458
  ["compact", /^!compact\s*$/i],
54459
+ ["plugin", /^!plugin(?:\s+(.+))?$/i],
54446
54460
  ["kill", /^!kill\s*$/i],
54447
- ["bug", /^!bug(?:\s+(.+))?$/i]
54461
+ ["bug", /^!bug(?:\s+(.+))?$/i],
54462
+ ["_dynamic", /^!([a-z][-a-z0-9]*)(?:\s+(.+))?$/i]
54448
54463
  ];
54449
54464
  function parseCommand(text) {
54450
54465
  for (const [command, pattern] of COMMAND_PATTERNS) {
54451
54466
  const match = text.match(pattern);
54452
54467
  if (match) {
54468
+ if (command === "_dynamic") {
54469
+ return {
54470
+ command: match[1].toLowerCase(),
54471
+ args: match[2]?.trim(),
54472
+ match: match[0]
54473
+ };
54474
+ }
54453
54475
  return {
54454
54476
  command,
54455
54477
  args: match[1]?.trim(),
@@ -65705,6 +65727,10 @@ function handleEventPreProcessing(session, event, ctx) {
65705
65727
  }
65706
65728
  if (event.type === "system") {
65707
65729
  const e = event;
65730
+ if (e.subtype === "init" && e.slash_commands && Array.isArray(e.slash_commands)) {
65731
+ session.availableSlashCommands = new Set(e.slash_commands.map((cmd) => cmd.startsWith("/") ? cmd.slice(1) : cmd));
65732
+ sessionLog4(session).info(`Captured ${session.availableSlashCommands.size} slash commands from init: ${[...session.availableSlashCommands].join(", ")}`);
65733
+ }
65708
65734
  if (e.subtype === "status" && e.status === "compacting") {
65709
65735
  handleCompactionStart(session, ctx);
65710
65736
  }
@@ -67024,6 +67050,114 @@ class SessionMonitor {
67024
67050
  }
67025
67051
  }
67026
67052
  }
67053
+ // src/operations/plugin/handler.ts
67054
+ import { spawn as spawn7 } from "child_process";
67055
+ var log27 = createLogger("plugin");
67056
+ var sessionLog7 = createSessionLog(log27);
67057
+ async function runPluginCommand(args, cwd, timeout2 = 60000) {
67058
+ return new Promise((resolve6) => {
67059
+ const claudePath = process.env.CLAUDE_PATH || "claude";
67060
+ const proc = spawn7(claudePath, ["plugin", ...args], {
67061
+ cwd,
67062
+ timeout: timeout2
67063
+ });
67064
+ let stdout = "";
67065
+ let stderr = "";
67066
+ proc.stdout.on("data", (data) => {
67067
+ stdout += data.toString();
67068
+ });
67069
+ proc.stderr.on("data", (data) => {
67070
+ stderr += data.toString();
67071
+ });
67072
+ proc.on("close", (code) => {
67073
+ resolve6({ stdout, stderr, exitCode: code ?? 1 });
67074
+ });
67075
+ proc.on("error", (err) => {
67076
+ resolve6({ stdout, stderr, exitCode: 1 });
67077
+ log27.error(`Plugin command error: ${err.message}`);
67078
+ });
67079
+ });
67080
+ }
67081
+ async function handlePluginList(session) {
67082
+ const formatter = session.platform.getFormatter();
67083
+ await post(session, "info", `\uD83D\uDCE6 Listing installed plugins...`);
67084
+ const result = await runPluginCommand(["list"], session.workingDir);
67085
+ if (result.exitCode !== 0) {
67086
+ await postError(session, `Failed to list plugins:
67087
+ ${formatter.formatCodeBlock(result.stderr || result.stdout, "text")}`);
67088
+ return;
67089
+ }
67090
+ const output = result.stdout.trim() || "No plugins installed";
67091
+ await post(session, "info", `${formatter.formatBold("Installed plugins:")}
67092
+ ${formatter.formatCodeBlock(output, "text")}`);
67093
+ sessionLog7(session).info(`Listed plugins: ${output.substring(0, 100)}...`);
67094
+ }
67095
+ async function handlePluginInstall(session, pluginName, username, ctx) {
67096
+ const formatter = session.platform.getFormatter();
67097
+ await post(session, "info", `\uD83D\uDCE6 Installing plugin: ${formatter.formatCode(pluginName)}...`);
67098
+ sessionLog7(session).info(`Installing plugin: ${pluginName} (requested by @${username})`);
67099
+ session.threadLogger?.logCommand("plugin install", pluginName, username);
67100
+ const result = await runPluginCommand(["install", pluginName], session.workingDir);
67101
+ if (result.exitCode !== 0) {
67102
+ const errorMsg = result.stderr || result.stdout || "Unknown error";
67103
+ await postError(session, `Failed to install plugin ${formatter.formatCode(pluginName)}:
67104
+ ${formatter.formatCodeBlock(errorMsg, "text")}`);
67105
+ sessionLog7(session).error(`Failed to install plugin ${pluginName}: ${errorMsg}`);
67106
+ return;
67107
+ }
67108
+ await post(session, "success", `✅ Plugin installed: ${formatter.formatCode(pluginName)}
67109
+ \uD83D\uDD04 Restarting Claude to load plugin...`);
67110
+ const cliOptions = {
67111
+ workingDir: session.workingDir,
67112
+ threadId: session.threadId,
67113
+ skipPermissions: ctx.config.skipPermissions || !session.forceInteractivePermissions,
67114
+ sessionId: session.claudeSessionId,
67115
+ resume: true,
67116
+ chrome: ctx.config.chromeEnabled,
67117
+ platformConfig: session.platform.getMcpConfig(),
67118
+ logSessionId: session.sessionId,
67119
+ permissionTimeoutMs: ctx.config.permissionTimeoutMs
67120
+ };
67121
+ const success = await restartClaudeSession(session, cliOptions, ctx, `Plugin installation: ${pluginName}`);
67122
+ if (success) {
67123
+ sessionLog7(session).info(`Claude restarted after installing plugin: ${pluginName}`);
67124
+ } else {
67125
+ await postError(session, `Plugin installed but failed to restart Claude. Try ${formatter.formatCode("!cd .")} to manually restart.`);
67126
+ }
67127
+ }
67128
+ async function handlePluginUninstall(session, pluginName, username, ctx) {
67129
+ const formatter = session.platform.getFormatter();
67130
+ await post(session, "info", `\uD83D\uDDD1️ Uninstalling plugin: ${formatter.formatCode(pluginName)}...`);
67131
+ sessionLog7(session).info(`Uninstalling plugin: ${pluginName} (requested by @${username})`);
67132
+ session.threadLogger?.logCommand("plugin uninstall", pluginName, username);
67133
+ const result = await runPluginCommand(["uninstall", pluginName], session.workingDir);
67134
+ if (result.exitCode !== 0) {
67135
+ const errorMsg = result.stderr || result.stdout || "Unknown error";
67136
+ await postError(session, `Failed to uninstall plugin ${formatter.formatCode(pluginName)}:
67137
+ ${formatter.formatCodeBlock(errorMsg, "text")}`);
67138
+ sessionLog7(session).error(`Failed to uninstall plugin ${pluginName}: ${errorMsg}`);
67139
+ return;
67140
+ }
67141
+ await post(session, "success", `✅ Plugin uninstalled: ${formatter.formatCode(pluginName)}
67142
+ \uD83D\uDD04 Restarting Claude...`);
67143
+ const cliOptions = {
67144
+ workingDir: session.workingDir,
67145
+ threadId: session.threadId,
67146
+ skipPermissions: ctx.config.skipPermissions || !session.forceInteractivePermissions,
67147
+ sessionId: session.claudeSessionId,
67148
+ resume: true,
67149
+ chrome: ctx.config.chromeEnabled,
67150
+ platformConfig: session.platform.getMcpConfig(),
67151
+ logSessionId: session.sessionId,
67152
+ permissionTimeoutMs: ctx.config.permissionTimeoutMs
67153
+ };
67154
+ const success = await restartClaudeSession(session, cliOptions, ctx, `Plugin uninstallation: ${pluginName}`);
67155
+ if (success) {
67156
+ sessionLog7(session).info(`Claude restarted after uninstalling plugin: ${pluginName}`);
67157
+ } else {
67158
+ await postError(session, `Plugin uninstalled but failed to restart Claude. Try ${formatter.formatCode("!cd .")} to manually restart.`);
67159
+ }
67160
+ }
67027
67161
  // src/session/registry.ts
67028
67162
  class SessionRegistry {
67029
67163
  sessions = new Map;
@@ -67138,7 +67272,7 @@ class SessionRegistry {
67138
67272
  }
67139
67273
 
67140
67274
  // src/session/manager.ts
67141
- var log27 = createLogger("manager");
67275
+ var log28 = createLogger("manager");
67142
67276
 
67143
67277
  class SessionManager extends EventEmitter4 {
67144
67278
  platforms = new Map;
@@ -67203,7 +67337,7 @@ class SessionManager extends EventEmitter4 {
67203
67337
  markNeedsBump(platformId);
67204
67338
  this.updateStickyMessage();
67205
67339
  });
67206
- log27.info(`\uD83D\uDCE1 Platform "${platformId}" registered`);
67340
+ log28.info(`\uD83D\uDCE1 Platform "${platformId}" registered`);
67207
67341
  }
67208
67342
  removePlatform(platformId) {
67209
67343
  this.platforms.delete(platformId);
@@ -67219,7 +67353,7 @@ class SessionManager extends EventEmitter4 {
67219
67353
  if (users) {
67220
67354
  users.add(sessionId);
67221
67355
  }
67222
- log27.debug(`Registered session ${sessionId.substring(0, 20)} as worktree user for ${worktreePath}`);
67356
+ log28.debug(`Registered session ${sessionId.substring(0, 20)} as worktree user for ${worktreePath}`);
67223
67357
  }
67224
67358
  unregisterWorktreeUser(worktreePath, sessionId) {
67225
67359
  const users = this.worktreeUsers.get(worktreePath);
@@ -67366,7 +67500,7 @@ class SessionManager extends EventEmitter4 {
67366
67500
  return false;
67367
67501
  }
67368
67502
  const shortId = persistedSession.threadId.substring(0, 8);
67369
- log27.info(`\uD83D\uDD04 Resuming session ${shortId}... via emoji reaction by @${username}`);
67503
+ log28.info(`\uD83D\uDD04 Resuming session ${shortId}... via emoji reaction by @${username}`);
67370
67504
  await resumeSession(persistedSession, this.getContext());
67371
67505
  return true;
67372
67506
  }
@@ -67396,7 +67530,7 @@ class SessionManager extends EventEmitter4 {
67396
67530
  }
67397
67531
  if (session.lastError?.postId === postId && isBugReportEmoji(emojiName)) {
67398
67532
  if (session.startedBy === username || session.platform.isUserAllowed(username) || session.sessionAllowedUsers.has(username)) {
67399
- log27.info(`\uD83D\uDC1B @${username} triggered bug report from error reaction`);
67533
+ log28.info(`\uD83D\uDC1B @${username} triggered bug report from error reaction`);
67400
67534
  await reportBug(session, undefined, username, this.getContext(), session.lastError);
67401
67535
  return;
67402
67536
  }
@@ -67556,11 +67690,11 @@ class SessionManager extends EventEmitter4 {
67556
67690
  }
67557
67691
  }
67558
67692
  if (sessionsToKill.length === 0) {
67559
- log27.info(`No active sessions to pause for platform ${platformId}`);
67693
+ log28.info(`No active sessions to pause for platform ${platformId}`);
67560
67694
  await this.updateStickyMessage();
67561
67695
  return;
67562
67696
  }
67563
- log27.info(`⏸️ Pausing ${sessionsToKill.length} session(s) for platform ${platformId}`);
67697
+ log28.info(`⏸️ Pausing ${sessionsToKill.length} session(s) for platform ${platformId}`);
67564
67698
  for (const session of sessionsToKill) {
67565
67699
  try {
67566
67700
  const fmt = session.platform.getFormatter();
@@ -67576,9 +67710,9 @@ class SessionManager extends EventEmitter4 {
67576
67710
  session.claude.kill();
67577
67711
  this.registry.unregister(session.sessionId);
67578
67712
  this.emitSessionRemove(session.sessionId);
67579
- log27.info(`⏸️ Paused session ${session.threadId.substring(0, 8)}`);
67713
+ log28.info(`⏸️ Paused session ${session.threadId.substring(0, 8)}`);
67580
67714
  } catch (err) {
67581
- log27.warn(`Failed to pause session ${session.threadId}: ${err}`);
67715
+ log28.warn(`Failed to pause session ${session.threadId}: ${err}`);
67582
67716
  }
67583
67717
  }
67584
67718
  for (const session of sessionsToKill) {
@@ -67599,17 +67733,17 @@ class SessionManager extends EventEmitter4 {
67599
67733
  sessionsToResume.push(state);
67600
67734
  }
67601
67735
  if (sessionsToResume.length === 0) {
67602
- log27.info(`No paused sessions to resume for platform ${platformId}`);
67736
+ log28.info(`No paused sessions to resume for platform ${platformId}`);
67603
67737
  await this.updateStickyMessage();
67604
67738
  return;
67605
67739
  }
67606
- log27.info(`▶️ Resuming ${sessionsToResume.length} paused session(s) for platform ${platformId}`);
67740
+ log28.info(`▶️ Resuming ${sessionsToResume.length} paused session(s) for platform ${platformId}`);
67607
67741
  for (const state of sessionsToResume) {
67608
67742
  try {
67609
67743
  await resumeSession(state, this.getContext());
67610
- log27.info(`▶️ Resumed session ${state.threadId.substring(0, 8)}`);
67744
+ log28.info(`▶️ Resumed session ${state.threadId.substring(0, 8)}`);
67611
67745
  } catch (err) {
67612
- log27.warn(`Failed to resume session ${state.threadId}: ${err}`);
67746
+ log28.warn(`Failed to resume session ${state.threadId}: ${err}`);
67613
67747
  }
67614
67748
  }
67615
67749
  await this.updateStickyMessage();
@@ -67621,14 +67755,14 @@ class SessionManager extends EventEmitter4 {
67621
67755
  const sessionTimeoutMs = this.limits.sessionTimeoutMinutes * 60 * 1000;
67622
67756
  const staleIds = this.sessionStore.cleanStale(sessionTimeoutMs * 2);
67623
67757
  if (staleIds.length > 0) {
67624
- log27.info(`\uD83E\uDDF9 Soft-deleted ${staleIds.length} stale session(s) (kept for history)`);
67758
+ log28.info(`\uD83E\uDDF9 Soft-deleted ${staleIds.length} stale session(s) (kept for history)`);
67625
67759
  }
67626
67760
  const removedCount = this.sessionStore.cleanHistory();
67627
67761
  if (removedCount > 0) {
67628
- log27.info(`\uD83D\uDDD1️ Permanently removed ${removedCount} old session(s) from history`);
67762
+ log28.info(`\uD83D\uDDD1️ Permanently removed ${removedCount} old session(s) from history`);
67629
67763
  }
67630
67764
  const persisted = this.sessionStore.load();
67631
- log27.info(`\uD83D\uDCC2 Loaded ${persisted.size} session(s) from persistence`);
67765
+ log28.info(`\uD83D\uDCC2 Loaded ${persisted.size} session(s) from persistence`);
67632
67766
  const excludePostIdsByPlatform = new Map;
67633
67767
  for (const session of persisted.values()) {
67634
67768
  const platformId = session.platformId;
@@ -67648,10 +67782,10 @@ class SessionManager extends EventEmitter4 {
67648
67782
  const excludePostIds = excludePostIdsByPlatform.get(platform.platformId);
67649
67783
  platform.getBotUser().then((botUser) => {
67650
67784
  cleanupOldStickyMessages(platform, botUser.id, true, excludePostIds).catch((err) => {
67651
- log27.warn(`Failed to cleanup old sticky messages for ${platform.platformId}: ${err}`);
67785
+ log28.warn(`Failed to cleanup old sticky messages for ${platform.platformId}: ${err}`);
67652
67786
  });
67653
67787
  }).catch((err) => {
67654
- log27.warn(`Failed to get bot user for cleanup on ${platform.platformId}: ${err}`);
67788
+ log28.warn(`Failed to get bot user for cleanup on ${platform.platformId}: ${err}`);
67655
67789
  });
67656
67790
  }
67657
67791
  if (persisted.size > 0) {
@@ -67665,10 +67799,10 @@ class SessionManager extends EventEmitter4 {
67665
67799
  }
67666
67800
  }
67667
67801
  if (pausedToSkip.length > 0) {
67668
- log27.info(`⏸️ ${pausedToSkip.length} session(s) remain paused (waiting for user message)`);
67802
+ log28.info(`⏸️ ${pausedToSkip.length} session(s) remain paused (waiting for user message)`);
67669
67803
  }
67670
67804
  if (activeToResume.length > 0) {
67671
- log27.info(`\uD83D\uDD04 Attempting to resume ${activeToResume.length} active session(s)...`);
67805
+ log28.info(`\uD83D\uDD04 Attempting to resume ${activeToResume.length} active session(s)...`);
67672
67806
  for (const state of activeToResume) {
67673
67807
  await resumeSession(state, this.getContext());
67674
67808
  }
@@ -67786,6 +67920,24 @@ class SessionManager extends EventEmitter4 {
67786
67920
  return;
67787
67921
  await deferUpdate(session, username, this.autoUpdateManager);
67788
67922
  }
67923
+ async pluginList(threadId) {
67924
+ const session = this.findSessionByThreadId(threadId);
67925
+ if (!session)
67926
+ return;
67927
+ await handlePluginList(session);
67928
+ }
67929
+ async pluginInstall(threadId, pluginName, username) {
67930
+ const session = this.findSessionByThreadId(threadId);
67931
+ if (!session)
67932
+ return;
67933
+ await handlePluginInstall(session, pluginName, username, this.getContext());
67934
+ }
67935
+ async pluginUninstall(threadId, pluginName, username) {
67936
+ const session = this.findSessionByThreadId(threadId);
67937
+ if (!session)
67938
+ return;
67939
+ await handlePluginUninstall(session, pluginName, username, this.getContext());
67940
+ }
67789
67941
  isSessionInteractive(threadId) {
67790
67942
  const session = this.findSessionByThreadId(threadId);
67791
67943
  if (!session)
@@ -67988,7 +68140,7 @@ class SessionManager extends EventEmitter4 {
67988
68140
  const message = messageBuilder(formatter);
67989
68141
  await post(session, "info", message);
67990
68142
  } catch (err) {
67991
- log27.warn(`Failed to broadcast to session ${session.threadId}: ${err}`);
68143
+ log28.warn(`Failed to broadcast to session ${session.threadId}: ${err}`);
67992
68144
  }
67993
68145
  }
67994
68146
  }
@@ -68007,7 +68159,7 @@ class SessionManager extends EventEmitter4 {
68007
68159
  session.messageManager?.setPendingUpdatePrompt({ postId: post2.id });
68008
68160
  this.registerPost(post2.id, session.threadId);
68009
68161
  } catch (err) {
68010
- log27.warn(`Failed to post ask message to ${threadId}: ${err}`);
68162
+ log28.warn(`Failed to post ask message to ${threadId}: ${err}`);
68011
68163
  }
68012
68164
  }
68013
68165
  }
@@ -76583,29 +76735,29 @@ function SessionLog({ logs, maxLines = 20 }) {
76583
76735
  return /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
76584
76736
  flexDirection: "column",
76585
76737
  flexShrink: 0,
76586
- children: displayLogs.map((log28) => /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
76738
+ children: displayLogs.map((log29) => /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Box_default, {
76587
76739
  flexShrink: 0,
76588
76740
  children: [
76589
76741
  /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
76590
- color: getColorForLevel(log28.level),
76742
+ color: getColorForLevel(log29.level),
76591
76743
  dimColor: true,
76592
76744
  wrap: "truncate",
76593
76745
  children: [
76594
76746
  "[",
76595
- padComponent(log28.component),
76747
+ padComponent(log29.component),
76596
76748
  "]"
76597
76749
  ]
76598
76750
  }, undefined, true, undefined, this),
76599
76751
  /* @__PURE__ */ jsx_dev_runtime4.jsxDEV(Text, {
76600
- color: getColorForLevel(log28.level),
76752
+ color: getColorForLevel(log29.level),
76601
76753
  wrap: "truncate",
76602
76754
  children: [
76603
76755
  " ",
76604
- log28.message
76756
+ log29.message
76605
76757
  ]
76606
76758
  }, undefined, true, undefined, this)
76607
76759
  ]
76608
- }, log28.id, true, undefined, this))
76760
+ }, log29.id, true, undefined, this))
76609
76761
  }, undefined, false, undefined, this);
76610
76762
  }
76611
76763
  // src/ui/components/Footer.tsx
@@ -77103,7 +77255,7 @@ function LogPanel({ logs, maxLines = 10, focused = false }) {
77103
77255
  const scrollRef = import_react59.default.useRef(null);
77104
77256
  const { stdout } = use_stdout_default();
77105
77257
  const isDebug = process.env.DEBUG === "1";
77106
- const displayLogs = logs.filter((log28) => isDebug || log28.level !== "debug");
77258
+ const displayLogs = logs.filter((log29) => isDebug || log29.level !== "debug");
77107
77259
  const visibleLogs = displayLogs.slice(-Math.max(maxLines * 3, 100));
77108
77260
  import_react59.default.useEffect(() => {
77109
77261
  const handleResize = () => scrollRef.current?.remeasure();
@@ -77143,25 +77295,25 @@ function LogPanel({ logs, maxLines = 10, focused = false }) {
77143
77295
  overflow: "hidden",
77144
77296
  children: /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(ScrollView, {
77145
77297
  ref: scrollRef,
77146
- children: visibleLogs.map((log28) => /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Box_default, {
77298
+ children: visibleLogs.map((log29) => /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Box_default, {
77147
77299
  children: [
77148
77300
  /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Text, {
77149
77301
  dimColor: true,
77150
77302
  children: [
77151
77303
  "[",
77152
- padComponent2(log28.component),
77304
+ padComponent2(log29.component),
77153
77305
  "]"
77154
77306
  ]
77155
77307
  }, undefined, true, undefined, this),
77156
77308
  /* @__PURE__ */ jsx_dev_runtime6.jsxDEV(Text, {
77157
- color: getLevelColor(log28.level),
77309
+ color: getLevelColor(log29.level),
77158
77310
  children: [
77159
77311
  " ",
77160
- log28.message
77312
+ log29.message
77161
77313
  ]
77162
77314
  }, undefined, true, undefined, this)
77163
77315
  ]
77164
- }, log28.id, true, undefined, this))
77316
+ }, log29.id, true, undefined, this))
77165
77317
  }, undefined, false, undefined, this)
77166
77318
  }, undefined, false, undefined, this);
77167
77319
  }
@@ -77678,10 +77830,10 @@ function useAppState(initialConfig) {
77678
77830
  });
77679
77831
  }, []);
77680
77832
  const getLogsForSession = import_react60.useCallback((sessionId) => {
77681
- return state.logs.filter((log28) => log28.sessionId === sessionId);
77833
+ return state.logs.filter((log29) => log29.sessionId === sessionId);
77682
77834
  }, [state.logs]);
77683
77835
  const getGlobalLogs = import_react60.useCallback(() => {
77684
- return state.logs.filter((log28) => !log28.sessionId);
77836
+ return state.logs.filter((log29) => !log29.sessionId);
77685
77837
  }, [state.logs]);
77686
77838
  const togglePlatformEnabled = import_react60.useCallback((platformId) => {
77687
77839
  let newEnabled = false;
@@ -78599,14 +78751,6 @@ Release notes not available. See ${formatter.formatLink("GitHub releases", "http
78599
78751
  }
78600
78752
  return;
78601
78753
  }
78602
- case "context":
78603
- case "cost":
78604
- case "compact":
78605
- if (isAllowed) {
78606
- const claudeCommand = "/" + parsed.command;
78607
- await session.sendFollowUp(threadRoot, claudeCommand);
78608
- }
78609
- return;
78610
78754
  case "bug":
78611
78755
  if (isAllowed) {
78612
78756
  const files3 = post2.metadata?.files;
@@ -78615,6 +78759,47 @@ Release notes not available. See ${formatter.formatLink("GitHub releases", "http
78615
78759
  return;
78616
78760
  case "kill":
78617
78761
  return;
78762
+ case "plugin": {
78763
+ if (!isAllowed)
78764
+ return;
78765
+ const pluginArgs = parsed.args?.split(/\s+/) || [];
78766
+ const subcommand = pluginArgs[0]?.toLowerCase() || "list";
78767
+ const pluginName = pluginArgs.slice(1).join(" ");
78768
+ switch (subcommand) {
78769
+ case "list":
78770
+ await session.pluginList(threadRoot);
78771
+ break;
78772
+ case "install":
78773
+ if (!pluginName) {
78774
+ await client.createPost(`❌ Usage: ${formatter.formatCode("!plugin install <plugin-name>")}`, threadRoot);
78775
+ } else {
78776
+ await session.pluginInstall(threadRoot, pluginName, username);
78777
+ }
78778
+ break;
78779
+ case "uninstall":
78780
+ if (!pluginName) {
78781
+ await client.createPost(`❌ Usage: ${formatter.formatCode("!plugin uninstall <plugin-name>")}`, threadRoot);
78782
+ } else {
78783
+ await session.pluginUninstall(threadRoot, pluginName, username);
78784
+ }
78785
+ break;
78786
+ default:
78787
+ await client.createPost(`❌ Unknown subcommand: ${formatter.formatCode(subcommand)}
78788
+ Usage: ${formatter.formatCode("!plugin <list|install|uninstall> [name]")}`, threadRoot);
78789
+ }
78790
+ return;
78791
+ }
78792
+ default: {
78793
+ const defaultPassthroughCommands = new Set(["context", "cost", "compact"]);
78794
+ const availableCommands = activeSession.availableSlashCommands ?? defaultPassthroughCommands;
78795
+ if (availableCommands.has(parsed.command)) {
78796
+ if (isAllowed) {
78797
+ const claudeCommand = "/" + parsed.command + (parsed.args ? " " + parsed.args : "");
78798
+ await session.sendFollowUp(threadRoot, claudeCommand);
78799
+ }
78800
+ }
78801
+ return;
78802
+ }
78618
78803
  }
78619
78804
  }
78620
78805
  if (session.hasPendingWorktreePrompt(threadRoot)) {
@@ -78691,7 +78876,7 @@ import { EventEmitter as EventEmitter9 } from "events";
78691
78876
 
78692
78877
  // src/auto-update/checker.ts
78693
78878
  import { EventEmitter as EventEmitter7 } from "events";
78694
- var log28 = createLogger("checker");
78879
+ var log29 = createLogger("checker");
78695
78880
  var PACKAGE_NAME = "claude-threads";
78696
78881
  function compareVersions(a, b) {
78697
78882
  const partsA = a.replace(/^v/, "").split(".").map(Number);
@@ -78714,13 +78899,13 @@ async function fetchLatestVersion() {
78714
78899
  }
78715
78900
  });
78716
78901
  if (!response.ok) {
78717
- log28.warn(`Failed to fetch latest version: HTTP ${response.status}`);
78902
+ log29.warn(`Failed to fetch latest version: HTTP ${response.status}`);
78718
78903
  return null;
78719
78904
  }
78720
78905
  const data = await response.json();
78721
78906
  return data.version ?? null;
78722
78907
  } catch (err) {
78723
- log28.warn(`Failed to fetch latest version: ${err}`);
78908
+ log29.warn(`Failed to fetch latest version: ${err}`);
78724
78909
  return null;
78725
78910
  }
78726
78911
  }
@@ -78737,38 +78922,38 @@ class UpdateChecker extends EventEmitter7 {
78737
78922
  }
78738
78923
  start() {
78739
78924
  if (!this.config.enabled) {
78740
- log28.debug("Auto-update disabled, not starting checker");
78925
+ log29.debug("Auto-update disabled, not starting checker");
78741
78926
  return;
78742
78927
  }
78743
78928
  setTimeout(() => {
78744
78929
  this.check().catch((err) => {
78745
- log28.warn(`Initial update check failed: ${err}`);
78930
+ log29.warn(`Initial update check failed: ${err}`);
78746
78931
  });
78747
78932
  }, 5000);
78748
78933
  const intervalMs = this.config.checkIntervalMinutes * 60 * 1000;
78749
78934
  this.checkInterval = setInterval(() => {
78750
78935
  this.check().catch((err) => {
78751
- log28.warn(`Periodic update check failed: ${err}`);
78936
+ log29.warn(`Periodic update check failed: ${err}`);
78752
78937
  });
78753
78938
  }, intervalMs);
78754
- log28.info(`\uD83D\uDD04 Update checker started (every ${this.config.checkIntervalMinutes} minutes)`);
78939
+ log29.info(`\uD83D\uDD04 Update checker started (every ${this.config.checkIntervalMinutes} minutes)`);
78755
78940
  }
78756
78941
  stop() {
78757
78942
  if (this.checkInterval) {
78758
78943
  clearInterval(this.checkInterval);
78759
78944
  this.checkInterval = null;
78760
78945
  }
78761
- log28.debug("Update checker stopped");
78946
+ log29.debug("Update checker stopped");
78762
78947
  }
78763
78948
  async check() {
78764
78949
  if (this.isChecking) {
78765
- log28.debug("Check already in progress, skipping");
78950
+ log29.debug("Check already in progress, skipping");
78766
78951
  return this.lastUpdateInfo;
78767
78952
  }
78768
78953
  this.isChecking = true;
78769
78954
  this.emit("check:start");
78770
78955
  try {
78771
- log28.debug("Checking for updates...");
78956
+ log29.debug("Checking for updates...");
78772
78957
  const latestVersion2 = await fetchLatestVersion();
78773
78958
  if (!latestVersion2) {
78774
78959
  this.emit("check:complete", false);
@@ -78785,18 +78970,18 @@ class UpdateChecker extends EventEmitter7 {
78785
78970
  detectedAt: new Date
78786
78971
  };
78787
78972
  if (!this.lastUpdateInfo || this.lastUpdateInfo.latestVersion !== latestVersion2) {
78788
- log28.info(`\uD83C\uDD95 Update available: v${currentVersion} → v${latestVersion2}`);
78973
+ log29.info(`\uD83C\uDD95 Update available: v${currentVersion} → v${latestVersion2}`);
78789
78974
  this.lastUpdateInfo = updateInfo;
78790
78975
  this.emit("update", updateInfo);
78791
78976
  }
78792
78977
  this.emit("check:complete", true);
78793
78978
  return updateInfo;
78794
78979
  }
78795
- log28.debug(`Up to date (v${currentVersion})`);
78980
+ log29.debug(`Up to date (v${currentVersion})`);
78796
78981
  this.emit("check:complete", false);
78797
78982
  return null;
78798
78983
  } catch (err) {
78799
- log28.warn(`Update check failed: ${err}`);
78984
+ log29.warn(`Update check failed: ${err}`);
78800
78985
  this.emit("check:error", err);
78801
78986
  return null;
78802
78987
  } finally {
@@ -78866,7 +79051,7 @@ function isInScheduledWindow(window2) {
78866
79051
  }
78867
79052
 
78868
79053
  // src/auto-update/scheduler.ts
78869
- var log29 = createLogger("scheduler");
79054
+ var log30 = createLogger("scheduler");
78870
79055
 
78871
79056
  class UpdateScheduler extends EventEmitter8 {
78872
79057
  config;
@@ -78890,7 +79075,7 @@ class UpdateScheduler extends EventEmitter8 {
78890
79075
  scheduleUpdate(updateInfo) {
78891
79076
  this.pendingUpdate = updateInfo;
78892
79077
  if (this.config.autoRestartMode === "immediate") {
78893
- log29.info("Immediate mode: triggering update now");
79078
+ log30.info("Immediate mode: triggering update now");
78894
79079
  this.emit("ready", updateInfo);
78895
79080
  return;
78896
79081
  }
@@ -78903,19 +79088,19 @@ class UpdateScheduler extends EventEmitter8 {
78903
79088
  this.scheduledRestartAt = null;
78904
79089
  this.askApprovals.clear();
78905
79090
  this.askStartTime = null;
78906
- log29.debug("Update schedule cancelled");
79091
+ log30.debug("Update schedule cancelled");
78907
79092
  }
78908
79093
  deferUpdate(minutes) {
78909
79094
  const deferUntil = new Date(Date.now() + minutes * 60 * 1000);
78910
79095
  this.scheduledRestartAt = null;
78911
79096
  this.idleStartTime = null;
78912
79097
  this.emit("deferred", deferUntil);
78913
- log29.info(`Update deferred until ${deferUntil.toLocaleTimeString()}`);
79098
+ log30.info(`Update deferred until ${deferUntil.toLocaleTimeString()}`);
78914
79099
  return deferUntil;
78915
79100
  }
78916
79101
  recordAskResponse(threadId, approved) {
78917
79102
  this.askApprovals.set(threadId, approved);
78918
- log29.debug(`Thread ${threadId.substring(0, 8)} ${approved ? "approved" : "denied"} update`);
79103
+ log30.debug(`Thread ${threadId.substring(0, 8)} ${approved ? "approved" : "denied"} update`);
78919
79104
  this.checkAskCondition();
78920
79105
  }
78921
79106
  getScheduledRestartAt() {
@@ -78936,7 +79121,7 @@ class UpdateScheduler extends EventEmitter8 {
78936
79121
  return;
78937
79122
  this.checkCondition();
78938
79123
  this.checkTimer = setInterval(() => this.checkCondition(), 1e4);
78939
- log29.debug(`Started checking for ${this.config.autoRestartMode} condition`);
79124
+ log30.debug(`Started checking for ${this.config.autoRestartMode} condition`);
78940
79125
  }
78941
79126
  stopChecking() {
78942
79127
  if (this.checkTimer) {
@@ -78967,17 +79152,17 @@ class UpdateScheduler extends EventEmitter8 {
78967
79152
  if (activity.activeSessionCount === 0) {
78968
79153
  if (!this.idleStartTime) {
78969
79154
  this.idleStartTime = new Date;
78970
- log29.debug("No active sessions, starting idle timer");
79155
+ log30.debug("No active sessions, starting idle timer");
78971
79156
  }
78972
79157
  const idleMs = Date.now() - this.idleStartTime.getTime();
78973
79158
  const requiredMs = this.config.idleTimeoutMinutes * 60 * 1000;
78974
79159
  if (idleMs >= requiredMs) {
78975
- log29.info(`Idle for ${this.config.idleTimeoutMinutes} minutes, triggering update`);
79160
+ log30.info(`Idle for ${this.config.idleTimeoutMinutes} minutes, triggering update`);
78976
79161
  this.triggerCountdown();
78977
79162
  }
78978
79163
  } else {
78979
79164
  if (this.idleStartTime) {
78980
- log29.debug("Sessions became active, resetting idle timer");
79165
+ log30.debug("Sessions became active, resetting idle timer");
78981
79166
  this.idleStartTime = null;
78982
79167
  }
78983
79168
  }
@@ -78988,7 +79173,7 @@ class UpdateScheduler extends EventEmitter8 {
78988
79173
  const quietMs = Date.now() - activity.lastActivityAt.getTime();
78989
79174
  const requiredMs = this.config.quietTimeoutMinutes * 60 * 1000;
78990
79175
  if (quietMs >= requiredMs && !activity.anySessionBusy) {
78991
- log29.info(`Sessions quiet for ${this.config.quietTimeoutMinutes} minutes, triggering update`);
79176
+ log30.info(`Sessions quiet for ${this.config.quietTimeoutMinutes} minutes, triggering update`);
78992
79177
  this.triggerCountdown();
78993
79178
  }
78994
79179
  } else if (activity.activeSessionCount === 0) {
@@ -78998,7 +79183,7 @@ class UpdateScheduler extends EventEmitter8 {
78998
79183
  const idleMs = Date.now() - this.idleStartTime.getTime();
78999
79184
  const requiredMs = this.config.quietTimeoutMinutes * 60 * 1000;
79000
79185
  if (idleMs >= requiredMs) {
79001
- log29.info("No sessions and quiet timeout reached, triggering update");
79186
+ log30.info("No sessions and quiet timeout reached, triggering update");
79002
79187
  this.triggerCountdown();
79003
79188
  }
79004
79189
  }
@@ -79009,13 +79194,13 @@ class UpdateScheduler extends EventEmitter8 {
79009
79194
  }
79010
79195
  const activity = this.getSessionActivity();
79011
79196
  if (activity.activeSessionCount === 0) {
79012
- log29.info("Within scheduled window and no active sessions, triggering update");
79197
+ log30.info("Within scheduled window and no active sessions, triggering update");
79013
79198
  this.triggerCountdown();
79014
79199
  } else if (activity.lastActivityAt) {
79015
79200
  const quietMs = Date.now() - activity.lastActivityAt.getTime();
79016
79201
  const requiredMs = this.config.idleTimeoutMinutes * 60 * 1000;
79017
79202
  if (quietMs >= requiredMs && !activity.anySessionBusy) {
79018
- log29.info("Within scheduled window and sessions quiet, triggering update");
79203
+ log30.info("Within scheduled window and sessions quiet, triggering update");
79019
79204
  this.triggerCountdown();
79020
79205
  }
79021
79206
  }
@@ -79023,14 +79208,14 @@ class UpdateScheduler extends EventEmitter8 {
79023
79208
  checkAskCondition() {
79024
79209
  const threadIds = this.getActiveThreadIds();
79025
79210
  if (threadIds.length === 0) {
79026
- log29.info("No active threads, proceeding with update");
79211
+ log30.info("No active threads, proceeding with update");
79027
79212
  this.triggerCountdown();
79028
79213
  return;
79029
79214
  }
79030
79215
  if (!this.askStartTime && this.pendingUpdate) {
79031
79216
  this.askStartTime = new Date;
79032
79217
  this.postAskMessage(threadIds, this.pendingUpdate.latestVersion).catch((err) => {
79033
- log29.warn(`Failed to post ask message: ${err}`);
79218
+ log30.warn(`Failed to post ask message: ${err}`);
79034
79219
  });
79035
79220
  return;
79036
79221
  }
@@ -79043,12 +79228,12 @@ class UpdateScheduler extends EventEmitter8 {
79043
79228
  denials++;
79044
79229
  }
79045
79230
  if (approvals > threadIds.length / 2) {
79046
- log29.info(`Majority approved (${approvals}/${threadIds.length}), triggering update`);
79231
+ log30.info(`Majority approved (${approvals}/${threadIds.length}), triggering update`);
79047
79232
  this.triggerCountdown();
79048
79233
  return;
79049
79234
  }
79050
79235
  if (denials > threadIds.length / 2) {
79051
- log29.info(`Majority denied (${denials}/${threadIds.length}), deferring update`);
79236
+ log30.info(`Majority denied (${denials}/${threadIds.length}), deferring update`);
79052
79237
  this.deferUpdate(60);
79053
79238
  return;
79054
79239
  }
@@ -79056,7 +79241,7 @@ class UpdateScheduler extends EventEmitter8 {
79056
79241
  const elapsedMs = Date.now() - this.askStartTime.getTime();
79057
79242
  const timeoutMs = this.config.askTimeoutMinutes * 60 * 1000;
79058
79243
  if (elapsedMs >= timeoutMs) {
79059
- log29.info(`Ask timeout reached (${this.config.askTimeoutMinutes} min), triggering update`);
79244
+ log30.info(`Ask timeout reached (${this.config.askTimeoutMinutes} min), triggering update`);
79060
79245
  this.triggerCountdown();
79061
79246
  }
79062
79247
  }
@@ -79076,7 +79261,7 @@ class UpdateScheduler extends EventEmitter8 {
79076
79261
  this.emit("ready", this.pendingUpdate);
79077
79262
  }
79078
79263
  }, 1000);
79079
- log29.info("Update countdown started (60 seconds)");
79264
+ log30.info("Update countdown started (60 seconds)");
79080
79265
  }
79081
79266
  stopCountdown() {
79082
79267
  if (this.countdownTimer) {
@@ -79087,11 +79272,69 @@ class UpdateScheduler extends EventEmitter8 {
79087
79272
  }
79088
79273
 
79089
79274
  // src/auto-update/installer.ts
79090
- import { spawn as spawn7 } from "child_process";
79275
+ import { spawn as spawn8, spawnSync } from "child_process";
79091
79276
  import { existsSync as existsSync13, readFileSync as readFileSync9, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
79092
79277
  import { dirname as dirname8, resolve as resolve6 } from "path";
79093
79278
  import { homedir as homedir5 } from "os";
79094
- var log30 = createLogger("installer");
79279
+ var log31 = createLogger("installer");
79280
+ function detectPackageManager() {
79281
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
79282
+ const originalInstaller = detectOriginalInstaller();
79283
+ if (originalInstaller) {
79284
+ log31.debug(`Detected original installer: ${originalInstaller}`);
79285
+ if (originalInstaller === "bun") {
79286
+ const bunCheck2 = spawnSync("bun", ["--version"], { stdio: "ignore" });
79287
+ if (bunCheck2.status === 0) {
79288
+ return { cmd: "bun", isBun: true };
79289
+ }
79290
+ log31.warn("Originally installed with bun, but bun not found. Falling back to npm.");
79291
+ } else {
79292
+ const npmCheck2 = spawnSync(npmCmd, ["--version"], { stdio: "ignore" });
79293
+ if (npmCheck2.status === 0) {
79294
+ return { cmd: npmCmd, isBun: false };
79295
+ }
79296
+ log31.warn("Originally installed with npm, but npm not found. Falling back to bun.");
79297
+ }
79298
+ }
79299
+ const bunCheck = spawnSync("bun", ["--version"], { stdio: "ignore" });
79300
+ if (bunCheck.status === 0) {
79301
+ return { cmd: "bun", isBun: true };
79302
+ }
79303
+ const npmCheck = spawnSync(npmCmd, ["--version"], { stdio: "ignore" });
79304
+ if (npmCheck.status === 0) {
79305
+ return { cmd: npmCmd, isBun: false };
79306
+ }
79307
+ return null;
79308
+ }
79309
+ function normalizePath(p) {
79310
+ if (process.platform === "win32") {
79311
+ return p.toLowerCase();
79312
+ }
79313
+ return p;
79314
+ }
79315
+ function detectOriginalInstaller() {
79316
+ try {
79317
+ const scriptPath = normalizePath(process.argv[1] || "");
79318
+ const bunGlobalDir = normalizePath(process.env.BUN_INSTALL || resolve6(homedir5(), ".bun"));
79319
+ if (scriptPath.startsWith(bunGlobalDir)) {
79320
+ return "bun";
79321
+ }
79322
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
79323
+ const npmPrefixResult = spawnSync(npmCmd, ["prefix", "-g"], {
79324
+ encoding: "utf-8",
79325
+ stdio: ["ignore", "pipe", "ignore"]
79326
+ });
79327
+ if (npmPrefixResult.status === 0 && npmPrefixResult.stdout) {
79328
+ const npmPrefix2 = normalizePath(npmPrefixResult.stdout.trim());
79329
+ if (scriptPath.startsWith(npmPrefix2)) {
79330
+ return "npm";
79331
+ }
79332
+ }
79333
+ return null;
79334
+ } catch {
79335
+ return null;
79336
+ }
79337
+ }
79095
79338
  var STATE_PATH = resolve6(homedir5(), ".config", "claude-threads", UPDATE_STATE_FILENAME);
79096
79339
  var PACKAGE_NAME2 = "claude-threads";
79097
79340
  function loadUpdateState() {
@@ -79101,7 +79344,7 @@ function loadUpdateState() {
79101
79344
  return JSON.parse(content);
79102
79345
  }
79103
79346
  } catch (err) {
79104
- log30.warn(`Failed to load update state: ${err}`);
79347
+ log31.warn(`Failed to load update state: ${err}`);
79105
79348
  }
79106
79349
  return {};
79107
79350
  }
@@ -79112,9 +79355,9 @@ function saveUpdateState(state) {
79112
79355
  mkdirSync4(dir, { recursive: true });
79113
79356
  }
79114
79357
  writeFileSync5(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
79115
- log30.debug("Update state saved");
79358
+ log31.debug("Update state saved");
79116
79359
  } catch (err) {
79117
- log30.warn(`Failed to save update state: ${err}`);
79360
+ log31.warn(`Failed to save update state: ${err}`);
79118
79361
  }
79119
79362
  }
79120
79363
  function clearUpdateState() {
@@ -79123,7 +79366,7 @@ function clearUpdateState() {
79123
79366
  writeFileSync5(STATE_PATH, "{}", "utf-8");
79124
79367
  }
79125
79368
  } catch (err) {
79126
- log30.warn(`Failed to clear update state: ${err}`);
79369
+ log31.warn(`Failed to clear update state: ${err}`);
79127
79370
  }
79128
79371
  }
79129
79372
  function checkJustUpdated() {
@@ -79155,7 +79398,13 @@ function clearRuntimeSettings() {
79155
79398
  }
79156
79399
  }
79157
79400
  async function installVersion(version) {
79158
- log30.info(`\uD83D\uDCE6 Installing ${PACKAGE_NAME2}@${version}...`);
79401
+ log31.info(`\uD83D\uDCE6 Installing ${PACKAGE_NAME2}@${version}...`);
79402
+ const pm = detectPackageManager();
79403
+ if (!pm) {
79404
+ const error = "Neither bun nor npm found in PATH. Cannot install update.";
79405
+ log31.error(`❌ ${error}`);
79406
+ return { success: false, error };
79407
+ }
79159
79408
  saveUpdateState({
79160
79409
  previousVersion: VERSION,
79161
79410
  targetVersion: version,
@@ -79163,11 +79412,10 @@ async function installVersion(version) {
79163
79412
  justUpdated: false
79164
79413
  });
79165
79414
  return new Promise((resolve7) => {
79166
- const useBun = process.platform !== "win32";
79167
- const cmd = useBun ? "bun" : "npm.cmd";
79168
- const args = useBun ? ["install", "-g", `${PACKAGE_NAME2}@${version}`] : ["install", "-g", `${PACKAGE_NAME2}@${version}`];
79169
- log30.debug(`Using ${useBun ? "bun" : "npm"} for installation`);
79170
- const child = spawn7(cmd, args, {
79415
+ const { cmd, isBun: isBun3 } = pm;
79416
+ const args = ["install", "-g", `${PACKAGE_NAME2}@${version}`];
79417
+ log31.debug(`Using ${isBun3 ? "bun" : "npm"} for installation`);
79418
+ const child = spawn8(cmd, args, {
79171
79419
  stdio: ["ignore", "pipe", "pipe"],
79172
79420
  env: {
79173
79421
  ...process.env,
@@ -79184,7 +79432,7 @@ async function installVersion(version) {
79184
79432
  });
79185
79433
  child.on("close", (code) => {
79186
79434
  if (code === 0) {
79187
- log30.info(`✅ Successfully installed ${PACKAGE_NAME2}@${version}`);
79435
+ log31.info(`✅ Successfully installed ${PACKAGE_NAME2}@${version}`);
79188
79436
  saveUpdateState({
79189
79437
  previousVersion: VERSION,
79190
79438
  targetVersion: version,
@@ -79194,20 +79442,20 @@ async function installVersion(version) {
79194
79442
  resolve7({ success: true });
79195
79443
  } else {
79196
79444
  const errorMsg = stderr || stdout || `Exit code: ${code}`;
79197
- log30.error(`❌ Installation failed: ${errorMsg}`);
79445
+ log31.error(`❌ Installation failed: ${errorMsg}`);
79198
79446
  clearUpdateState();
79199
79447
  resolve7({ success: false, error: errorMsg });
79200
79448
  }
79201
79449
  });
79202
79450
  child.on("error", (err) => {
79203
- log30.error(`❌ Failed to spawn npm: ${err}`);
79451
+ log31.error(`❌ Failed to spawn npm: ${err}`);
79204
79452
  clearUpdateState();
79205
79453
  resolve7({ success: false, error: err.message });
79206
79454
  });
79207
79455
  setTimeout(() => {
79208
79456
  if (child.exitCode === null) {
79209
79457
  child.kill();
79210
- log30.error("❌ Installation timed out");
79458
+ log31.error("❌ Installation timed out");
79211
79459
  clearUpdateState();
79212
79460
  resolve7({ success: false, error: "Installation timed out" });
79213
79461
  }
@@ -79215,7 +79463,8 @@ async function installVersion(version) {
79215
79463
  });
79216
79464
  }
79217
79465
  function getRollbackInstructions(previousVersion) {
79218
- const cmd = process.platform === "win32" ? "npm" : "bun";
79466
+ const pm = detectPackageManager();
79467
+ const cmd = pm?.isBun ? "bun" : "npm";
79219
79468
  return `To rollback to the previous version, run:
79220
79469
  ${cmd} install -g ${PACKAGE_NAME2}@${previousVersion}`;
79221
79470
  }
@@ -79248,7 +79497,7 @@ class UpdateInstaller {
79248
79497
  }
79249
79498
 
79250
79499
  // src/auto-update/manager.ts
79251
- var log31 = createLogger("updater");
79500
+ var log32 = createLogger("updater");
79252
79501
 
79253
79502
  class AutoUpdateManager extends EventEmitter9 {
79254
79503
  config;
@@ -79271,23 +79520,23 @@ class AutoUpdateManager extends EventEmitter9 {
79271
79520
  }
79272
79521
  start() {
79273
79522
  if (!this.config.enabled) {
79274
- log31.info("Auto-update is disabled");
79523
+ log32.info("Auto-update is disabled");
79275
79524
  return;
79276
79525
  }
79277
79526
  const updateResult = this.installer.checkJustUpdated();
79278
79527
  if (updateResult) {
79279
- log31.info(`\uD83C\uDF89 Updated from v${updateResult.previousVersion} to v${updateResult.currentVersion}`);
79528
+ log32.info(`\uD83C\uDF89 Updated from v${updateResult.previousVersion} to v${updateResult.currentVersion}`);
79280
79529
  this.callbacks.broadcastUpdate((fmt) => `\uD83C\uDF89 ${fmt.formatBold("Bot updated")} from v${updateResult.previousVersion} to v${updateResult.currentVersion}`).catch((err) => {
79281
- log31.warn(`Failed to broadcast update notification: ${err}`);
79530
+ log32.warn(`Failed to broadcast update notification: ${err}`);
79282
79531
  });
79283
79532
  }
79284
79533
  this.checker.start();
79285
- log31.info(`\uD83D\uDD04 Auto-update manager started (mode: ${this.config.autoRestartMode})`);
79534
+ log32.info(`\uD83D\uDD04 Auto-update manager started (mode: ${this.config.autoRestartMode})`);
79286
79535
  }
79287
79536
  stop() {
79288
79537
  this.checker.stop();
79289
79538
  this.scheduler.stop();
79290
- log31.debug("Auto-update manager stopped");
79539
+ log32.debug("Auto-update manager stopped");
79291
79540
  }
79292
79541
  getState() {
79293
79542
  return { ...this.state };
@@ -79301,10 +79550,10 @@ class AutoUpdateManager extends EventEmitter9 {
79301
79550
  async forceUpdate() {
79302
79551
  const updateInfo = this.state.updateInfo || await this.checker.check();
79303
79552
  if (!updateInfo) {
79304
- log31.info("No update available");
79553
+ log32.info("No update available");
79305
79554
  return;
79306
79555
  }
79307
- log31.info("Forcing immediate update");
79556
+ log32.info("Forcing immediate update");
79308
79557
  await this.performUpdate(updateInfo);
79309
79558
  }
79310
79559
  deferUpdate(minutes = 60) {
@@ -79359,7 +79608,7 @@ class AutoUpdateManager extends EventEmitter9 {
79359
79608
  await this.callbacks.broadcastUpdate((fmt) => `✅ ${fmt.formatBold("Update installed")} - restarting now. ${fmt.formatItalic("Sessions will resume automatically.")}`).catch(() => {});
79360
79609
  await new Promise((resolve7) => setTimeout(resolve7, 1000));
79361
79610
  await this.callbacks.prepareForRestart();
79362
- log31.info(`\uD83D\uDD04 Restarting for update to v${updateInfo.latestVersion}`);
79611
+ log32.info(`\uD83D\uDD04 Restarting for update to v${updateInfo.latestVersion}`);
79363
79612
  process.stdout.write("\x1B[2J\x1B[H");
79364
79613
  process.stdout.write("\x1B[?25h");
79365
79614
  process.exit(RESTART_EXIT_CODE);
@@ -79447,7 +79696,7 @@ async function main() {
79447
79696
  return false;
79448
79697
  };
79449
79698
  if (await shouldUseAutoRestart()) {
79450
- const { spawn: spawn8 } = await import("child_process");
79699
+ const { spawn: spawn9 } = await import("child_process");
79451
79700
  const { dirname: dirname9, resolve: resolve7 } = await import("path");
79452
79701
  const { fileURLToPath: fileURLToPath7 } = await import("url");
79453
79702
  const __filename2 = fileURLToPath7(import.meta.url);
@@ -79457,7 +79706,7 @@ async function main() {
79457
79706
  console.log("\uD83D\uDD04 Starting with auto-restart enabled...");
79458
79707
  console.log("");
79459
79708
  const binPath = __filename2;
79460
- const child = spawn8(daemonPath, ["--restart-on-error", ...args], {
79709
+ const child = spawn9(daemonPath, ["--restart-on-error", ...args], {
79461
79710
  stdio: "inherit",
79462
79711
  env: {
79463
79712
  ...process.env,
@@ -42509,6 +42509,19 @@ var COMMAND_REGISTRY = [
42509
42509
  claudeCanExecute: true,
42510
42510
  returnsResultToClaude: false
42511
42511
  },
42512
+ {
42513
+ command: "plugin",
42514
+ description: "Manage Claude Code plugins",
42515
+ args: "<subcommand> [name]",
42516
+ category: "system",
42517
+ audience: "user",
42518
+ claudeNotes: "User manages plugins themselves",
42519
+ subcommands: [
42520
+ { name: "list", description: "List installed plugins" },
42521
+ { name: "install", description: "Install a plugin (restarts Claude)", args: "<name>" },
42522
+ { name: "uninstall", description: "Uninstall a plugin (restarts Claude)", args: "<name>" }
42523
+ ]
42524
+ },
42512
42525
  {
42513
42526
  command: "context",
42514
42527
  description: "Show context usage",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.2.1",
3
+ "version": "1.3.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",