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.
@@ -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-Dmv_YFKX.js");
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-D1RuzsY6.js");
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-CskI3Qjp.js");
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
  };