claude-threads 1.6.3 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53500,6 +53500,69 @@ import { fileURLToPath as fileURLToPath3 } from "url";
53500
53500
  import { existsSync as existsSync4, readFileSync as readFileSync3, watchFile, unwatchFile, unlinkSync, statSync, readdirSync } from "fs";
53501
53501
  import { tmpdir } from "os";
53502
53502
  import { join as join3 } from "path";
53503
+
53504
+ // src/claude/rate-limit-detector.ts
53505
+ var RATE_LIMIT_PHRASES = [
53506
+ /usage limit reached/i,
53507
+ /rate[_\s-]?limit[_\s-]?error/i,
53508
+ /you have hit the rate limit/i,
53509
+ /quota (has been )?exceeded/i,
53510
+ /\b429\b.*(rate|limit|quota)/i
53511
+ ];
53512
+ var DEFAULT_COOLDOWN_MS = 60 * 60 * 1000;
53513
+ function detectRateLimit(text, now = Date.now()) {
53514
+ if (!text)
53515
+ return { detected: false };
53516
+ let matched;
53517
+ for (const phrase of RATE_LIMIT_PHRASES) {
53518
+ const m = text.match(phrase);
53519
+ if (m) {
53520
+ matched = m[0];
53521
+ break;
53522
+ }
53523
+ }
53524
+ if (!matched)
53525
+ return { detected: false };
53526
+ const resetAtEpochMs = extractResetAt(text, now);
53527
+ return { detected: true, matched, resetAtEpochMs };
53528
+ }
53529
+ function cooldownDeadline(hit, now = Date.now()) {
53530
+ if (!hit.detected)
53531
+ return now;
53532
+ return hit.resetAtEpochMs ?? now + DEFAULT_COOLDOWN_MS;
53533
+ }
53534
+ function extractResetAt(text, now) {
53535
+ const relative = text.match(/(?:retry[_\s-]?after|resets?\s+in)\s+(\d+)\s*(second|minute|hour|day)s?/i);
53536
+ if (relative) {
53537
+ const value = parseInt(relative[1], 10);
53538
+ const unit = relative[2].toLowerCase();
53539
+ const unitMs = {
53540
+ second: 1000,
53541
+ minute: 60000,
53542
+ hour: 3600000,
53543
+ day: 86400000
53544
+ };
53545
+ return now + value * unitMs[unit];
53546
+ }
53547
+ const unix = text.match(/\breset(?:_at)?\b\s*["']?\s*[:=]\s*(\d{10,13})/);
53548
+ if (unix) {
53549
+ const raw = parseInt(unix[1], 10);
53550
+ return unix[1].length === 13 ? raw : raw * 1000;
53551
+ }
53552
+ const clock = text.match(/resets?\s+at\s+(\d{1,2}):(\d{2})\s*(utc|gmt)?/i);
53553
+ if (clock) {
53554
+ const hh = parseInt(clock[1], 10);
53555
+ const mm = parseInt(clock[2], 10);
53556
+ if (hh < 24 && mm < 60) {
53557
+ const reference = new Date(now);
53558
+ const target = new Date(Date.UTC(reference.getUTCFullYear(), reference.getUTCMonth(), reference.getUTCDate(), hh, mm)).getTime();
53559
+ return target > now ? target : target + 86400000;
53560
+ }
53561
+ }
53562
+ return;
53563
+ }
53564
+
53565
+ // src/claude/cli.ts
53503
53566
  var log7 = createLogger("claude");
53504
53567
  function cleanupBrowserBridgeSockets() {
53505
53568
  try {
@@ -53521,6 +53584,14 @@ function cleanupBrowserBridgeSockets() {
53521
53584
  log7.debug(`Browser bridge cleanup failed: ${err}`);
53522
53585
  }
53523
53586
  }
53587
+ function isErrorResultEvent(event) {
53588
+ const ev = event;
53589
+ if (typeof ev.subtype === "string" && ev.subtype.startsWith("error"))
53590
+ return true;
53591
+ if (ev.is_error === true)
53592
+ return true;
53593
+ return false;
53594
+ }
53524
53595
 
53525
53596
  class ClaudeCli extends EventEmitter2 {
53526
53597
  process = null;
@@ -53530,6 +53601,7 @@ class ClaudeCli extends EventEmitter2 {
53530
53601
  statusFilePath = null;
53531
53602
  lastStatusData = null;
53532
53603
  stderrBuffer = "";
53604
+ lastEmittedRateLimitDeadline = 0;
53533
53605
  log;
53534
53606
  constructor(options2) {
53535
53607
  super();
@@ -53581,6 +53653,7 @@ class ClaudeCli extends EventEmitter2 {
53581
53653
  if (this.process)
53582
53654
  throw new Error("Already running");
53583
53655
  this.stderrBuffer = "";
53656
+ this.lastEmittedRateLimitDeadline = 0;
53584
53657
  cleanupBrowserBridgeSockets();
53585
53658
  const claudePath = getClaudePath();
53586
53659
  const args = [
@@ -53650,9 +53723,13 @@ class ClaudeCli extends EventEmitter2 {
53650
53723
  args.push("--settings", JSON.stringify(statusLineSettings));
53651
53724
  }
53652
53725
  this.log.debug(`Starting: ${claudePath} ${args.slice(0, 5).join(" ")}...`);
53726
+ const childEnv = this.buildChildEnv();
53727
+ if (this.options.account) {
53728
+ this.log.debug(`Spawning under Claude account "${this.options.account.id}"`);
53729
+ }
53653
53730
  this.process = crossSpawn(claudePath, args, {
53654
53731
  cwd: this.options.workingDir,
53655
- env: process.env,
53732
+ env: childEnv,
53656
53733
  stdio: ["pipe", "pipe", "pipe"]
53657
53734
  });
53658
53735
  this.log.debug(`Claude process spawned: pid=${this.process.pid}`);
@@ -53666,6 +53743,7 @@ class ClaudeCli extends EventEmitter2 {
53666
53743
  this.stderrBuffer = this.stderrBuffer.slice(-10240);
53667
53744
  }
53668
53745
  this.log.debug(`stderr: ${text.trim()}`);
53746
+ this.maybeEmitRateLimit(text);
53669
53747
  });
53670
53748
  this.process.on("error", (err) => {
53671
53749
  this.log.error(`Claude error: ${err}`);
@@ -53720,9 +53798,24 @@ class ClaudeCli extends EventEmitter2 {
53720
53798
  try {
53721
53799
  const event = JSON.parse(trimmed);
53722
53800
  this.emit("event", event);
53801
+ if (event.type === "result" && isErrorResultEvent(event)) {
53802
+ this.maybeEmitRateLimit(trimmed);
53803
+ }
53723
53804
  } catch {}
53724
53805
  }
53725
53806
  }
53807
+ maybeEmitRateLimit(text) {
53808
+ const hit = detectRateLimit(text);
53809
+ if (!hit.detected)
53810
+ return;
53811
+ const newDeadline = cooldownDeadline(hit);
53812
+ const MIN_ADVANCE_MS = 60000;
53813
+ if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS)
53814
+ return;
53815
+ this.lastEmittedRateLimitDeadline = newDeadline;
53816
+ this.log.warn(`Rate limit detected: ${hit.matched ?? "(no match text)"}`);
53817
+ this.emit("rate-limit", hit);
53818
+ }
53726
53819
  isRunning() {
53727
53820
  return this.process !== null;
53728
53821
  }
@@ -53791,6 +53884,22 @@ class ClaudeCli extends EventEmitter2 {
53791
53884
  this.process.kill("SIGINT");
53792
53885
  return true;
53793
53886
  }
53887
+ buildChildEnv() {
53888
+ const account = this.options.account;
53889
+ if (!account)
53890
+ return process.env;
53891
+ const env = { ...process.env };
53892
+ if (account.home) {
53893
+ env.HOME = account.home;
53894
+ env.USERPROFILE = account.home;
53895
+ delete env.ANTHROPIC_API_KEY;
53896
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
53897
+ } else if (account.apiKey) {
53898
+ env.ANTHROPIC_API_KEY = account.apiKey;
53899
+ delete env.CLAUDE_CODE_OAUTH_TOKEN;
53900
+ }
53901
+ return env;
53902
+ }
53794
53903
  getMcpServerPath() {
53795
53904
  const __filename2 = fileURLToPath3(import.meta.url);
53796
53905
  const __dirname4 = dirname4(__filename2);
@@ -53811,42 +53920,6 @@ class ClaudeCli extends EventEmitter2 {
53811
53920
  }
53812
53921
  }
53813
53922
 
53814
- // src/update-notifier.ts
53815
- var import_semver2 = __toESM(require_semver2(), 1);
53816
-
53817
- // src/operations/commands/handler.ts
53818
- init_emoji();
53819
-
53820
- // src/utils/error-handler/index.ts
53821
- var log8 = createLogger("error");
53822
-
53823
- // src/utils/session-log.ts
53824
- function createSessionLog(baseLog) {
53825
- return (session) => {
53826
- if (session?.sessionId) {
53827
- return baseLog.forSession(session.sessionId);
53828
- }
53829
- return baseLog;
53830
- };
53831
- }
53832
-
53833
- // src/operations/post-helpers/index.ts
53834
- init_emoji();
53835
-
53836
- // src/git/worktree.ts
53837
- import * as path from "path";
53838
- import { homedir as homedir2 } from "os";
53839
- var log9 = createLogger("git-wt");
53840
- var WORKTREES_DIR = path.join(homedir2(), ".claude-threads", "worktrees");
53841
- var METADATA_STORE_PATH = path.join(homedir2(), ".claude-threads", "worktree-metadata.json");
53842
-
53843
- // src/operations/post-helpers/index.ts
53844
- var log10 = createLogger("helpers");
53845
- var sessionLog = createSessionLog(log10);
53846
-
53847
- // src/claude/quick-query.ts
53848
- var log11 = createLogger("query");
53849
-
53850
53923
  // src/commands/registry.ts
53851
53924
  var COMMAND_REGISTRY = [
53852
53925
  {
@@ -54399,6 +54472,36 @@ ${avoidCommands.map((c) => `- \`!${c.command}\` - ${c.reason}`).join(`
54399
54472
  `)}
54400
54473
  `.trim();
54401
54474
  }
54475
+ // src/utils/error-handler/index.ts
54476
+ var log8 = createLogger("error");
54477
+
54478
+ // src/utils/session-log.ts
54479
+ function createSessionLog(baseLog) {
54480
+ return (session) => {
54481
+ if (session?.sessionId) {
54482
+ return baseLog.forSession(session.sessionId);
54483
+ }
54484
+ return baseLog;
54485
+ };
54486
+ }
54487
+
54488
+ // src/operations/post-helpers/index.ts
54489
+ init_emoji();
54490
+
54491
+ // src/git/worktree.ts
54492
+ import * as path from "path";
54493
+ import { homedir as homedir2 } from "os";
54494
+ var log9 = createLogger("git-wt");
54495
+ var WORKTREES_DIR = path.join(homedir2(), ".claude-threads", "worktrees");
54496
+ var METADATA_STORE_PATH = path.join(homedir2(), ".claude-threads", "worktree-metadata.json");
54497
+
54498
+ // src/operations/post-helpers/index.ts
54499
+ var log10 = createLogger("helpers");
54500
+ var sessionLog = createSessionLog(log10);
54501
+
54502
+ // src/claude/quick-query.ts
54503
+ var log11 = createLogger("query");
54504
+
54402
54505
  // src/operations/suggestions/title.ts
54403
54506
  var log12 = createLogger("title");
54404
54507
 
@@ -54416,7 +54519,11 @@ var log15 = createLogger("lifecycle");
54416
54519
  var sessionLog3 = createSessionLog(log15);
54417
54520
  var CHAT_PLATFORM_PROMPT = generateChatPlatformPrompt();
54418
54521
 
54522
+ // src/update-notifier.ts
54523
+ var import_semver2 = __toESM(require_semver2(), 1);
54524
+
54419
54525
  // src/operations/commands/handler.ts
54526
+ init_emoji();
54420
54527
  var log16 = createLogger("commands");
54421
54528
  var sessionLog4 = createSessionLog(log16);
54422
54529
  // src/operations/suggestions/branch.ts
@@ -72,6 +72,33 @@ platforms:
72
72
  | `allowedUsers` | No | List of Slack usernames |
73
73
  | `skipPermissions` | No | Auto-approve actions (default: `false`) |
74
74
 
75
+ ## Claude Accounts (optional, multi-account mode)
76
+
77
+ By default every session spawns `claude` with the bot's own `process.env`, so they all share one subscription's token budget. Add a `claudeAccounts` block to spread load across multiple accounts — the bot round-robins new sessions across the pool and automatically skips accounts in rate-limit cooldown. Omit the block entirely to stay in single-account mode (unchanged behavior).
78
+
79
+ ```yaml
80
+ claudeAccounts:
81
+ # OAuth accounts — prepare each HOME first with `HOME=<path> claude login`
82
+ - id: primary
83
+ home: /home/bot/.claude-accounts/primary
84
+ - id: backup
85
+ displayName: Backup (Pro)
86
+ home: /home/bot/.claude-accounts/backup
87
+
88
+ # API-key billed
89
+ - id: shared-api
90
+ apiKey: sk-ant-api03-xxxxxxxx...
91
+ ```
92
+
93
+ | Setting | Required | Description |
94
+ |---------|----------|-------------|
95
+ | `id` | Yes | Stable identifier used in logs, UI, and persisted session state |
96
+ | `home` | One of | Alternate `$HOME` containing `.claude/.credentials.json` from a prior `HOME=<path> claude login`. For OAuth Pro/Max subscriptions. Session history also lives here, so resumed sessions pick the same account. |
97
+ | `apiKey` | One of | Anthropic API key. Billed against that key; session history stays under the bot's default `HOME`. |
98
+ | `displayName` | No | Human-readable label in UI (defaults to `id`) |
99
+
100
+ Exactly one of `home` or `apiKey` should be set per account. Persisted sessions record which account they ran under and resume on the same one.
101
+
75
102
  ## Environment Variables
76
103
 
77
104
  | Variable | Description | Default |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.6.3",
3
+ "version": "1.7.1",
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",