@link-assistant/hive-mind 1.50.8 → 1.50.10
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 +12 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/agent.prompts.lib.mjs +25 -37
- package/src/architecture-care.prompts.lib.mjs +11 -11
- package/src/claude.prompts.lib.mjs +31 -46
- package/src/codex.lib.mjs +481 -100
- package/src/codex.options.lib.mjs +52 -0
- package/src/codex.prompts.lib.mjs +84 -39
- package/src/experiments-examples.prompts.lib.mjs +7 -7
- package/src/hive.bootstrap.lib.mjs +32 -0
- package/src/hive.config.lib.mjs +3 -3
- package/src/hive.mjs +13 -20
- package/src/interactive-mode.lib.mjs +200 -265
- package/src/interactive-mode.shared.lib.mjs +133 -0
- package/src/lib.mjs +101 -4
- package/src/limits.lib.mjs +339 -2
- package/src/log-upload.lib.mjs +46 -3
- package/src/models/index.mjs +21 -12
- package/src/opencode.prompts.lib.mjs +26 -38
- package/src/queue-config.lib.mjs +6 -0
- package/src/solve.auto-continue.lib.mjs +1 -0
- package/src/solve.bootstrap.lib.mjs +39 -0
- package/src/solve.config.lib.mjs +11 -11
- package/src/solve.mjs +35 -40
- package/src/solve.progress-monitoring.lib.mjs +10 -2
- package/src/solve.restart-shared.lib.mjs +13 -1
- package/src/solve.results.lib.mjs +43 -5
- package/src/solve.validation.lib.mjs +1 -1
- package/src/telegram-bot.mjs +4 -2
- package/src/telegram-solve-queue.helpers.lib.mjs +151 -0
- package/src/telegram-solve-queue.lib.mjs +82 -181
- package/src/version-info.lib.mjs +8 -5
package/src/lib.mjs
CHANGED
|
@@ -188,16 +188,107 @@ export const setupVerboseLogInterceptor = () => {
|
|
|
188
188
|
*/
|
|
189
189
|
let stdioInterceptorInstalled = false;
|
|
190
190
|
let _writingFromLog = false; // Guard flag to prevent double-logging from log()
|
|
191
|
+
let stdoutBroken = false;
|
|
192
|
+
let stderrBroken = false;
|
|
193
|
+
let brokenPipeDiagnosticsWritten = false;
|
|
194
|
+
|
|
195
|
+
const isBrokenPipeError = error => {
|
|
196
|
+
return error?.code === 'EPIPE' || error?.code === 'ERR_STREAM_DESTROYED';
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const invokeWriteCallback = (callback, error = null) => {
|
|
200
|
+
if (typeof callback === 'function') {
|
|
201
|
+
callback(error);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const appendInternalDiagnostic = async message => {
|
|
206
|
+
if (!logFile) return;
|
|
207
|
+
const prefix = `[${new Date().toISOString()}] [INTERNAL]`;
|
|
208
|
+
await fs.appendFile(logFile, `${prefix} ${message}\n`).catch(() => {
|
|
209
|
+
// Silent fail to avoid recursive logging errors
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const formatStreamDiagnostic = stream => {
|
|
214
|
+
return JSON.stringify({
|
|
215
|
+
isTTY: Boolean(stream?.isTTY),
|
|
216
|
+
destroyed: Boolean(stream?.destroyed),
|
|
217
|
+
writable: stream?.writable,
|
|
218
|
+
writableEnded: Boolean(stream?.writableEnded),
|
|
219
|
+
writableFinished: Boolean(stream?.writableFinished),
|
|
220
|
+
errored: stream?.errored?.code || stream?.errored?.message || null,
|
|
221
|
+
fd: typeof stream?.fd === 'number' ? stream.fd : null,
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const normalizeWriteCallback = (encoding, callback) => {
|
|
226
|
+
return typeof encoding === 'function' ? encoding : callback;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const safeTerminalWrite = ({ originalWrite, chunk, encoding, callback, streamName }) => {
|
|
230
|
+
const isStdout = streamName === 'stdout';
|
|
231
|
+
const normalizedCallback = normalizeWriteCallback(encoding, callback);
|
|
232
|
+
if ((isStdout && stdoutBroken) || (!isStdout && stderrBroken)) {
|
|
233
|
+
invokeWriteCallback(normalizedCallback);
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
return originalWrite(chunk, encoding, callback);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (!isBrokenPipeError(error)) {
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (isStdout) {
|
|
245
|
+
stdoutBroken = true;
|
|
246
|
+
} else {
|
|
247
|
+
stderrBroken = true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
invokeWriteCallback(normalizedCallback, error);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const installBrokenPipeGuard = (stream, streamName) => {
|
|
256
|
+
stream.on('error', error => {
|
|
257
|
+
if (isBrokenPipeError(error)) {
|
|
258
|
+
if (streamName === 'stdout') {
|
|
259
|
+
stdoutBroken = true;
|
|
260
|
+
} else {
|
|
261
|
+
stderrBroken = true;
|
|
262
|
+
}
|
|
263
|
+
if (!brokenPipeDiagnosticsWritten) {
|
|
264
|
+
brokenPipeDiagnosticsWritten = true;
|
|
265
|
+
void appendInternalDiagnostic(`Detected broken ${streamName} stream (${error.code || 'unknown'}). Stream state=${formatStreamDiagnostic(stream)}. Further terminal writes will be skipped when possible.`);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
throw error;
|
|
271
|
+
});
|
|
272
|
+
};
|
|
273
|
+
|
|
191
274
|
export const setupStdioLogInterceptor = () => {
|
|
192
275
|
if (stdioInterceptorInstalled) return;
|
|
193
276
|
stdioInterceptorInstalled = true;
|
|
194
277
|
|
|
195
278
|
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
196
279
|
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
280
|
+
installBrokenPipeGuard(process.stdout, 'stdout');
|
|
281
|
+
installBrokenPipeGuard(process.stderr, 'stderr');
|
|
197
282
|
|
|
198
283
|
process.stdout.write = (chunk, encoding, callback) => {
|
|
199
|
-
// Always write to terminal first
|
|
200
|
-
const result =
|
|
284
|
+
// Always write to terminal first, unless the output pipe is already broken.
|
|
285
|
+
const result = safeTerminalWrite({
|
|
286
|
+
originalWrite: originalStdoutWrite,
|
|
287
|
+
chunk,
|
|
288
|
+
encoding,
|
|
289
|
+
callback,
|
|
290
|
+
streamName: 'stdout',
|
|
291
|
+
});
|
|
201
292
|
|
|
202
293
|
// Also append to log file if set, but skip if this write originated from log()
|
|
203
294
|
if (logFile && !_writingFromLog) {
|
|
@@ -214,8 +305,14 @@ export const setupStdioLogInterceptor = () => {
|
|
|
214
305
|
};
|
|
215
306
|
|
|
216
307
|
process.stderr.write = (chunk, encoding, callback) => {
|
|
217
|
-
// Always write to terminal first
|
|
218
|
-
const result =
|
|
308
|
+
// Always write to terminal first, unless the output pipe is already broken.
|
|
309
|
+
const result = safeTerminalWrite({
|
|
310
|
+
originalWrite: originalStderrWrite,
|
|
311
|
+
chunk,
|
|
312
|
+
encoding,
|
|
313
|
+
callback,
|
|
314
|
+
streamName: 'stderr',
|
|
315
|
+
});
|
|
219
316
|
|
|
220
317
|
// Also append to log file if set, but skip if this write originated from log()
|
|
221
318
|
if (logFile && !_writingFromLog) {
|
package/src/limits.lib.mjs
CHANGED
|
@@ -30,11 +30,87 @@ const execAsync = promisify(exec);
|
|
|
30
30
|
* Default path to Claude credentials file
|
|
31
31
|
*/
|
|
32
32
|
const DEFAULT_CREDENTIALS_PATH = join(homedir(), '.claude', '.credentials.json');
|
|
33
|
+
const DEFAULT_CODEX_AUTH_PATH = join(homedir(), '.codex', 'auth.json');
|
|
34
|
+
const DEFAULT_CODEX_CONFIG_PATH = join(homedir(), '.codex', 'config.toml');
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Anthropic OAuth usage API endpoint
|
|
36
38
|
*/
|
|
37
39
|
const USAGE_API_ENDPOINT = 'https://api.anthropic.com/api/oauth/usage';
|
|
40
|
+
const CODEX_USAGE_API_DEFAULT_BASE_URL = 'https://chatgpt.com/backend-api';
|
|
41
|
+
|
|
42
|
+
function decodeJwtPayload(token) {
|
|
43
|
+
if (!token || typeof token !== 'string') return null;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const payload = token.split('.')[1];
|
|
47
|
+
if (!payload) return null;
|
|
48
|
+
const normalized = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
49
|
+
const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
|
|
50
|
+
return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function unixSecondsToIsoDate(seconds) {
|
|
57
|
+
if (seconds === null || seconds === undefined) return null;
|
|
58
|
+
const numeric = Number(seconds);
|
|
59
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return null;
|
|
60
|
+
return new Date(numeric * 1000).toISOString();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function mapCodexWindow(window) {
|
|
64
|
+
const resetsAt = unixSecondsToIsoDate(window?.reset_at);
|
|
65
|
+
return {
|
|
66
|
+
percentage: window?.used_percent ?? null,
|
|
67
|
+
resetTime: formatResetTime(resetsAt),
|
|
68
|
+
resetsAt,
|
|
69
|
+
windowSeconds: window?.limit_window_seconds ?? null,
|
|
70
|
+
resetAfterSeconds: window?.reset_after_seconds ?? null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readCodexAuth(authPath = DEFAULT_CODEX_AUTH_PATH, verbose = false) {
|
|
75
|
+
try {
|
|
76
|
+
const content = await readFile(authPath, 'utf-8');
|
|
77
|
+
const auth = JSON.parse(content);
|
|
78
|
+
|
|
79
|
+
if (verbose) {
|
|
80
|
+
console.log('[VERBOSE] /limits Codex auth loaded from:', authPath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return auth;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (verbose) {
|
|
86
|
+
console.error('[VERBOSE] /limits failed to read Codex auth:', error.message);
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function getCodexUsageBaseUrl(configPath = DEFAULT_CODEX_CONFIG_PATH, verbose = false) {
|
|
93
|
+
try {
|
|
94
|
+
const content = await readFile(configPath, 'utf-8');
|
|
95
|
+
const match = content.match(/^\s*chatgpt_base_url\s*=\s*["']([^"']+)["']/m);
|
|
96
|
+
if (!match?.[1]) return CODEX_USAGE_API_DEFAULT_BASE_URL;
|
|
97
|
+
|
|
98
|
+
const baseUrl = match[1].trim().replace(/\/+$/, '');
|
|
99
|
+
const normalized = baseUrl.endsWith('/backend-api') ? baseUrl : `${baseUrl}/backend-api`;
|
|
100
|
+
|
|
101
|
+
if (verbose) {
|
|
102
|
+
console.log('[VERBOSE] /limits Codex base URL loaded from config:', normalized);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return normalized;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (verbose) {
|
|
108
|
+
console.log('[VERBOSE] /limits using default Codex base URL:', CODEX_USAGE_API_DEFAULT_BASE_URL);
|
|
109
|
+
console.log('[VERBOSE] /limits failed to read Codex config:', error.message);
|
|
110
|
+
}
|
|
111
|
+
return CODEX_USAGE_API_DEFAULT_BASE_URL;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
38
114
|
|
|
39
115
|
/**
|
|
40
116
|
* Read Claude credentials from the credentials file
|
|
@@ -702,6 +778,162 @@ export async function getClaudeUsageLimits(verbose = false, credentialsPath = DE
|
|
|
702
778
|
}
|
|
703
779
|
}
|
|
704
780
|
|
|
781
|
+
/**
|
|
782
|
+
* Get Codex usage limits through the ChatGPT-authenticated usage endpoint.
|
|
783
|
+
* Mirrors the supported upstream Codex account/rate-limits path.
|
|
784
|
+
*
|
|
785
|
+
* Returns usage data for:
|
|
786
|
+
* - Current session (5-hour) usage percentage and reset time
|
|
787
|
+
* - Current week usage percentage and reset date
|
|
788
|
+
* - Additional metered Codex limits when available
|
|
789
|
+
*
|
|
790
|
+
* @param {boolean} verbose - Whether to log verbose output
|
|
791
|
+
* @param {string} authPath - Optional path to Codex auth.json
|
|
792
|
+
* @param {string|null} baseUrl - Optional backend base URL override
|
|
793
|
+
* @returns {Object} Object with success boolean, and either usage data or error message
|
|
794
|
+
*/
|
|
795
|
+
export async function getCodexUsageLimits(verbose = false, authPath = DEFAULT_CODEX_AUTH_PATH, baseUrl = null) {
|
|
796
|
+
try {
|
|
797
|
+
const auth = await readCodexAuth(authPath, verbose);
|
|
798
|
+
|
|
799
|
+
if (!auth) {
|
|
800
|
+
return {
|
|
801
|
+
success: false,
|
|
802
|
+
error: 'Could not read Codex authentication. Make sure Codex is properly installed and authenticated.',
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (auth.auth_mode && auth.auth_mode !== 'chatgpt') {
|
|
807
|
+
return {
|
|
808
|
+
success: false,
|
|
809
|
+
error: 'Codex rate limits require ChatGPT authentication. API key auth does not expose account usage windows.',
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const accessToken = auth?.tokens?.access_token;
|
|
814
|
+
if (!accessToken) {
|
|
815
|
+
return {
|
|
816
|
+
success: false,
|
|
817
|
+
error: 'No Codex access token found. Please authenticate Codex with your ChatGPT account.',
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const resolvedBaseUrl = (baseUrl || (await getCodexUsageBaseUrl(undefined, verbose))).replace(/\/+$/, '');
|
|
822
|
+
const usageEndpoint = `${resolvedBaseUrl}/wham/usage`;
|
|
823
|
+
const tokenPayload = decodeJwtPayload(accessToken);
|
|
824
|
+
const requestHeaders = {
|
|
825
|
+
Accept: 'application/json',
|
|
826
|
+
Authorization: `Bearer ${accessToken}`,
|
|
827
|
+
'User-Agent': 'hive-mind-codex-limits/1.0',
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
if (verbose) {
|
|
831
|
+
console.log('[VERBOSE] /limits fetching Codex usage from API...');
|
|
832
|
+
console.log(`[VERBOSE] /limits Codex API request: GET ${usageEndpoint}`);
|
|
833
|
+
console.log('[VERBOSE] /limits Codex auth mode:', auth.auth_mode || 'unknown');
|
|
834
|
+
console.log('[VERBOSE] /limits Codex account id:', auth?.tokens?.account_id || tokenPayload?.['https://api.openai.com/auth']?.chatgpt_account_id || 'unknown');
|
|
835
|
+
console.log('[VERBOSE] /limits Codex plan type:', tokenPayload?.['https://api.openai.com/auth']?.chatgpt_plan_type || 'unknown');
|
|
836
|
+
console.log(
|
|
837
|
+
'[VERBOSE] /limits Codex API request headers:',
|
|
838
|
+
JSON.stringify(
|
|
839
|
+
{
|
|
840
|
+
Accept: requestHeaders.Accept,
|
|
841
|
+
Authorization: `Bearer ...${accessToken.slice(-8)}`,
|
|
842
|
+
'User-Agent': requestHeaders['User-Agent'],
|
|
843
|
+
},
|
|
844
|
+
null,
|
|
845
|
+
2
|
|
846
|
+
)
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const response = await fetch(usageEndpoint, {
|
|
851
|
+
method: 'GET',
|
|
852
|
+
headers: requestHeaders,
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
if (verbose) {
|
|
856
|
+
console.log(`[VERBOSE] /limits Codex API HTTP status: ${response.status} ${response.statusText}`);
|
|
857
|
+
const responseHeaders = {};
|
|
858
|
+
response.headers.forEach((value, key) => {
|
|
859
|
+
responseHeaders[key] = value;
|
|
860
|
+
});
|
|
861
|
+
console.log('[VERBOSE] /limits Codex API response headers:', JSON.stringify(responseHeaders, null, 2));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (!response.ok) {
|
|
865
|
+
const errorText = await response.text();
|
|
866
|
+
if (verbose) {
|
|
867
|
+
console.error('[VERBOSE] /limits Codex API error body:', errorText);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (response.status === 401) {
|
|
871
|
+
return {
|
|
872
|
+
success: false,
|
|
873
|
+
error: 'Codex authentication expired. Please re-authenticate Codex with your ChatGPT account.',
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (response.status === 429) {
|
|
878
|
+
const retryAfter = response.headers.get('retry-after');
|
|
879
|
+
return {
|
|
880
|
+
success: false,
|
|
881
|
+
error: `Codex usage API access has reached rate limit.${formatRetryAfterMessage(retryAfter)}`,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return {
|
|
886
|
+
success: false,
|
|
887
|
+
error: `Failed to fetch Codex usage from API: ${response.status} ${response.statusText}`,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const data = await response.json();
|
|
892
|
+
|
|
893
|
+
if (verbose) {
|
|
894
|
+
console.log('[VERBOSE] /limits Codex API response body:', JSON.stringify(data, null, 2));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const usage = {
|
|
898
|
+
currentSession: mapCodexWindow(data?.rate_limit?.primary_window),
|
|
899
|
+
allModels: mapCodexWindow(data?.rate_limit?.secondary_window),
|
|
900
|
+
sonnetOnly: {
|
|
901
|
+
percentage: null,
|
|
902
|
+
resetTime: null,
|
|
903
|
+
resetsAt: null,
|
|
904
|
+
},
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
const additionalRateLimits = Array.isArray(data?.additional_rate_limits)
|
|
908
|
+
? data.additional_rate_limits.map(limit => ({
|
|
909
|
+
limitId: limit?.metered_feature || null,
|
|
910
|
+
limitName: limit?.limit_name || limit?.metered_feature || 'additional',
|
|
911
|
+
currentSession: mapCodexWindow(limit?.rate_limit?.primary_window),
|
|
912
|
+
allModels: mapCodexWindow(limit?.rate_limit?.secondary_window),
|
|
913
|
+
allowed: limit?.rate_limit?.allowed ?? null,
|
|
914
|
+
limitReached: limit?.rate_limit?.limit_reached ?? null,
|
|
915
|
+
}))
|
|
916
|
+
: [];
|
|
917
|
+
|
|
918
|
+
return {
|
|
919
|
+
success: true,
|
|
920
|
+
usage,
|
|
921
|
+
planType: data?.plan_type || tokenPayload?.['https://api.openai.com/auth']?.chatgpt_plan_type || null,
|
|
922
|
+
credits: data?.credits || null,
|
|
923
|
+
additionalRateLimits,
|
|
924
|
+
raw: data,
|
|
925
|
+
};
|
|
926
|
+
} catch (error) {
|
|
927
|
+
if (verbose) {
|
|
928
|
+
console.error('[VERBOSE] /limits Codex error:', error);
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
success: false,
|
|
932
|
+
error: `Failed to get Codex usage limits: ${error.message}`,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
705
937
|
/**
|
|
706
938
|
* Generate a text-based progress bar for usage percentage
|
|
707
939
|
* @param {number} percentage - Usage percentage (0-100)
|
|
@@ -953,6 +1185,85 @@ export function formatUsageMessage(usage, diskSpace = null, githubRateLimit = nu
|
|
|
953
1185
|
return '```\n' + sections.join('\n') + '```';
|
|
954
1186
|
}
|
|
955
1187
|
|
|
1188
|
+
/**
|
|
1189
|
+
* Format Codex usage data into a section suitable for appending to /limits output.
|
|
1190
|
+
*
|
|
1191
|
+
* @param {Object|null} codexLimits - Result object from getCodexUsageLimits, or null
|
|
1192
|
+
* @param {string|null} codexError - Optional error message
|
|
1193
|
+
* @returns {string} Formatted section text
|
|
1194
|
+
*/
|
|
1195
|
+
export function formatCodexLimitsSection(codexLimits, codexError = null) {
|
|
1196
|
+
if (codexError) {
|
|
1197
|
+
return `Codex limits\n${codexError}\n`;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
const usage = codexLimits?.usage || null;
|
|
1201
|
+
const additionalRateLimits = codexLimits?.additionalRateLimits || [];
|
|
1202
|
+
const credits = codexLimits?.credits || null;
|
|
1203
|
+
const planType = codexLimits?.planType || null;
|
|
1204
|
+
|
|
1205
|
+
let section = 'Codex limits\n';
|
|
1206
|
+
if (planType) {
|
|
1207
|
+
section += `Plan: ${planType}\n`;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
let sessionSection = 'Codex 5 hour session\n';
|
|
1211
|
+
if (usage?.currentSession?.percentage !== null) {
|
|
1212
|
+
const timePassed = calculateTimePassedPercentage(usage.currentSession.resetsAt, 5);
|
|
1213
|
+
if (timePassed !== null) {
|
|
1214
|
+
sessionSection += `${getProgressBar(timePassed)} ${timePassed}% passed\n`;
|
|
1215
|
+
}
|
|
1216
|
+
const pct = Math.floor(usage.currentSession.percentage);
|
|
1217
|
+
const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CODEX_5_HOUR_SESSION);
|
|
1218
|
+
const suffix = pct >= DISPLAY_THRESHOLDS.CODEX_5_HOUR_SESSION ? ' ⚠️' : ' used';
|
|
1219
|
+
sessionSection += `${bar} ${pct}%${suffix}\n`;
|
|
1220
|
+
if (usage.currentSession.resetTime) {
|
|
1221
|
+
const relativeTime = formatRelativeTime(usage.currentSession.resetsAt);
|
|
1222
|
+
sessionSection += relativeTime ? `Resets in ${relativeTime} (${usage.currentSession.resetTime})\n` : `Resets ${usage.currentSession.resetTime}\n`;
|
|
1223
|
+
}
|
|
1224
|
+
} else {
|
|
1225
|
+
sessionSection += 'N/A\n';
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
let weeklySection = 'Current week (all models)\n';
|
|
1229
|
+
if (usage?.allModels?.percentage !== null) {
|
|
1230
|
+
const timePassed = calculateTimePassedPercentage(usage.allModels.resetsAt, 168);
|
|
1231
|
+
if (timePassed !== null) {
|
|
1232
|
+
weeklySection += `${getProgressBar(timePassed)} ${timePassed}% passed\n`;
|
|
1233
|
+
}
|
|
1234
|
+
const pct = Math.floor(usage.allModels.percentage);
|
|
1235
|
+
const bar = getProgressBar(pct, DISPLAY_THRESHOLDS.CODEX_WEEKLY);
|
|
1236
|
+
const suffix = pct >= DISPLAY_THRESHOLDS.CODEX_WEEKLY ? ' ⚠️' : ' used';
|
|
1237
|
+
weeklySection += `${bar} ${pct}%${suffix}\n`;
|
|
1238
|
+
if (usage.allModels.resetTime) {
|
|
1239
|
+
const relativeTime = formatRelativeTime(usage.allModels.resetsAt);
|
|
1240
|
+
weeklySection += relativeTime ? `Resets in ${relativeTime} (${usage.allModels.resetTime})\n` : `Resets ${usage.allModels.resetTime}\n`;
|
|
1241
|
+
}
|
|
1242
|
+
} else {
|
|
1243
|
+
weeklySection += 'N/A\n';
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
section += `${sessionSection}\n${weeklySection}`;
|
|
1247
|
+
|
|
1248
|
+
if (additionalRateLimits.length > 0) {
|
|
1249
|
+
section += '\nAdditional Codex limits\n';
|
|
1250
|
+
for (const limit of additionalRateLimits) {
|
|
1251
|
+
const sessionPct = limit.currentSession?.percentage;
|
|
1252
|
+
const weeklyPct = limit.allModels?.percentage;
|
|
1253
|
+
const sessionText = sessionPct === null ? 'session N/A' : `session ${Math.floor(sessionPct)}%`;
|
|
1254
|
+
const weeklyText = weeklyPct === null ? 'week N/A' : `week ${Math.floor(weeklyPct)}%`;
|
|
1255
|
+
section += `${limit.limitName}: ${sessionText}, ${weeklyText}\n`;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (credits) {
|
|
1260
|
+
const creditSummary = credits.unlimited ? 'unlimited' : `${credits.balance ?? '0'} balance`;
|
|
1261
|
+
section += `\nCodex credits\n${creditSummary}\n`;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
return section;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
956
1267
|
// ============================================================================
|
|
957
1268
|
// Caching Layer
|
|
958
1269
|
// ============================================================================
|
|
@@ -1064,6 +1375,29 @@ export async function getCachedClaudeLimits(verbose = false) {
|
|
|
1064
1375
|
return result;
|
|
1065
1376
|
}
|
|
1066
1377
|
|
|
1378
|
+
export async function getCachedCodexLimits(verbose = false) {
|
|
1379
|
+
const cache = getLimitCache();
|
|
1380
|
+
const cached = cache.get('codex', CACHE_TTL.USAGE_API);
|
|
1381
|
+
if (cached) {
|
|
1382
|
+
if (verbose) console.log('[VERBOSE] /limits-cache: Using cached Codex limits (TTL: ' + Math.round(CACHE_TTL.USAGE_API / 60000) + ' minutes)');
|
|
1383
|
+
return cached;
|
|
1384
|
+
}
|
|
1385
|
+
const cachedError = cache.get('codex-rate-limited', CACHE_TTL.USAGE_API);
|
|
1386
|
+
if (cachedError) {
|
|
1387
|
+
if (verbose) console.log('[VERBOSE] /limits-cache: Using cached Codex rate-limit error');
|
|
1388
|
+
return cachedError;
|
|
1389
|
+
}
|
|
1390
|
+
if (verbose) console.log('[VERBOSE] /limits-cache: Cache miss for Codex limits, fetching from API...');
|
|
1391
|
+
const result = await getCodexUsageLimits(verbose);
|
|
1392
|
+
if (result.success) {
|
|
1393
|
+
cache.set('codex', result, CACHE_TTL.USAGE_API);
|
|
1394
|
+
} else if (result.error && result.error.includes('rate limit')) {
|
|
1395
|
+
cache.set('codex-rate-limited', result, CACHE_TTL.USAGE_API);
|
|
1396
|
+
if (verbose) console.log('[VERBOSE] /limits-cache: Cached Codex rate-limit error for ' + Math.round(CACHE_TTL.USAGE_API / 60000) + ' minutes');
|
|
1397
|
+
}
|
|
1398
|
+
return result;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1067
1401
|
export async function getCachedGitHubLimits(verbose = false) {
|
|
1068
1402
|
const cache = getLimitCache();
|
|
1069
1403
|
const cached = cache.get('github', CACHE_TTL.API);
|
|
@@ -1113,13 +1447,14 @@ export async function getCachedDiskInfo(verbose = false) {
|
|
|
1113
1447
|
}
|
|
1114
1448
|
|
|
1115
1449
|
export async function getAllCachedLimits(verbose = false) {
|
|
1116
|
-
const [claude, github, memory, cpu, disk] = await Promise.all([getCachedClaudeLimits(verbose), getCachedGitHubLimits(verbose), getCachedMemoryInfo(verbose), getCachedCpuInfo(verbose), getCachedDiskInfo(verbose)]);
|
|
1117
|
-
return { claude, github, memory, cpu, disk };
|
|
1450
|
+
const [claude, codex, github, memory, cpu, disk] = await Promise.all([getCachedClaudeLimits(verbose), getCachedCodexLimits(verbose), getCachedGitHubLimits(verbose), getCachedMemoryInfo(verbose), getCachedCpuInfo(verbose), getCachedDiskInfo(verbose)]);
|
|
1451
|
+
return { claude, codex, github, memory, cpu, disk };
|
|
1118
1452
|
}
|
|
1119
1453
|
|
|
1120
1454
|
export default {
|
|
1121
1455
|
// Raw functions (no caching)
|
|
1122
1456
|
getClaudeUsageLimits,
|
|
1457
|
+
getCodexUsageLimits,
|
|
1123
1458
|
getCpuLoadInfo,
|
|
1124
1459
|
getMemoryInfo,
|
|
1125
1460
|
getDiskSpaceInfo,
|
|
@@ -1127,6 +1462,7 @@ export default {
|
|
|
1127
1462
|
getProgressBar,
|
|
1128
1463
|
calculateTimePassedPercentage,
|
|
1129
1464
|
formatUsageMessage,
|
|
1465
|
+
formatCodexLimitsSection,
|
|
1130
1466
|
formatRetryAfterMessage,
|
|
1131
1467
|
// Threshold constants for progress bar visualization
|
|
1132
1468
|
DISPLAY_THRESHOLDS,
|
|
@@ -1136,6 +1472,7 @@ export default {
|
|
|
1136
1472
|
resetLimitCache,
|
|
1137
1473
|
// Cached functions
|
|
1138
1474
|
getCachedClaudeLimits,
|
|
1475
|
+
getCachedCodexLimits,
|
|
1139
1476
|
getCachedGitHubLimits,
|
|
1140
1477
|
getCachedMemoryInfo,
|
|
1141
1478
|
getCachedCpuInfo,
|
package/src/log-upload.lib.mjs
CHANGED
|
@@ -11,6 +11,7 @@ const use = globalThis.use;
|
|
|
11
11
|
|
|
12
12
|
// Use command-stream for consistent $ behavior across runtimes
|
|
13
13
|
const { $ } = await use('command-stream');
|
|
14
|
+
const $silent = $({ mirror: false, capture: true });
|
|
14
15
|
|
|
15
16
|
// Import shared library functions
|
|
16
17
|
const lib = await import('./lib.mjs');
|
|
@@ -20,6 +21,12 @@ const { log } = lib;
|
|
|
20
21
|
const sentryLib = await import('./sentry.lib.mjs');
|
|
21
22
|
const { reportError } = sentryLib;
|
|
22
23
|
|
|
24
|
+
const summarizeCommandOutput = value => {
|
|
25
|
+
const text = value?.toString()?.trim() || '';
|
|
26
|
+
if (!text) return '';
|
|
27
|
+
return text.length > 500 ? `${text.slice(0, 500)}... [truncated ${text.length - 500} chars]` : text;
|
|
28
|
+
};
|
|
29
|
+
|
|
23
30
|
/**
|
|
24
31
|
* Upload a log file using gh-upload-log command
|
|
25
32
|
* @param {Object} options - Upload options
|
|
@@ -92,12 +99,18 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
|
|
|
92
99
|
// For gist: get raw URL from gist API
|
|
93
100
|
const gistId = result.url.split('/').pop();
|
|
94
101
|
try {
|
|
95
|
-
|
|
102
|
+
if (verbose) {
|
|
103
|
+
await log(` 🔍 Fetching gist metadata for raw URL resolution (gistId=${gistId})`, { verbose: true });
|
|
104
|
+
}
|
|
105
|
+
const gistDetailsResult = await $silent`gh api gists/${gistId} --jq '{owner: .owner.login, history: .history, fileNames: (.files | keys)}'`;
|
|
106
|
+
if (verbose) {
|
|
107
|
+
await log(` 📥 Gist metadata fetch completed (code=${gistDetailsResult.code ?? 'unknown'})`, { verbose: true });
|
|
108
|
+
}
|
|
96
109
|
if (gistDetailsResult.code === 0) {
|
|
97
110
|
const gistDetails = JSON.parse(gistDetailsResult.stdout.toString());
|
|
98
111
|
const gistOwner = gistDetails.owner;
|
|
99
112
|
const commitSha = gistDetails.history?.[0]?.version;
|
|
100
|
-
const fileNames = gistDetails.
|
|
113
|
+
const fileNames = Array.isArray(gistDetails.fileNames) ? gistDetails.fileNames : [];
|
|
101
114
|
const fileName = fileNames.length > 0 ? fileNames[0] : 'log.txt';
|
|
102
115
|
|
|
103
116
|
if (commitSha) {
|
|
@@ -105,6 +118,18 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
|
|
|
105
118
|
} else {
|
|
106
119
|
result.rawUrl = `https://gist.githubusercontent.com/${gistOwner}/${gistId}/raw/${fileName}`;
|
|
107
120
|
}
|
|
121
|
+
if (verbose) {
|
|
122
|
+
await log(` 🧩 Gist metadata resolved owner=${gistOwner}, commitSha=${commitSha || 'latest'}, fileName=${fileName}`, { verbose: true });
|
|
123
|
+
}
|
|
124
|
+
} else if (verbose) {
|
|
125
|
+
const stderrSummary = summarizeCommandOutput(gistDetailsResult.stderr);
|
|
126
|
+
const stdoutSummary = summarizeCommandOutput(gistDetailsResult.stdout);
|
|
127
|
+
if (stderrSummary) {
|
|
128
|
+
await log(` ⚠️ Gist metadata stderr: ${stderrSummary}`, { verbose: true });
|
|
129
|
+
}
|
|
130
|
+
if (stdoutSummary) {
|
|
131
|
+
await log(` ⚠️ Gist metadata stdout: ${stdoutSummary}`, { verbose: true });
|
|
132
|
+
}
|
|
108
133
|
}
|
|
109
134
|
} catch (apiError) {
|
|
110
135
|
if (verbose) {
|
|
@@ -121,7 +146,13 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
|
|
|
121
146
|
try {
|
|
122
147
|
const repoUrl = result.url;
|
|
123
148
|
const repoPath = repoUrl.replace('https://github.com/', '');
|
|
124
|
-
|
|
149
|
+
if (verbose) {
|
|
150
|
+
await log(` 🔍 Fetching repository contents for raw URL resolution (repoPath=${repoPath})`, { verbose: true });
|
|
151
|
+
}
|
|
152
|
+
const contentsResult = await $silent`gh api repos/${repoPath}/contents --jq '.[].name'`;
|
|
153
|
+
if (verbose) {
|
|
154
|
+
await log(` 📥 Repository contents fetch completed (code=${contentsResult.code ?? 'unknown'})`, { verbose: true });
|
|
155
|
+
}
|
|
125
156
|
if (contentsResult.code === 0) {
|
|
126
157
|
const files = contentsResult.stdout
|
|
127
158
|
.toString()
|
|
@@ -131,6 +162,18 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
|
|
|
131
162
|
if (files.length > 0) {
|
|
132
163
|
const fileName = files[0];
|
|
133
164
|
result.rawUrl = `${repoUrl}/raw/main/${fileName}`;
|
|
165
|
+
if (verbose) {
|
|
166
|
+
await log(` 🧩 Repository contents resolved fileName=${fileName}`, { verbose: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else if (verbose) {
|
|
170
|
+
const stderrSummary = summarizeCommandOutput(contentsResult.stderr);
|
|
171
|
+
const stdoutSummary = summarizeCommandOutput(contentsResult.stdout);
|
|
172
|
+
if (stderrSummary) {
|
|
173
|
+
await log(` ⚠️ Repository contents stderr: ${stderrSummary}`, { verbose: true });
|
|
174
|
+
}
|
|
175
|
+
if (stdoutSummary) {
|
|
176
|
+
await log(` ⚠️ Repository contents stdout: ${stdoutSummary}`, { verbose: true });
|
|
134
177
|
}
|
|
135
178
|
}
|
|
136
179
|
} catch (apiError) {
|