@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.
- package/CHANGELOG.md +14 -0
- package/package.json +4 -1
- package/src/claude.connection.lib.mjs +209 -0
- package/src/claude.lib.mjs +6 -202
- package/src/codex.lib.mjs +0 -128
- package/src/github-rate-limit.lib.mjs +3 -0
- package/src/github-url-parser.lib.mjs +255 -0
- package/src/github.lib.mjs +22 -343
- package/src/hive.mjs +0 -152
- package/src/interactive-mode.lib.mjs +0 -43
- package/src/isolation-runner.lib.mjs +14 -174
- package/src/limits.lib.mjs +0 -89
- package/src/models/index.mjs +0 -15
- package/src/session-monitor.lib.mjs +14 -172
- package/src/solve.auto-merge.lib.mjs +70 -164
- package/src/solve.mjs +31 -193
- package/src/solve.repository.lib.mjs +0 -83
- package/src/solve.results.lib.mjs +0 -90
- package/src/solve.session.lib.mjs +52 -19
- package/src/solve.tool-uncommitted.lib.mjs +22 -0
- package/src/telegram-bot.mjs +0 -66
- package/src/telegram-merge-queue.lib.mjs +3 -155
- package/src/telegram-solve-queue.lib.mjs +9 -168
- package/src/use-m-bootstrap.lib.mjs +6 -5
- package/src/use-with-retry.lib.mjs +128 -2
package/src/limits.lib.mjs
CHANGED
|
@@ -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,
|