@link-assistant/hive-mind 2.12.0 → 2.12.2

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.
@@ -11,7 +11,6 @@ import { join } from 'node:path';
11
11
  import { promisify } from 'node:util';
12
12
  import dayjs from 'dayjs';
13
13
  import utc from 'dayjs/plugin/utc.js';
14
-
15
14
  import { classifyCodexRateLimitWindows } from './codex-rate-limit-windows.lib.mjs';
16
15
  import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry, execGhWithRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller. execGhWithRetry adds transient-network retry (#1756).
17
16
  import { formatLimitResetsAt, formatLimitResetsIn, formatLocalizedCurrentTime, formatLocalizedRelativeTime, formatLocalizedResetTime, localizeCompactDuration, lt, resolveLimitLocale } from './limits-i18n.lib.mjs';
@@ -26,7 +25,6 @@ dayjs.extend(utc);
26
25
 
27
26
  // Import cache TTL configuration
28
27
  import { cacheTtl } from './config.lib.mjs';
29
-
30
28
  // Import centralized queue thresholds for progress bar visualization
31
29
  // This ensures thresholds are consistent between queue logic and display formatting
32
30
  // See: https://github.com/link-assistant/hive-mind/issues/1242
@@ -34,7 +32,6 @@ export { DISPLAY_THRESHOLDS } from './queue-config.lib.mjs';
34
32
  import { DISPLAY_THRESHOLDS } from './queue-config.lib.mjs';
35
33
 
36
34
  const execAsync = promisify(exec);
37
-
38
35
  /**
39
36
  * Default path to Claude credentials file
40
37
  */
@@ -47,7 +44,6 @@ const DEFAULT_CODEX_CONFIG_PATH = join(homedir(), '.codex', 'config.toml');
47
44
  */
48
45
  const USAGE_API_ENDPOINT = 'https://api.anthropic.com/api/oauth/usage';
49
46
  const CODEX_USAGE_API_DEFAULT_BASE_URL = 'https://chatgpt.com/backend-api';
50
-
51
47
  export function decodeJwtPayload(token) {
52
48
  if (!token || typeof token !== 'string') return null;
53
49
 
@@ -61,14 +57,12 @@ export function decodeJwtPayload(token) {
61
57
  return null;
62
58
  }
63
59
  }
64
-
65
60
  function unixSecondsToIsoDate(seconds) {
66
61
  if (seconds === null || seconds === undefined) return null;
67
62
  const numeric = Number(seconds);
68
63
  if (!Number.isFinite(numeric) || numeric <= 0) return null;
69
64
  return new Date(numeric * 1000).toISOString();
70
65
  }
71
-
72
66
  function mapCodexWindow(window) {
73
67
  const resetsAt = unixSecondsToIsoDate(window?.reset_at);
74
68
  return {
@@ -82,7 +76,6 @@ function mapCodexWindow(window) {
82
76
 
83
77
  export function mapCodexRateLimitWindows(rateLimit) {
84
78
  const { sessionWindow, weeklyWindow } = classifyCodexRateLimitWindows(rateLimit);
85
-
86
79
  return {
87
80
  currentSession: mapCodexWindow(sessionWindow),
88
81
  allModels: mapCodexWindow(weeklyWindow),
@@ -93,7 +86,6 @@ export async function readCodexAuth(authPath = DEFAULT_CODEX_AUTH_PATH, verbose
93
86
  try {
94
87
  const content = await readFile(authPath, 'utf-8');
95
88
  const auth = JSON.parse(content);
96
-
97
89
  if (verbose) {
98
90
  console.log('[VERBOSE] /limits Codex auth loaded from:', authPath);
99
91
  }
@@ -106,7 +98,6 @@ export async function readCodexAuth(authPath = DEFAULT_CODEX_AUTH_PATH, verbose
106
98
  return null;
107
99
  }
108
100
  }
109
-
110
101
  async function getCodexUsageBaseUrl(configPath = DEFAULT_CODEX_CONFIG_PATH, verbose = false) {
111
102
  try {
112
103
  const content = await readFile(configPath, 'utf-8');
@@ -115,7 +106,6 @@ async function getCodexUsageBaseUrl(configPath = DEFAULT_CODEX_CONFIG_PATH, verb
115
106
 
116
107
  const baseUrl = match[1].trim().replace(/\/+$/, '');
117
108
  const normalized = baseUrl.endsWith('/backend-api') ? baseUrl : `${baseUrl}/backend-api`;
118
-
119
109
  if (verbose) {
120
110
  console.log('[VERBOSE] /limits Codex base URL loaded from config:', normalized);
121
111
  }
@@ -129,7 +119,6 @@ async function getCodexUsageBaseUrl(configPath = DEFAULT_CODEX_CONFIG_PATH, verb
129
119
  return CODEX_USAGE_API_DEFAULT_BASE_URL;
130
120
  }
131
121
  }
132
-
133
122
  /**
134
123
  * Read Claude credentials from the credentials file
135
124
  *
@@ -141,7 +130,6 @@ export async function readCredentials(credentialsPath = DEFAULT_CREDENTIALS_PATH
141
130
  try {
142
131
  const content = await readFile(credentialsPath, 'utf-8');
143
132
  const credentials = JSON.parse(content);
144
-
145
133
  if (verbose) {
146
134
  console.log('[VERBOSE] /limits credentials loaded from:', credentialsPath);
147
135
  }
@@ -154,7 +142,6 @@ export async function readCredentials(credentialsPath = DEFAULT_CREDENTIALS_PATH
154
142
  return null;
155
143
  }
156
144
  }
157
-
158
145
  /**
159
146
  * Format a retry-after value into a user-friendly message.
160
147
  * The retry-after header can be either a number of seconds or an HTTP-date.
@@ -175,7 +162,6 @@ export function formatRetryAfterMessage(retryAfter) {
175
162
  // Calculate reset time from now + seconds
176
163
  const resetAt = dayjs().add(seconds, 'second').utc();
177
164
  const resetTimeStr = resetAt.format('MMM D, h:mma');
178
-
179
165
  // Format relative time
180
166
  const totalMinutes = Math.floor(seconds / 60);
181
167
  const remainingSeconds = Math.round(seconds % 60);
@@ -190,7 +176,6 @@ export function formatRetryAfterMessage(retryAfter) {
190
176
  } else {
191
177
  relativeStr = `${remainingSeconds}s`;
192
178
  }
193
-
194
179
  return ` Resets in ${relativeStr} (${resetTimeStr} UTC)`;
195
180
  }
196
181
 
@@ -207,7 +192,6 @@ export function formatRetryAfterMessage(retryAfter) {
207
192
  return ` Resets in ${relativeStr} (${resetTimeStr} UTC)`;
208
193
  }
209
194
  }
210
-
211
195
  // Fallback for 0, negative, or unparseable values - don't show misleading info
212
196
  return ' Try again later.';
213
197
  }
@@ -222,11 +206,9 @@ export function formatRetryAfterMessage(retryAfter) {
222
206
  function formatResetTime(isoDate, includeTimezone = true, options = {}) {
223
207
  return formatLocalizedResetTime(isoDate, includeTimezone, options);
224
208
  }
225
-
226
209
  function formatRelativeTime(isoDate, options = {}) {
227
210
  return formatLocalizedRelativeTime(isoDate, options);
228
211
  }
229
-
230
212
  /**
231
213
  * Format current time in UTC using dayjs
232
214
  *
@@ -251,7 +233,6 @@ function formatBytes(bytes) {
251
233
  const decimals = i >= 3 ? 1 : 0;
252
234
  return `${value.toFixed(decimals)} ${sizes[i]}`;
253
235
  }
254
-
255
236
  /**
256
237
  * @param {number} usedBytes - Used size in bytes
257
238
  * @param {number} totalBytes - Total size in bytes
@@ -275,7 +256,6 @@ function formatBytesRange(usedBytes, totalBytes, options = {}) {
275
256
  function formatRoundedNumber(value, decimals = 2) {
276
257
  return parseFloat(value.toFixed(decimals));
277
258
  }
278
-
279
259
  function getDisplayCpuCoresUsed(loadAvg5, cpuCount) {
280
260
  const boundedLoad = Math.min(Math.max(loadAvg5, 0), cpuCount);
281
261
  return formatRoundedNumber(boundedLoad);
@@ -284,7 +264,6 @@ function getDisplayCpuCoresUsed(loadAvg5, cpuCount) {
284
264
  function hasLimitPercentage(window) {
285
265
  return window?.percentage !== null && window?.percentage !== undefined;
286
266
  }
287
-
288
267
  function getLocalizedResetTime(window, options = {}) {
289
268
  if (!window) return null;
290
269
  return formatResetTime(window.resetsAt, true, options) || window.resetTime || null;
@@ -293,7 +272,6 @@ function getLocalizedResetTime(window, options = {}) {
293
272
  function getLocalizedRelativeReset(window, options = {}, fallbackRelative = null) {
294
273
  return formatRelativeTime(window?.resetsAt, options) || localizeCompactDuration(fallbackRelative, options);
295
274
  }
296
-
297
275
  function formatCodeBlock(content) {
298
276
  const text = Array.isArray(content) ? content.join('\n') : String(content ?? '');
299
277
  return '```\n' + (text.endsWith('\n') ? text : `${text}\n`) + '```';
@@ -303,14 +281,12 @@ function formatPlainTitledCodeSection(section) {
303
281
  const text = String(section ?? '').trimEnd();
304
282
  if (!text) return '';
305
283
  if (text.includes('```')) return text;
306
-
307
284
  const lines = text.split('\n');
308
285
  const title = lines.shift();
309
286
  const body = lines.join('\n');
310
287
  if (!body) return formatCodeBlock(title);
311
288
  return `${title}\n\n${formatCodeBlock(body)}`;
312
289
  }
313
-
314
290
  function formatLimitWindowSection(label, window, periodHours, threshold, options = {}) {
315
291
  const locale = resolveLimitLocale(options);
316
292
  let section = `${label}\n`;
@@ -324,7 +300,6 @@ function formatLimitWindowSection(label, window, periodHours, threshold, options
324
300
  const bar = getProgressBar(pct, threshold);
325
301
  const suffix = pct >= threshold ? ' ⚠️' : ` ${lt('used', {}, { locale })}`;
326
302
  section += `${bar} ${pct}%${suffix}\n`;
327
-
328
303
  const resetTime = getLocalizedResetTime(window, { locale });
329
304
  if (resetTime) {
330
305
  const relativeTime = getLocalizedRelativeReset(window, { locale });
@@ -341,7 +316,6 @@ function hasPositivePercentage(value) {
341
316
  const numeric = Number(value);
342
317
  return Number.isFinite(numeric) && numeric > 0;
343
318
  }
344
-
345
319
  function hasPositiveCreditBalance(credits) {
346
320
  if (!credits) return false;
347
321
  if (credits.unlimited) return true;
@@ -361,7 +335,6 @@ export async function getGitHubRateLimits(verbose = false) {
361
335
  // #1756: route through execGhWithRetry for transient 5xx; skip rate-limit retry budget (this is the endpoint we'd consult to know about rate limits).
362
336
  const { stdout } = await execGhWithRetry('gh api rate_limit 2>/dev/null', { label: 'gh api rate_limit', maxAttempts: 1 });
363
337
  const data = JSON.parse(stdout);
364
-
365
338
  if (verbose) {
366
339
  console.log('[VERBOSE] /limits GitHub rate limit response:', JSON.stringify(data, null, 2));
367
340
  }
@@ -374,7 +347,6 @@ export async function getGitHubRateLimits(verbose = false) {
374
347
  error: 'Could not parse GitHub rate limit response',
375
348
  };
376
349
  }
377
-
378
350
  // Calculate remaining percentage
379
351
  const usedPercentage = core.limit > 0 ? Math.round((core.used / core.limit) * 100) : 0;
380
352
  const remainingPercentage = 100 - usedPercentage;
@@ -382,7 +354,6 @@ export async function getGitHubRateLimits(verbose = false) {
382
354
  // Format reset time from Unix timestamp
383
355
  const resetDate = new Date(core.reset * 1000);
384
356
  const resetTimeFormatted = formatResetTime(resetDate.toISOString());
385
-
386
357
  // Calculate relative time until reset
387
358
  const now = new Date();
388
359
  const diffMs = resetDate - now;
@@ -397,7 +368,6 @@ export async function getGitHubRateLimits(verbose = false) {
397
368
  relativeReset = `${minutes}m`;
398
369
  }
399
370
  }
400
-
401
371
  if (verbose) {
402
372
  console.log(`[VERBOSE] /limits GitHub API: ${core.remaining}/${core.limit} remaining (${remainingPercentage}% available)`);
403
373
  }
@@ -426,7 +396,6 @@ export async function getGitHubRateLimits(verbose = false) {
426
396
  };
427
397
  }
428
398
  }
429
-
430
399
  /**
431
400
  * Get CPU load average information
432
401
  * Returns 1-minute, 5-minute, and 15-minute load averages
@@ -443,7 +412,6 @@ export async function getCpuLoadInfo(verbose = false) {
443
412
  const { stdout: cpuStdout } = await execAsync('wmic cpu get NumberOfCores /format:value 2>nul');
444
413
  const coresMatch = cpuStdout.match(/NumberOfCores=(\d+)/);
445
414
  cpuCount = coresMatch ? parseInt(coresMatch[1]) : 1;
446
-
447
415
  // Windows doesn't have load average, use current CPU usage as approximation
448
416
  const { stdout: loadStdout } = await execAsync('wmic cpu get LoadPercentage /format:value 2>nul');
449
417
  const loadMatch = loadStdout.match(/LoadPercentage=(\d+)/);
@@ -459,7 +427,6 @@ export async function getCpuLoadInfo(verbose = false) {
459
427
  loadAvg5 = parseFloat(numbers[1]);
460
428
  loadAvg15 = parseFloat(numbers[2]);
461
429
  }
462
-
463
430
  // Get CPU count
464
431
  if (process.platform === 'darwin') {
465
432
  const { stdout: cpuStdout } = await execAsync('sysctl -n hw.ncpu 2>/dev/null');
@@ -476,7 +443,6 @@ export async function getCpuLoadInfo(verbose = false) {
476
443
  error: 'Failed to parse CPU load information',
477
444
  };
478
445
  }
479
-
480
446
  // Calculate usage percentage based on 5-minute load average vs CPU count
481
447
  // Load average of 1.0 per CPU = 100% utilization
482
448
  // Using 5m average for consistency with solve queue (see issue #1137)
@@ -486,7 +452,6 @@ export async function getCpuLoadInfo(verbose = false) {
486
452
  if (verbose) {
487
453
  console.log(`[VERBOSE] /limits CPU load: ${loadAvg1.toFixed(2)} (1m), ${loadAvg5.toFixed(2)} (5m), ${loadAvg15.toFixed(2)} (15m), ${cpuCount} CPUs, ${usagePercentage}% used`);
488
454
  }
489
-
490
455
  return {
491
456
  success: true,
492
457
  cpuLoad: {
@@ -508,7 +473,6 @@ export async function getCpuLoadInfo(verbose = false) {
508
473
  };
509
474
  }
510
475
  }
511
-
512
476
  /**
513
477
  * Get RAM/memory usage information
514
478
  * Returns total, used, and available memory with usage percentage
@@ -525,7 +489,6 @@ export async function getMemoryInfo(verbose = false) {
525
489
  const { stdout: memTotal } = await execAsync('sysctl -n hw.memsize 2>/dev/null');
526
490
  const totalBytes = parseInt(memTotal.trim());
527
491
  totalMB = Math.round(totalBytes / (1024 * 1024));
528
-
529
492
  const { stdout: vmStat } = await execAsync('vm_stat 2>/dev/null');
530
493
  const pageSize = 4096; // Default page size on macOS
531
494
  const freeMatch = vmStat.match(/Pages free:\s+(\d+)/);
@@ -535,7 +498,6 @@ export async function getMemoryInfo(verbose = false) {
535
498
  const freePages = freeMatch ? parseInt(freeMatch[1]) : 0;
536
499
  const inactivePages = inactiveMatch ? parseInt(inactiveMatch[1]) : 0;
537
500
  const speculativePages = speculativeMatch ? parseInt(speculativeMatch[1]) : 0;
538
-
539
501
  // Available = free + inactive + speculative (approximately)
540
502
  availableMB = Math.round(((freePages + inactivePages + speculativePages) * pageSize) / (1024 * 1024));
541
503
  usedMB = totalMB - availableMB;
@@ -557,7 +519,6 @@ export async function getMemoryInfo(verbose = false) {
557
519
  const { stdout } = await execAsync("grep -E '^(MemTotal|MemAvailable):' /proc/meminfo 2>/dev/null");
558
520
  const totalMatch = stdout.match(/MemTotal:\s+(\d+)/);
559
521
  const availableMatch = stdout.match(/MemAvailable:\s+(\d+)/);
560
-
561
522
  if (totalMatch && availableMatch) {
562
523
  const totalKB = parseInt(totalMatch[1]);
563
524
  const availableKB = parseInt(availableMatch[1]);
@@ -573,14 +534,12 @@ export async function getMemoryInfo(verbose = false) {
573
534
  error: 'Failed to parse memory information',
574
535
  };
575
536
  }
576
-
577
537
  // Calculate used percentage
578
538
  const usedPercentage = Math.round((usedMB / totalMB) * 100);
579
539
 
580
540
  if (verbose) {
581
541
  console.log(`[VERBOSE] /limits memory: ${usedMB}MB used of ${totalMB}MB total (${usedPercentage}% used)`);
582
542
  }
583
-
584
543
  return {
585
544
  success: true,
586
545
  memory: {
@@ -607,7 +566,6 @@ export async function getMemoryInfo(verbose = false) {
607
566
  };
608
567
  }
609
568
  }
610
-
611
569
  /**
612
570
  * Get disk space information for the current filesystem
613
571
  * Returns total, used, available space and usage percentage
@@ -643,7 +601,6 @@ export async function getDiskSpaceInfo(verbose = false) {
643
601
  .map(s => parseInt(s.replace('M', '')));
644
602
  [totalMB, usedMB, availableMB] = parts;
645
603
  }
646
-
647
604
  if (isNaN(totalMB) || isNaN(usedMB) || isNaN(availableMB)) {
648
605
  return {
649
606
  success: false,
@@ -655,7 +612,6 @@ export async function getDiskSpaceInfo(verbose = false) {
655
612
  usedPercentage = Math.round((usedMB / totalMB) * 100);
656
613
  // Free percentage is the inverse
657
614
  const freePercentage = 100 - usedPercentage;
658
-
659
615
  if (verbose) {
660
616
  console.log(`[VERBOSE] /limits disk space: ${availableMB}MB free of ${totalMB}MB total (${freePercentage}% free)`);
661
617
  }
@@ -686,7 +642,6 @@ export async function getDiskSpaceInfo(verbose = false) {
686
642
  };
687
643
  }
688
644
  }
689
-
690
645
  /**
691
646
  * Get Claude usage limits by calling the Anthropic OAuth usage API
692
647
  * This approach is more reliable than trying to parse CLI output
@@ -712,7 +667,6 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
712
667
  error: 'Could not read Claude credentials. Make sure Claude is properly installed and authenticated.',
713
668
  };
714
669
  }
715
-
716
670
  const accessToken = credentials?.claudeAiOauth?.accessToken;
717
671
 
718
672
  if (!accessToken) {
@@ -721,7 +675,6 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
721
675
  error: 'No access token found in Claude credentials. Please use `/solve` or `/hive` commands to trigger re-authentication of Claude.',
722
676
  };
723
677
  }
724
-
725
678
  const requestHeaders = {
726
679
  Accept: 'application/json',
727
680
  'Content-Type': 'application/json',
@@ -729,7 +682,6 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
729
682
  Authorization: `Bearer ${accessToken}`,
730
683
  'anthropic-beta': 'oauth-2025-04-20',
731
684
  };
732
-
733
685
  if (verbose) {
734
686
  console.log('[VERBOSE] /limits fetching usage from API...');
735
687
  console.log(`[VERBOSE] /limits API request: GET ${USAGE_API_ENDPOINT}`);
@@ -747,7 +699,6 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
747
699
  method: 'GET',
748
700
  headers: requestHeaders,
749
701
  });
750
-
751
702
  // Log HTTP response status and headers for debugging (always in verbose mode, not just on error)
752
703
  if (verbose) {
753
704
  console.log(`[VERBOSE] /limits API HTTP status: ${response.status} ${response.statusText}`);
@@ -764,7 +715,6 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
764
715
  if (verbose) {
765
716
  console.error('[VERBOSE] /limits API error body:', errorText);
766
717
  }
767
-
768
718
  // Check for specific error conditions
769
719
  if (response.status === 401) {
770
720
  return {
@@ -781,7 +731,6 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
781
731
  error: `Claude Usage API access has reached rate limit.${formatRetryAfterMessage(retryAfter)}`,
782
732
  };
783
733
  }
784
-
785
734
  return {
786
735
  success: false,
787
736
  error: `Failed to fetch usage from API: ${response.status} ${response.statusText}`,
@@ -789,7 +738,6 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
789
738
  }
790
739
 
791
740
  const data = await response.json();
792
-
793
741
  if (verbose) {
794
742
  console.log('[VERBOSE] /limits API response body:', JSON.stringify(data, null, 2));
795
743
  }
@@ -799,7 +747,6 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
799
747
  // - five_hour: { utilization: number, resets_at: string }
800
748
  // - seven_day: { utilization: number, resets_at: string }
801
749
  // - seven_day_sonnet: { utilization: number, resets_at: string } (optional)
802
-
803
750
  const usage = {
804
751
  currentSession: {
805
752
  percentage: data.five_hour?.utilization ?? null,
@@ -817,7 +764,6 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
817
764
  resetsAt: data.seven_day_sonnet?.resets_at ?? null,
818
765
  },
819
766
  };
820
-
821
767
  return {
822
768
  success: true,
823
769
  usage,
@@ -850,7 +796,6 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
850
796
  export async function getCodexUsageLimits(verbose = false, authPath = DEFAULT_CODEX_AUTH_PATH, baseUrl = null) {
851
797
  try {
852
798
  const auth = await readCodexAuth(authPath, verbose);
853
-
854
799
  if (!auth) {
855
800
  return {
856
801
  success: false,
@@ -864,7 +809,6 @@ export async function getCodexUsageLimits(verbose = false, authPath = DEFAULT_CO
864
809
  error: 'Codex rate limits require ChatGPT authentication. API key auth does not expose account usage windows.',
865
810
  };
866
811
  }
867
-
868
812
  const accessToken = auth?.tokens?.access_token;
869
813
  if (!accessToken) {
870
814
  return {
@@ -881,7 +825,6 @@ export async function getCodexUsageLimits(verbose = false, authPath = DEFAULT_CO
881
825
  Authorization: `Bearer ${accessToken}`,
882
826
  'User-Agent': 'hive-mind-codex-limits/1.0',
883
827
  };
884
-
885
828
  if (verbose) {
886
829
  console.log('[VERBOSE] /limits fetching Codex usage from API...');
887
830
  console.log(`[VERBOSE] /limits Codex API request: GET ${usageEndpoint}`);
@@ -906,7 +849,6 @@ export async function getCodexUsageLimits(verbose = false, authPath = DEFAULT_CO
906
849
  method: 'GET',
907
850
  headers: requestHeaders,
908
851
  });
909
-
910
852
  if (verbose) {
911
853
  console.log(`[VERBOSE] /limits Codex API HTTP status: ${response.status} ${response.statusText}`);
912
854
  const responseHeaders = {};
@@ -921,14 +863,12 @@ export async function getCodexUsageLimits(verbose = false, authPath = DEFAULT_CO
921
863
  if (verbose) {
922
864
  console.error('[VERBOSE] /limits Codex API error body:', errorText);
923
865
  }
924
-
925
866
  if (response.status === 401) {
926
867
  return {
927
868
  success: false,
928
869
  error: 'Codex authentication expired. Please re-authenticate Codex with your ChatGPT account.',
929
870
  };
930
871
  }
931
-
932
872
  if (response.status === 429) {
933
873
  const retryAfter = response.headers.get('retry-after');
934
874
  return {
@@ -942,13 +882,11 @@ export async function getCodexUsageLimits(verbose = false, authPath = DEFAULT_CO
942
882
  error: `Failed to fetch Codex usage from API: ${response.status} ${response.statusText}`,
943
883
  };
944
884
  }
945
-
946
885
  const data = await response.json();
947
886
 
948
887
  if (verbose) {
949
888
  console.log('[VERBOSE] /limits Codex API response body:', JSON.stringify(data, null, 2));
950
889
  }
951
-
952
890
  const usage = {
953
891
  ...mapCodexRateLimitWindows(data?.rate_limit),
954
892
  sonnetOnly: {
@@ -967,7 +905,6 @@ export async function getCodexUsageLimits(verbose = false, authPath = DEFAULT_CO
967
905
  limitReached: limit?.rate_limit?.limit_reached ?? null,
968
906
  }))
969
907
  : [];
970
-
971
908
  return {
972
909
  success: true,
973
910
  usage,
@@ -995,7 +932,6 @@ export async function getCodexUsageLimits(verbose = false, authPath = DEFAULT_CO
995
932
  */
996
933
  export function calculateTimePassedPercentage(resetsAt, periodHours) {
997
934
  if (!resetsAt) return null;
998
-
999
935
  try {
1000
936
  const now = new Date();
1001
937
  const resetTime = new Date(resetsAt);
@@ -1003,11 +939,9 @@ export function calculateTimePassedPercentage(resetsAt, periodHours) {
1003
939
 
1004
940
  // Calculate when the period started
1005
941
  const startTime = new Date(resetTime.getTime() - periodMs);
1006
-
1007
942
  // Calculate time passed and total duration
1008
943
  const timePassed = now.getTime() - startTime.getTime();
1009
944
  const percentage = Math.max(0, Math.min(100, (timePassed / periodMs) * 100));
1010
-
1011
945
  return Math.round(percentage);
1012
946
  } catch {
1013
947
  return null;
@@ -1037,7 +971,6 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
1037
971
  const locale = resolveLimitLocale(options);
1038
972
  const subscription = options?.subscription || null;
1039
973
  const sections = [];
1040
-
1041
974
  sections.push(`${lt('current_time', {}, { locale })}: ${formatCurrentTime({ locale })}\n`);
1042
975
 
1043
976
  if (cpuLoad) {
@@ -1057,7 +990,6 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
1057
990
  section += `${cpuCoresLine}\n`;
1058
991
  sections.push(section);
1059
992
  }
1060
-
1061
993
  if (memory) {
1062
994
  let section = `${lt('ram', {}, { locale })}\n`;
1063
995
  const usedBar = getProgressBar(memory.usedPercentage, DISPLAY_THRESHOLDS.RAM);
@@ -1075,7 +1007,6 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
1075
1007
  section += `${formatBytesRange(diskSpace.usedBytes, diskSpace.totalBytes, { locale })}\n`;
1076
1008
  sections.push(section);
1077
1009
  }
1078
-
1079
1010
  // GitHub API rate limits section (if provided)
1080
1011
  // Threshold: Blocks parallel claude commands when >= 75%
1081
1012
  if (githubRateLimit) {
@@ -1096,7 +1027,6 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
1096
1027
 
1097
1028
  const telegramSection = formatTelegramLimitsSection(options?.telegramRateLimit, { locale });
1098
1029
  if (telegramSection) sections.push(telegramSection);
1099
-
1100
1030
  const claudeHeading = formatSubscriptionHeading('claude', subscription, { locale });
1101
1031
  const useShortClaudeLabels = Boolean(claudeHeading);
1102
1032
  const claudeSections = [];
@@ -1111,13 +1041,11 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
1111
1041
  if (hasSonnetOnly || !useShortClaudeLabels) {
1112
1042
  claudeSections.push(formatLimitWindowSection(lt('current_week_sonnet_only', {}, { locale }), usage?.sonnetOnly, 168, DISPLAY_THRESHOLDS.CLAUDE_WEEKLY, { locale }));
1113
1043
  }
1114
-
1115
1044
  if (!useShortClaudeLabels) {
1116
1045
  const subscriptionLines = formatSubscriptionLines(subscription, { locale });
1117
1046
  if (subscriptionLines) claudeSections.push(subscriptionLines);
1118
1047
  }
1119
1048
  }
1120
-
1121
1049
  const hasFencedExtraSection = extraSections.some(extra => String(extra ?? '').includes('```'));
1122
1050
  const useSplitLayout = Boolean(claudeHeading) || hasFencedExtraSection;
1123
1051
 
@@ -1128,7 +1056,6 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
1128
1056
  }
1129
1057
  return formatCodeBlock(sections.join('\n'));
1130
1058
  }
1131
-
1132
1059
  const markdownSections = [];
1133
1060
  markdownSections.push(formatCodeBlock(claudeHeading ? sections.join('\n') : [...sections, ...claudeSections].join('\n')));
1134
1061
  if (claudeHeading) {
@@ -1140,7 +1067,6 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
1140
1067
  const formatted = formatPlainTitledCodeSection(extra);
1141
1068
  if (formatted) markdownSections.push(formatted);
1142
1069
  }
1143
-
1144
1070
  return markdownSections.join('\n\n');
1145
1071
  }
1146
1072
 
@@ -1161,7 +1087,6 @@ export function formatCodexLimitsSection(codexLimits, codexError = null, options
1161
1087
  const planType = subscription?.planType || codexLimits?.planType || null;
1162
1088
  const heading = formatSubscriptionHeading('codex', subscription, { locale, planType });
1163
1089
  const useTitledLayout = Boolean(heading);
1164
-
1165
1090
  if (codexError) {
1166
1091
  const errorSection = useTitledLayout ? `${codexError}\n` : `${lt('codex_limits', {}, { locale })}\n${codexError}\n`;
1167
1092
  return useTitledLayout ? `${heading}\n\n${formatCodeBlock(errorSection)}` : errorSection;
@@ -1171,12 +1096,10 @@ export function formatCodexLimitsSection(codexLimits, codexError = null, options
1171
1096
  if (planType && !useTitledLayout) {
1172
1097
  section += `${lt('plan', {}, { locale })}: ${planType}\n`;
1173
1098
  }
1174
-
1175
1099
  const sessionSection = formatLimitWindowSection(useTitledLayout ? lt('five_hour_limit_session', {}, { locale }) : lt('codex_5_hour_session', {}, { locale }), usage?.currentSession, 5, DISPLAY_THRESHOLDS.CODEX_5_HOUR_SESSION, { locale });
1176
1100
  const weeklySection = formatLimitWindowSection(useTitledLayout ? lt('current_week', {}, { locale }) : lt('current_week_all_models', {}, { locale }), usage?.allModels, 168, DISPLAY_THRESHOLDS.CODEX_WEEKLY, { locale });
1177
1101
 
1178
1102
  section += [sessionSection, weeklySection].filter((_, index) => (index === 0 ? hasLimitPercentage(usage?.currentSession) : hasLimitPercentage(usage?.allModels))).join('\n');
1179
-
1180
1103
  const visibleAdditionalRateLimits = additionalRateLimits.filter(limit => hasPositivePercentage(limit.allModels?.percentage));
1181
1104
  if (visibleAdditionalRateLimits.length > 0) {
1182
1105
  section += `\n${lt('additional_codex_limits', {}, { locale })}\n`;
@@ -1189,7 +1112,6 @@ export function formatCodexLimitsSection(codexLimits, codexError = null, options
1189
1112
  section += `${limit.limitName}: ${windowTexts.join(', ')}\n`;
1190
1113
  }
1191
1114
  }
1192
-
1193
1115
  if (hasPositiveCreditBalance(credits)) {
1194
1116
  const creditSummary = credits.unlimited ? lt('unlimited', {}, { locale }) : `${credits.balance ?? '0'} ${lt('balance', {}, { locale })}`;
1195
1117
  section += `\n${lt('codex_credits', {}, { locale })}\n${creditSummary}\n`;
@@ -1199,14 +1121,12 @@ export function formatCodexLimitsSection(codexLimits, codexError = null, options
1199
1121
  const subscriptionLines = formatSubscriptionLines(subscription, { locale });
1200
1122
  if (subscriptionLines) section += subscriptionLines;
1201
1123
  }
1202
-
1203
1124
  return useTitledLayout ? `${heading}\n\n${formatCodeBlock(section)}` : section;
1204
1125
  }
1205
1126
 
1206
1127
  // ============================================================================
1207
1128
  // Caching Layer
1208
1129
  // ============================================================================
1209
-
1210
1130
  /**
1211
1131
  * Cache TTL constants (in milliseconds)
1212
1132
  * Values are loaded from config.lib.mjs which supports environment variable overrides.
@@ -1237,7 +1157,6 @@ class LimitCache {
1237
1157
  this.defaultTtlMs = defaultTtlMs;
1238
1158
  this.cache = new Map();
1239
1159
  }
1240
-
1241
1160
  get(key, ttlMs) {
1242
1161
  const entry = this.cache.get(key);
1243
1162
  if (!entry) return null;
@@ -1252,7 +1171,6 @@ class LimitCache {
1252
1171
  set(key, value, ttlMs) {
1253
1172
  this.cache.set(key, { value, timestamp: Date.now(), ttlMs: ttlMs ?? this.defaultTtlMs });
1254
1173
  }
1255
-
1256
1174
  clear() {
1257
1175
  this.cache.clear();
1258
1176
  }
@@ -1272,9 +1190,7 @@ class LimitCache {
1272
1190
  return { validEntries, expiredEntries, totalEntries: this.cache.size };
1273
1191
  }
1274
1192
  }
1275
-
1276
1193
  let globalCache = null;
1277
-
1278
1194
  export function getLimitCache() {
1279
1195
  if (!globalCache) globalCache = new LimitCache();
1280
1196
  return globalCache;
@@ -1286,7 +1202,6 @@ export function resetLimitCache() {
1286
1202
  globalCache = null;
1287
1203
  }
1288
1204
  }
1289
-
1290
1205
  export async function getCachedClaudeLimits(verbose = false) {
1291
1206
  const cache = getLimitCache();
1292
1207
  // Use USAGE_API TTL (13 min by default, see issue #1798) for Claude limits to avoid rate limiting.
@@ -1341,7 +1256,6 @@ export async function getCachedCodexLimits(verbose = false) {
1341
1256
  }
1342
1257
  return result;
1343
1258
  }
1344
-
1345
1259
  export async function getCachedGitHubLimits(verbose = false) {
1346
1260
  const cache = getLimitCache();
1347
1261
  const cached = cache.get('github', CACHE_TTL.API);
@@ -1365,7 +1279,6 @@ export async function getCachedMemoryInfo(verbose = false) {
1365
1279
  if (result.success) cache.set('memory', result, CACHE_TTL.SYSTEM);
1366
1280
  return result;
1367
1281
  }
1368
-
1369
1282
  export async function getCachedCpuInfo(verbose = false) {
1370
1283
  const cache = getLimitCache();
1371
1284
  const cached = cache.get('cpu', CACHE_TTL.SYSTEM);
@@ -1389,12 +1302,10 @@ export async function getCachedDiskInfo(verbose = false) {
1389
1302
  if (result.success) cache.set('disk', result, CACHE_TTL.SYSTEM);
1390
1303
  return result;
1391
1304
  }
1392
-
1393
1305
  export async function getAllCachedLimits(verbose = false) {
1394
1306
  const [claude, codex, github, memory, cpu, disk, claudeSubscription, codexSubscription, telegram] = await Promise.all([getCachedClaudeLimits(verbose), getCachedCodexLimits(verbose), getCachedGitHubLimits(verbose), getCachedMemoryInfo(verbose), getCachedCpuInfo(verbose), getCachedDiskInfo(verbose), getCachedClaudeSubscription(verbose), getCachedCodexSubscription(verbose), getTelegramRateLimits(verbose)]);
1395
1307
  return { claude, codex, github, memory, cpu, disk, claudeSubscription, codexSubscription, telegram };
1396
1308
  }
1397
-
1398
1309
  export default {
1399
1310
  // Raw functions (no caching)
1400
1311
  getClaudeUsageLimits,