omniagent 0.1.9 → 0.1.11
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/dist/claude-C0SMAkM3.js +388 -0
- package/dist/cli.js +3 -3
- package/dist/{codex-D1RuzsY6.js → codex-0b2YLh_8.js} +256 -0
- package/dist/gemini-BVRg6OMO.js +437 -0
- package/package.json +1 -1
- package/dist/claude-Dmv_YFKX.js +0 -146
- package/dist/gemini-CskI3Qjp.js +0 -168
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { c as cleanControlOutput, a as compactLines, p as parsePercentUsed, m as makeUsageLimit } from "./cli.js";
|
|
7
|
+
import { r as runPtyScenario, e as enterKey, a as escapeKey } from "./pty-CZBSAJzE.js";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const CLAUDE_CODE_KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
10
|
+
const CLAUDE_CODE_CREDENTIALS_PATH = [".claude", ".credentials.json"];
|
|
11
|
+
const CLAUDE_USAGE_API_URL = "https://api.anthropic.com/v1/messages";
|
|
12
|
+
const CLAUDE_USAGE_API_TIMEOUT_MS = 1e4;
|
|
13
|
+
const CLAUDE_USAGE_API_HEADERS = {
|
|
14
|
+
"anthropic-version": "2023-06-01",
|
|
15
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
16
|
+
"content-type": "application/json",
|
|
17
|
+
"user-agent": "claude-code/2.1.5"
|
|
18
|
+
};
|
|
19
|
+
const CLAUDE_USAGE_API_BODY = {
|
|
20
|
+
model: "claude-haiku-4-5-20251001",
|
|
21
|
+
max_tokens: 1,
|
|
22
|
+
messages: [{ role: "user", content: "hi" }]
|
|
23
|
+
};
|
|
24
|
+
async function extractClaudeUsage(context) {
|
|
25
|
+
try {
|
|
26
|
+
return await extractClaudeUsageFromApi(context);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (context.signal.aborted) {
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
return extractClaudeUsageFromTui(context);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function extractClaudeUsageFromTui(context) {
|
|
35
|
+
const command = context.command ?? context.launch?.command ?? "claude";
|
|
36
|
+
const model = context.launch?.cheapModel ?? "haiku";
|
|
37
|
+
validateClaudeModel(model);
|
|
38
|
+
const ptyResult = await runPtyScenario({
|
|
39
|
+
command,
|
|
40
|
+
args: context.launch?.args ?? ["--model", model],
|
|
41
|
+
cwd: context.repoRoot,
|
|
42
|
+
cols: 100,
|
|
43
|
+
rows: 40,
|
|
44
|
+
timeoutMs: context.launch?.timeoutMs ?? 6e4,
|
|
45
|
+
signal: context.signal,
|
|
46
|
+
debug: context.debug,
|
|
47
|
+
steps: [
|
|
48
|
+
{ waitFor: /Claude|>|❯/u, waitForSource: "screen", waitForTimeoutMs: 4e3 },
|
|
49
|
+
{ write: enterKey() },
|
|
50
|
+
{ waitFor: /Claude|>|❯/u, waitForSource: "screen", waitForTimeoutMs: 8e3 },
|
|
51
|
+
{ write: `/usage${enterKey()}` },
|
|
52
|
+
{
|
|
53
|
+
waitFor: hasClaudeUsageRows,
|
|
54
|
+
waitForTimeoutMs: 15e3,
|
|
55
|
+
capture: "usage",
|
|
56
|
+
captureWaitMs: 500
|
|
57
|
+
},
|
|
58
|
+
{ write: escapeKey() },
|
|
59
|
+
{ waitMs: 500 },
|
|
60
|
+
{ write: `/exit${enterKey()}` }
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
const usageSnapshot = ptyResult.snapshots.usage ?? ptyResult;
|
|
64
|
+
const cleanedOutput = cleanControlOutput(usageSnapshot.raw);
|
|
65
|
+
const parsed = parseClaudeUsage(usageSnapshot.screen, cleanedOutput);
|
|
66
|
+
const limits = buildClaudeUsageLimits(parsed, context);
|
|
67
|
+
if (limits.length === 0) {
|
|
68
|
+
throw new Error("Claude usage output did not include session or weekly usage rows.");
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
targetId: context.targetId,
|
|
72
|
+
displayName: context.displayName,
|
|
73
|
+
command,
|
|
74
|
+
limits,
|
|
75
|
+
debug: ptyResult.debug.length > 0 ? ptyResult.debug : void 0
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function extractClaudeUsageFromApi(context) {
|
|
79
|
+
const command = context.command ?? context.launch?.command ?? "claude";
|
|
80
|
+
const token = await readClaudeAccessToken(context);
|
|
81
|
+
if (token == null) {
|
|
82
|
+
throw new Error("Claude Code OAuth token was not available.");
|
|
83
|
+
}
|
|
84
|
+
const response = await fetchClaudeUsageHeaders(token, context.signal);
|
|
85
|
+
if (response.status >= 400) {
|
|
86
|
+
throw new Error(`Claude usage API returned HTTP ${response.status}.`);
|
|
87
|
+
}
|
|
88
|
+
const result = buildClaudeApiUsageResult(response.headers, {
|
|
89
|
+
targetId: context.targetId,
|
|
90
|
+
displayName: context.displayName,
|
|
91
|
+
now: context.now,
|
|
92
|
+
command
|
|
93
|
+
});
|
|
94
|
+
if (result.limits.length === 0) {
|
|
95
|
+
throw new Error("Claude usage API response did not include usage headers.");
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
function buildClaudeApiUsageResult(headers, context) {
|
|
100
|
+
const sessionUsed = parseUsageHeaderFraction(
|
|
101
|
+
headers.get("anthropic-ratelimit-unified-5h-utilization")
|
|
102
|
+
);
|
|
103
|
+
const weekUsed = parseUsageHeaderFraction(
|
|
104
|
+
headers.get("anthropic-ratelimit-unified-7d-utilization")
|
|
105
|
+
);
|
|
106
|
+
if (sessionUsed == null || weekUsed == null) {
|
|
107
|
+
throw new Error("Claude usage API response did not include complete usage headers.");
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
targetId: context.targetId,
|
|
111
|
+
displayName: context.displayName,
|
|
112
|
+
command: context.command,
|
|
113
|
+
limits: [
|
|
114
|
+
makeClaudeApiUsageLimit({
|
|
115
|
+
targetId: context.targetId,
|
|
116
|
+
scope: "current_session",
|
|
117
|
+
window: "session",
|
|
118
|
+
percentUsed: sessionUsed,
|
|
119
|
+
resetAt: parseEpochSecondsHeader(headers.get("anthropic-ratelimit-unified-5h-reset")),
|
|
120
|
+
now: context.now
|
|
121
|
+
}),
|
|
122
|
+
makeClaudeApiUsageLimit({
|
|
123
|
+
targetId: context.targetId,
|
|
124
|
+
scope: "current_week",
|
|
125
|
+
window: "weekly",
|
|
126
|
+
percentUsed: weekUsed,
|
|
127
|
+
resetAt: parseEpochSecondsHeader(headers.get("anthropic-ratelimit-unified-7d-reset")),
|
|
128
|
+
now: context.now
|
|
129
|
+
})
|
|
130
|
+
]
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function extractClaudeAccessToken(blob) {
|
|
134
|
+
const trimmed = blob.trim();
|
|
135
|
+
if (!trimmed) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(trimmed);
|
|
140
|
+
const token = findAccessToken(parsed);
|
|
141
|
+
if (token != null) {
|
|
142
|
+
return token;
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
const match = /"accessToken"\s*:\s*"([^"]+)"/.exec(trimmed);
|
|
147
|
+
if (match?.[1]) {
|
|
148
|
+
return match[1];
|
|
149
|
+
}
|
|
150
|
+
if (/^[A-Za-z0-9_\-.~+/=]{20,}$/.test(trimmed)) {
|
|
151
|
+
return trimmed;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
async function readClaudeAccessToken(context) {
|
|
156
|
+
const tokenFromFile = await readClaudeAccessTokenFromFile(context.homeDir);
|
|
157
|
+
if (tokenFromFile != null) {
|
|
158
|
+
return tokenFromFile;
|
|
159
|
+
}
|
|
160
|
+
if (process.platform !== "darwin") {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return readClaudeAccessTokenFromKeychain(context.signal);
|
|
164
|
+
}
|
|
165
|
+
async function readClaudeAccessTokenFromFile(homeDir) {
|
|
166
|
+
try {
|
|
167
|
+
const raw = await readFile(path.join(homeDir, ...CLAUDE_CODE_CREDENTIALS_PATH), "utf8");
|
|
168
|
+
return extractClaudeAccessToken(raw);
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function readClaudeAccessTokenFromKeychain(signal) {
|
|
174
|
+
const username = os.userInfo().username;
|
|
175
|
+
const keychainArgs = [
|
|
176
|
+
["find-generic-password", "-s", CLAUDE_CODE_KEYCHAIN_SERVICE, "-a", username, "-w"],
|
|
177
|
+
["find-generic-password", "-s", CLAUDE_CODE_KEYCHAIN_SERVICE, "-w"]
|
|
178
|
+
];
|
|
179
|
+
for (const args of keychainArgs) {
|
|
180
|
+
try {
|
|
181
|
+
const { stdout } = await execFileAsync("security", args, {
|
|
182
|
+
timeout: 5e3,
|
|
183
|
+
signal,
|
|
184
|
+
maxBuffer: 1024 * 1024
|
|
185
|
+
});
|
|
186
|
+
const token = extractClaudeAccessToken(stdout);
|
|
187
|
+
if (token != null) {
|
|
188
|
+
return token;
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
if (signal.aborted) {
|
|
192
|
+
throw signal.reason;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
async function fetchClaudeUsageHeaders(token, parentSignal) {
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const timeout = setTimeout(() => {
|
|
201
|
+
controller.abort(new Error("Claude usage API request timed out."));
|
|
202
|
+
}, CLAUDE_USAGE_API_TIMEOUT_MS);
|
|
203
|
+
const abortFromParent = () => {
|
|
204
|
+
controller.abort(parentSignal.reason);
|
|
205
|
+
};
|
|
206
|
+
parentSignal.addEventListener("abort", abortFromParent, { once: true });
|
|
207
|
+
try {
|
|
208
|
+
const response = await fetch(CLAUDE_USAGE_API_URL, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: {
|
|
211
|
+
...CLAUDE_USAGE_API_HEADERS,
|
|
212
|
+
authorization: `Bearer ${token}`
|
|
213
|
+
},
|
|
214
|
+
body: JSON.stringify(CLAUDE_USAGE_API_BODY),
|
|
215
|
+
signal: controller.signal
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
status: response.status,
|
|
219
|
+
headers: response.headers
|
|
220
|
+
};
|
|
221
|
+
} finally {
|
|
222
|
+
clearTimeout(timeout);
|
|
223
|
+
parentSignal.removeEventListener("abort", abortFromParent);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function findAccessToken(value) {
|
|
227
|
+
if (value == null || typeof value !== "object") {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const record = value;
|
|
231
|
+
if (typeof record.accessToken === "string") {
|
|
232
|
+
return record.accessToken;
|
|
233
|
+
}
|
|
234
|
+
for (const child of Object.values(record)) {
|
|
235
|
+
const token = findAccessToken(child);
|
|
236
|
+
if (token != null) {
|
|
237
|
+
return token;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
function makeClaudeApiUsageLimit(options) {
|
|
243
|
+
const percentUsed = clampPercent(options.percentUsed);
|
|
244
|
+
const resetText = options.resetAt == null ? null : `resets ${options.resetAt}`;
|
|
245
|
+
const raw = `${formatPercent(percentUsed)} used${resetText == null ? "" : ` (${resetText})`}`;
|
|
246
|
+
const limit = makeUsageLimit({
|
|
247
|
+
targetId: options.targetId,
|
|
248
|
+
scope: options.scope,
|
|
249
|
+
window: options.window,
|
|
250
|
+
percentUsed,
|
|
251
|
+
percentRemaining: 100 - percentUsed,
|
|
252
|
+
resetText,
|
|
253
|
+
raw,
|
|
254
|
+
now: options.now
|
|
255
|
+
});
|
|
256
|
+
return {
|
|
257
|
+
...limit,
|
|
258
|
+
resetAt: options.resetAt
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function parseUsageHeaderFraction(value) {
|
|
262
|
+
if (value == null || !value.trim()) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const fraction = Number(value);
|
|
266
|
+
if (!Number.isFinite(fraction)) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
return fraction * 100;
|
|
270
|
+
}
|
|
271
|
+
function parseEpochSecondsHeader(value) {
|
|
272
|
+
if (value == null || !value.trim()) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const seconds = Number(value);
|
|
276
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
return new Date(seconds * 1e3).toISOString();
|
|
280
|
+
}
|
|
281
|
+
function clampPercent(value) {
|
|
282
|
+
return Math.max(0, Math.min(100, value));
|
|
283
|
+
}
|
|
284
|
+
function formatPercent(value) {
|
|
285
|
+
return Number.isInteger(value) ? `${value}%` : `${Number(value.toFixed(2))}%`;
|
|
286
|
+
}
|
|
287
|
+
function hasClaudeUsageRows(snapshot) {
|
|
288
|
+
const parsed = parseClaudeUsage(snapshot.screen, cleanControlOutput(snapshot.raw));
|
|
289
|
+
return Boolean(parsed.currentSessionUsed || parsed.currentWeekUsed);
|
|
290
|
+
}
|
|
291
|
+
function buildClaudeUsageLimits(parsed, context) {
|
|
292
|
+
const sessionUsed = parsePercentUsed(parsed.currentSessionUsed);
|
|
293
|
+
const weekUsed = parsePercentUsed(parsed.currentWeekUsed);
|
|
294
|
+
const limits = [];
|
|
295
|
+
if (parsed.currentSessionUsed.trim()) {
|
|
296
|
+
limits.push(
|
|
297
|
+
makeUsageLimit({
|
|
298
|
+
targetId: context.targetId,
|
|
299
|
+
scope: "current_session",
|
|
300
|
+
window: "session",
|
|
301
|
+
percentUsed: sessionUsed,
|
|
302
|
+
percentRemaining: sessionUsed == null ? null : 100 - sessionUsed,
|
|
303
|
+
resetText: parsed.currentSessionResets,
|
|
304
|
+
raw: formatRaw(parsed.currentSessionUsed, parsed.currentSessionResets),
|
|
305
|
+
now: context.now
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (parsed.currentWeekUsed.trim()) {
|
|
310
|
+
limits.push(
|
|
311
|
+
makeUsageLimit({
|
|
312
|
+
targetId: context.targetId,
|
|
313
|
+
scope: "current_week",
|
|
314
|
+
window: "weekly",
|
|
315
|
+
percentUsed: weekUsed,
|
|
316
|
+
percentRemaining: weekUsed == null ? null : 100 - weekUsed,
|
|
317
|
+
resetText: parsed.currentWeekResets,
|
|
318
|
+
raw: formatRaw(parsed.currentWeekUsed, parsed.currentWeekResets),
|
|
319
|
+
now: context.now
|
|
320
|
+
})
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return limits;
|
|
324
|
+
}
|
|
325
|
+
function parseClaudeUsage(screen, cleanedOutput = "") {
|
|
326
|
+
const fromScreen = parseClaudeLines(compactLines(screen));
|
|
327
|
+
if (fromScreen.currentSessionUsed || fromScreen.currentWeekUsed) {
|
|
328
|
+
return fromScreen;
|
|
329
|
+
}
|
|
330
|
+
return parseClaudeLines(compactLines(cleanedOutput));
|
|
331
|
+
}
|
|
332
|
+
function parseClaudeLines(lines) {
|
|
333
|
+
const values = {
|
|
334
|
+
currentSessionUsed: "",
|
|
335
|
+
currentSessionResets: "",
|
|
336
|
+
currentWeekUsed: "",
|
|
337
|
+
currentWeekResets: ""
|
|
338
|
+
};
|
|
339
|
+
let section = "";
|
|
340
|
+
for (const line of lines) {
|
|
341
|
+
if (line === "Current session") {
|
|
342
|
+
section = "currentSession";
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (line.startsWith("Current week")) {
|
|
346
|
+
section = "currentWeek";
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (!section) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const usedMatch = /(\d+(?:\.\d+)?% used)/i.exec(line);
|
|
353
|
+
if (usedMatch != null) {
|
|
354
|
+
if (section === "currentSession") {
|
|
355
|
+
values.currentSessionUsed = usedMatch[1];
|
|
356
|
+
} else {
|
|
357
|
+
values.currentWeekUsed = usedMatch[1];
|
|
358
|
+
}
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (line.startsWith("Resets ")) {
|
|
362
|
+
if (section === "currentSession") {
|
|
363
|
+
values.currentSessionResets = line.slice("Resets ".length).trim();
|
|
364
|
+
} else {
|
|
365
|
+
values.currentWeekResets = line.slice("Resets ".length).trim();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return values;
|
|
370
|
+
}
|
|
371
|
+
function formatRaw(used, resets) {
|
|
372
|
+
if (!used) {
|
|
373
|
+
return "";
|
|
374
|
+
}
|
|
375
|
+
return resets ? `${used} (resets ${resets})` : used;
|
|
376
|
+
}
|
|
377
|
+
function validateClaudeModel(model) {
|
|
378
|
+
if (!/^[A-Za-z0-9._:-]+$/.test(model)) {
|
|
379
|
+
throw new Error(`Unsupported Claude usage model value: ${model}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
export {
|
|
383
|
+
buildClaudeApiUsageResult,
|
|
384
|
+
buildClaudeUsageLimits,
|
|
385
|
+
extractClaudeAccessToken,
|
|
386
|
+
extractClaudeUsage,
|
|
387
|
+
parseClaudeUsage
|
|
388
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1322,7 +1322,7 @@ const claudeTarget = {
|
|
|
1322
1322
|
timeoutMs: 6e4
|
|
1323
1323
|
},
|
|
1324
1324
|
extract: async (context) => {
|
|
1325
|
-
const { extractClaudeUsage } = await import("./claude-
|
|
1325
|
+
const { extractClaudeUsage } = await import("./claude-C0SMAkM3.js");
|
|
1326
1326
|
return extractClaudeUsage(context);
|
|
1327
1327
|
}
|
|
1328
1328
|
}
|
|
@@ -1402,7 +1402,7 @@ const codexTarget = {
|
|
|
1402
1402
|
timeoutMs: 6e4
|
|
1403
1403
|
},
|
|
1404
1404
|
extract: async (context) => {
|
|
1405
|
-
const { extractCodexUsage } = await import("./codex-
|
|
1405
|
+
const { extractCodexUsage } = await import("./codex-0b2YLh_8.js");
|
|
1406
1406
|
return extractCodexUsage(context);
|
|
1407
1407
|
}
|
|
1408
1408
|
}
|
|
@@ -1606,7 +1606,7 @@ const geminiTarget = {
|
|
|
1606
1606
|
timeoutMs: 7e4
|
|
1607
1607
|
},
|
|
1608
1608
|
extract: async (context) => {
|
|
1609
|
-
const { extractGeminiUsage } = await import("./gemini-
|
|
1609
|
+
const { extractGeminiUsage } = await import("./gemini-BVRg6OMO.js");
|
|
1610
1610
|
return extractGeminiUsage(context);
|
|
1611
1611
|
}
|
|
1612
1612
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { c as cleanControlOutput, b as parsePercentRemaining, m as makeUsageLimit, d as parseResetText } from "./cli.js";
|
|
2
4
|
import { r as runPtyScenario, e as enterKey, t as typeTextSteps } from "./pty-CZBSAJzE.js";
|
|
3
5
|
const CODEX_WINDOWS = [
|
|
@@ -7,7 +9,21 @@ const CODEX_WINDOWS = [
|
|
|
7
9
|
["spark", "weekly", "sparkWeeklyLimit"]
|
|
8
10
|
];
|
|
9
11
|
const CLEAR_LINE = "";
|
|
12
|
+
const CODEX_AUTH_PATH = [".codex", "auth.json"];
|
|
13
|
+
const CODEX_INSTALLATION_ID_PATH = [".codex", "installation_id"];
|
|
14
|
+
const CODEX_USAGE_API_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
15
|
+
const CODEX_USAGE_API_TIMEOUT_MS = 1e4;
|
|
10
16
|
async function extractCodexUsage(context) {
|
|
17
|
+
try {
|
|
18
|
+
return await extractCodexUsageFromApi(context);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (context.signal.aborted) {
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
return extractCodexUsageFromTui(context);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function extractCodexUsageFromTui(context) {
|
|
11
27
|
const command = context.command ?? context.launch?.command ?? "codex";
|
|
12
28
|
const ptyResult = await runPtyScenario({
|
|
13
29
|
command,
|
|
@@ -56,6 +72,244 @@ async function extractCodexUsage(context) {
|
|
|
56
72
|
debug: ptyResult.debug.length > 0 ? ptyResult.debug : void 0
|
|
57
73
|
};
|
|
58
74
|
}
|
|
75
|
+
async function extractCodexUsageFromApi(context) {
|
|
76
|
+
const command = context.command ?? context.launch?.command ?? "codex";
|
|
77
|
+
const auth = await readCodexBackendAuth(context.homeDir);
|
|
78
|
+
if (auth == null) {
|
|
79
|
+
throw new Error("Codex ChatGPT backend auth was not available.");
|
|
80
|
+
}
|
|
81
|
+
const installationId = await readCodexInstallationId(context.homeDir);
|
|
82
|
+
const response = await fetchCodexUsage(auth, installationId, context.signal);
|
|
83
|
+
if (response.status >= 400) {
|
|
84
|
+
throw new Error(`Codex usage API returned HTTP ${response.status}.`);
|
|
85
|
+
}
|
|
86
|
+
const body = await response.json();
|
|
87
|
+
return buildCodexApiUsageResult(body, {
|
|
88
|
+
targetId: context.targetId,
|
|
89
|
+
displayName: context.displayName,
|
|
90
|
+
now: context.now,
|
|
91
|
+
command
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function buildCodexApiUsageResult(payload, context) {
|
|
95
|
+
const usage = isRecord(payload) ? payload : {};
|
|
96
|
+
const limits = [
|
|
97
|
+
...buildCodexApiRateLimitUsageLimits({
|
|
98
|
+
targetId: context.targetId,
|
|
99
|
+
scope: "main",
|
|
100
|
+
rateLimit: usage.rate_limit,
|
|
101
|
+
now: context.now,
|
|
102
|
+
requireBothWindows: true
|
|
103
|
+
}),
|
|
104
|
+
...buildCodexApiAdditionalUsageLimits(usage.additional_rate_limits, context)
|
|
105
|
+
];
|
|
106
|
+
const hasMainHourly = limits.some((limit) => limit.scope === "main" && limit.window === "hourly");
|
|
107
|
+
const hasMainWeekly = limits.some((limit) => limit.scope === "main" && limit.window === "weekly");
|
|
108
|
+
if (!hasMainHourly || !hasMainWeekly) {
|
|
109
|
+
throw new Error("Codex usage API response did not include complete main rate-limit windows.");
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
targetId: context.targetId,
|
|
113
|
+
displayName: context.displayName,
|
|
114
|
+
command: context.command,
|
|
115
|
+
limits
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function extractCodexBackendAuth(blob) {
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(blob);
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (!isRecord(parsed) || !isRecord(parsed.tokens)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const accessToken = parsed.tokens.access_token;
|
|
129
|
+
const accountId = parsed.tokens.account_id;
|
|
130
|
+
if (typeof accessToken !== "string" || typeof accountId !== "string") {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (!accessToken.trim() || !accountId.trim()) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
accessToken,
|
|
138
|
+
accountId
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async function readCodexBackendAuth(homeDir) {
|
|
142
|
+
try {
|
|
143
|
+
const raw = await readFile(path.join(homeDir, ...CODEX_AUTH_PATH), "utf8");
|
|
144
|
+
return extractCodexBackendAuth(raw);
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function readCodexInstallationId(homeDir) {
|
|
150
|
+
try {
|
|
151
|
+
const installationId = await readFile(
|
|
152
|
+
path.join(homeDir, ...CODEX_INSTALLATION_ID_PATH),
|
|
153
|
+
"utf8"
|
|
154
|
+
);
|
|
155
|
+
const trimmed = installationId.trim();
|
|
156
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function fetchCodexUsage(auth, installationId, parentSignal) {
|
|
162
|
+
const controller = new AbortController();
|
|
163
|
+
const timeout = setTimeout(() => {
|
|
164
|
+
controller.abort(new Error("Codex usage API request timed out."));
|
|
165
|
+
}, CODEX_USAGE_API_TIMEOUT_MS);
|
|
166
|
+
const abortFromParent = () => {
|
|
167
|
+
controller.abort(parentSignal.reason);
|
|
168
|
+
};
|
|
169
|
+
parentSignal.addEventListener("abort", abortFromParent, { once: true });
|
|
170
|
+
try {
|
|
171
|
+
const headers = {
|
|
172
|
+
accept: "application/json",
|
|
173
|
+
authorization: `Bearer ${auth.accessToken}`,
|
|
174
|
+
"chatgpt-account-id": auth.accountId,
|
|
175
|
+
"user-agent": "codex-cli"
|
|
176
|
+
};
|
|
177
|
+
if (installationId != null) {
|
|
178
|
+
headers["x-codex-installation-id"] = installationId;
|
|
179
|
+
}
|
|
180
|
+
return await fetch(CODEX_USAGE_API_URL, {
|
|
181
|
+
method: "GET",
|
|
182
|
+
headers,
|
|
183
|
+
signal: controller.signal
|
|
184
|
+
});
|
|
185
|
+
} finally {
|
|
186
|
+
clearTimeout(timeout);
|
|
187
|
+
parentSignal.removeEventListener("abort", abortFromParent);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function buildCodexApiAdditionalUsageLimits(additionalRateLimits, context) {
|
|
191
|
+
if (!Array.isArray(additionalRateLimits)) {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
return additionalRateLimits.flatMap((entry) => {
|
|
195
|
+
if (!isRecord(entry)) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
const limitName = typeof entry.limit_name === "string" ? entry.limit_name : "";
|
|
199
|
+
const meteredFeature = typeof entry.metered_feature === "string" ? entry.metered_feature : "";
|
|
200
|
+
const scope = codexAdditionalLimitScope(limitName, meteredFeature);
|
|
201
|
+
return buildCodexApiRateLimitUsageLimits({
|
|
202
|
+
targetId: context.targetId,
|
|
203
|
+
scope,
|
|
204
|
+
rateLimit: entry.rate_limit,
|
|
205
|
+
now: context.now,
|
|
206
|
+
labelPrefix: scope === "spark" ? void 0 : limitName || meteredFeature || scope,
|
|
207
|
+
requireBothWindows: false
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function buildCodexApiRateLimitUsageLimits(options) {
|
|
212
|
+
if (!isRecord(options.rateLimit)) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
const windows = [
|
|
216
|
+
["primary", options.rateLimit.primary_window],
|
|
217
|
+
["secondary", options.rateLimit.secondary_window]
|
|
218
|
+
];
|
|
219
|
+
const limits = windows.flatMap(([kind, window]) => {
|
|
220
|
+
const limit = makeCodexApiUsageLimit({
|
|
221
|
+
targetId: options.targetId,
|
|
222
|
+
scope: options.scope,
|
|
223
|
+
windowKind: kind,
|
|
224
|
+
window,
|
|
225
|
+
now: options.now,
|
|
226
|
+
labelPrefix: options.labelPrefix
|
|
227
|
+
});
|
|
228
|
+
return limit == null ? [] : [limit];
|
|
229
|
+
});
|
|
230
|
+
if (options.requireBothWindows && limits.length !== 2) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
return limits;
|
|
234
|
+
}
|
|
235
|
+
function makeCodexApiUsageLimit(options) {
|
|
236
|
+
if (!isRecord(options.window)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const percentUsed = parseApiNumber(options.window.used_percent);
|
|
240
|
+
if (percentUsed == null) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const resetAt = parseApiEpochSeconds(options.window.reset_at);
|
|
244
|
+
const window = codexApiWindowName(options.window, options.windowKind);
|
|
245
|
+
const percentUsedClamped = clampPercent(percentUsed);
|
|
246
|
+
const percentRemaining = 100 - percentUsedClamped;
|
|
247
|
+
const resetText = resetAt == null ? null : `resets ${resetAt}`;
|
|
248
|
+
const label = options.labelPrefix == null ? void 0 : `${options.labelPrefix} ${formatWindowLabel(window)}`;
|
|
249
|
+
const raw = `${formatPercent(percentRemaining)} left${resetText == null ? "" : ` (${resetText})`}`;
|
|
250
|
+
const limit = makeUsageLimit({
|
|
251
|
+
targetId: options.targetId,
|
|
252
|
+
scope: options.scope,
|
|
253
|
+
window,
|
|
254
|
+
label,
|
|
255
|
+
percentUsed: percentUsedClamped,
|
|
256
|
+
percentRemaining,
|
|
257
|
+
resetText,
|
|
258
|
+
raw,
|
|
259
|
+
now: options.now
|
|
260
|
+
});
|
|
261
|
+
return {
|
|
262
|
+
...limit,
|
|
263
|
+
resetAt
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function codexAdditionalLimitScope(limitName, meteredFeature) {
|
|
267
|
+
if (/\bspark\b/i.test(limitName) || /\bbengalfox\b/i.test(meteredFeature)) {
|
|
268
|
+
return "spark";
|
|
269
|
+
}
|
|
270
|
+
const source = limitName || meteredFeature || "additional";
|
|
271
|
+
const normalized = source.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
272
|
+
return normalized || "additional";
|
|
273
|
+
}
|
|
274
|
+
function codexApiWindowName(window, kind) {
|
|
275
|
+
const seconds = parseApiNumber(window.limit_window_seconds);
|
|
276
|
+
if (seconds === 18e3) {
|
|
277
|
+
return "5h";
|
|
278
|
+
}
|
|
279
|
+
if (seconds === 604800) {
|
|
280
|
+
return "weekly";
|
|
281
|
+
}
|
|
282
|
+
return kind === "primary" ? "5h" : "weekly";
|
|
283
|
+
}
|
|
284
|
+
function formatWindowLabel(window) {
|
|
285
|
+
return window === "weekly" ? "Weekly" : window === "5h" ? "5h" : window;
|
|
286
|
+
}
|
|
287
|
+
function parseApiNumber(value) {
|
|
288
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const parsed = Number(value);
|
|
295
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
296
|
+
}
|
|
297
|
+
function parseApiEpochSeconds(value) {
|
|
298
|
+
const seconds = parseApiNumber(value);
|
|
299
|
+
if (seconds == null || seconds <= 0) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
return new Date(seconds * 1e3).toISOString();
|
|
303
|
+
}
|
|
304
|
+
function clampPercent(value) {
|
|
305
|
+
return Math.max(0, Math.min(100, value));
|
|
306
|
+
}
|
|
307
|
+
function formatPercent(value) {
|
|
308
|
+
return Number.isInteger(value) ? `${value}%` : `${Number(value.toFixed(2))}%`;
|
|
309
|
+
}
|
|
310
|
+
function isRecord(value) {
|
|
311
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
312
|
+
}
|
|
59
313
|
function selectCodexStatus(result) {
|
|
60
314
|
const snapshots = [
|
|
61
315
|
result.snapshots.statusRetry,
|
|
@@ -277,8 +531,10 @@ function sanitizeCodexValue(value) {
|
|
|
277
531
|
return limitMatch?.[1].trim() ?? sanitized;
|
|
278
532
|
}
|
|
279
533
|
export {
|
|
534
|
+
buildCodexApiUsageResult,
|
|
280
535
|
buildCodexUsageLimits,
|
|
281
536
|
buildCodexUsageResult,
|
|
537
|
+
extractCodexBackendAuth,
|
|
282
538
|
extractCodexUsage,
|
|
283
539
|
parseCodexStatus
|
|
284
540
|
};
|