@ai-devkit/agent-manager 0.35.0 → 0.36.0

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.
Files changed (92) hide show
  1. package/dist/__tests__/adapters/AntigravityCliAdapter.test.js +381 -0
  2. package/dist/__tests__/adapters/AntigravityCliAdapter.test.js.map +1 -0
  3. package/dist/__tests__/adapters/KiroAdapter.test.js +607 -0
  4. package/dist/__tests__/adapters/KiroAdapter.test.js.map +1 -0
  5. package/dist/__tests__/capacity/claude.test.js +485 -0
  6. package/dist/__tests__/capacity/claude.test.js.map +1 -0
  7. package/dist/__tests__/capacity/openai.test.js +672 -0
  8. package/dist/__tests__/capacity/openai.test.js.map +1 -0
  9. package/dist/__tests__/capacity/wham.test.js +144 -0
  10. package/dist/__tests__/capacity/wham.test.js.map +1 -0
  11. package/dist/__tests__/utils/agents.test.js +6 -0
  12. package/dist/__tests__/utils/agents.test.js.map +1 -1
  13. package/dist/adapters/AgentAdapter.d.ts +1 -1
  14. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  15. package/dist/adapters/AgentAdapter.js.map +1 -1
  16. package/dist/adapters/AntigravityCliAdapter.d.ts +57 -0
  17. package/dist/adapters/AntigravityCliAdapter.d.ts.map +1 -0
  18. package/dist/adapters/AntigravityCliAdapter.js +273 -0
  19. package/dist/adapters/AntigravityCliAdapter.js.map +1 -0
  20. package/dist/adapters/KiroAdapter.d.ts +46 -0
  21. package/dist/adapters/KiroAdapter.d.ts.map +1 -0
  22. package/dist/adapters/KiroAdapter.js +344 -0
  23. package/dist/adapters/KiroAdapter.js.map +1 -0
  24. package/dist/adapters/index.d.ts +2 -0
  25. package/dist/adapters/index.d.ts.map +1 -1
  26. package/dist/adapters/index.js +2 -0
  27. package/dist/adapters/index.js.map +1 -1
  28. package/dist/capacity/claude.d.ts +17 -0
  29. package/dist/capacity/claude.d.ts.map +1 -0
  30. package/dist/capacity/claude.js +271 -0
  31. package/dist/capacity/claude.js.map +1 -0
  32. package/dist/capacity/codex.d.ts +1 -2
  33. package/dist/capacity/codex.d.ts.map +1 -1
  34. package/dist/capacity/codex.js +5 -47
  35. package/dist/capacity/codex.js.map +1 -1
  36. package/dist/capacity/index.d.ts +11 -0
  37. package/dist/capacity/index.d.ts.map +1 -1
  38. package/dist/capacity/index.js +17 -0
  39. package/dist/capacity/index.js.map +1 -1
  40. package/dist/capacity/openai.d.ts +48 -0
  41. package/dist/capacity/openai.d.ts.map +1 -0
  42. package/dist/capacity/openai.js +312 -0
  43. package/dist/capacity/openai.js.map +1 -0
  44. package/dist/capacity/wham.d.ts +16 -0
  45. package/dist/capacity/wham.d.ts.map +1 -0
  46. package/dist/capacity/wham.js +66 -0
  47. package/dist/capacity/wham.js.map +1 -0
  48. package/dist/index.d.ts +4 -2
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +3 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/readiness/AgentReadiness.d.ts +1 -1
  53. package/dist/readiness/AgentReadiness.d.ts.map +1 -1
  54. package/dist/readiness/AgentReadiness.js.map +1 -1
  55. package/dist/runtime/ManagedAgentRuntime.js +3 -1
  56. package/dist/runtime/ManagedAgentRuntime.js.map +1 -1
  57. package/dist/runtime/herdr/HerdrAgentDiscovery.js +10 -0
  58. package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -1
  59. package/dist/utils/agents.d.ts +1 -1
  60. package/dist/utils/agents.d.ts.map +1 -1
  61. package/dist/utils/agents.js +11 -0
  62. package/dist/utils/agents.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/__tests__/adapters/AntigravityCliAdapter.test.ts +324 -0
  65. package/src/__tests__/adapters/KiroAdapter.test.ts +534 -0
  66. package/src/__tests__/capacity/claude.test.ts +467 -0
  67. package/src/__tests__/capacity/fixtures/claude-credentials.json +7 -0
  68. package/src/__tests__/capacity/fixtures/claude-usage-malformed.json +1 -0
  69. package/src/__tests__/capacity/fixtures/claude-usage-partial.json +15 -0
  70. package/src/__tests__/capacity/fixtures/claude-usage.json +63 -0
  71. package/src/__tests__/capacity/fixtures/openai-auth.json +6 -0
  72. package/src/__tests__/capacity/fixtures/openai-codex-auth.json +9 -0
  73. package/src/__tests__/capacity/fixtures/openai-usage-completions.json +67 -0
  74. package/src/__tests__/capacity/fixtures/openai-usage-responses.json +20 -0
  75. package/src/__tests__/capacity/fixtures/wham-usage.json +32 -0
  76. package/src/__tests__/capacity/openai.test.ts +570 -0
  77. package/src/__tests__/capacity/wham.test.ts +104 -0
  78. package/src/__tests__/utils/agents.test.ts +7 -0
  79. package/src/adapters/AgentAdapter.ts +2 -0
  80. package/src/adapters/AntigravityCliAdapter.ts +356 -0
  81. package/src/adapters/KiroAdapter.ts +456 -0
  82. package/src/adapters/index.ts +2 -0
  83. package/src/capacity/claude.ts +313 -0
  84. package/src/capacity/codex.ts +25 -134
  85. package/src/capacity/index.ts +33 -4
  86. package/src/capacity/openai.ts +361 -0
  87. package/src/capacity/wham.ts +84 -0
  88. package/src/index.ts +10 -1
  89. package/src/readiness/AgentReadiness.ts +4 -1
  90. package/src/runtime/ManagedAgentRuntime.ts +2 -0
  91. package/src/runtime/herdr/HerdrAgentDiscovery.ts +5 -1
  92. package/src/utils/agents.ts +11 -1
@@ -0,0 +1,313 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, join, resolve } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import type { Availability, CapacityReport, CapacityWindow } from "./types.js";
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ export type ClaudeCapacityOptions = {
11
+ env?: NodeJS.ProcessEnv;
12
+ readFile?: (path: string, encoding: BufferEncoding) => Promise<string>;
13
+ fetch?: typeof globalThis.fetch;
14
+ timeoutMs?: number;
15
+ now?: () => Date;
16
+ cwd?: string;
17
+ platform?: NodeJS.Platform;
18
+ keychainRead?: (service: string) => Promise<string | null>;
19
+ };
20
+
21
+ export type ClaudeProbeOptions = ClaudeCapacityOptions & {
22
+ checkedAt: string;
23
+ };
24
+
25
+ const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
26
+ const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
27
+ const SESSION_DURATION_MINUTES = 5 * 60;
28
+ const WEEK_DURATION_MINUTES = 7 * 24 * 60;
29
+
30
+ type Credential = { token: string | null; expired: boolean };
31
+
32
+ function record(value: unknown): Record<string, unknown> | null {
33
+ return value !== null && typeof value === "object" && !Array.isArray(value)
34
+ ? (value as Record<string, unknown>)
35
+ : null;
36
+ }
37
+
38
+ function percent(value: unknown): number | null {
39
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100
40
+ ? value
41
+ : null;
42
+ }
43
+
44
+ function nonEmptyText(value: unknown): string | null {
45
+ return typeof value === "string" && value.trim() ? value.trim() : null;
46
+ }
47
+
48
+ function credentialsPath(options: ClaudeProbeOptions): string {
49
+ const env = options.env ?? process.env;
50
+ const configured = nonEmptyText(env.CLAUDE_CONFIG_DIR);
51
+ if (configured) {
52
+ const profileRoot = isAbsolute(configured)
53
+ ? configured
54
+ : resolve(options.cwd ?? process.cwd(), configured);
55
+ return join(profileRoot, ".credentials.json");
56
+ }
57
+ return join(env.HOME || homedir(), ".claude", ".credentials.json");
58
+ }
59
+
60
+ function credentialFromPayload(payload: string | null, now: Date): Credential {
61
+ if (!payload) return { token: null, expired: false };
62
+ try {
63
+ return credentialFromRoot(record(JSON.parse(payload)), now);
64
+ } catch {
65
+ return { token: null, expired: false };
66
+ }
67
+ }
68
+
69
+ async function readProfileCredential(options: ClaudeProbeOptions, now: Date): Promise<Credential> {
70
+ try {
71
+ const payload = await (options.readFile ?? readFile)(credentialsPath(options), "utf8");
72
+ return credentialFromPayload(payload, now);
73
+ } catch {
74
+ return { token: null, expired: false };
75
+ }
76
+ }
77
+
78
+ async function readKeychainCredential(options: ClaudeProbeOptions, now: Date): Promise<Credential> {
79
+ const env = options.env ?? process.env;
80
+ const usesDefaultProfile = !nonEmptyText(env.CLAUDE_CONFIG_DIR);
81
+ if (!usesDefaultProfile || (options.platform ?? process.platform) !== "darwin") {
82
+ return { token: null, expired: false };
83
+ }
84
+ try {
85
+ const payload = await (options.keychainRead ?? readClaudeKeychain)(CLAUDE_KEYCHAIN_SERVICE);
86
+ return credentialFromPayload(payload, now);
87
+ } catch {
88
+ return { token: null, expired: false };
89
+ }
90
+ }
91
+
92
+ async function resolveToken(options: ClaudeProbeOptions): Promise<string> {
93
+ const env = options.env ?? process.env;
94
+ const environmentToken = nonEmptyText(env.CLAUDE_CODE_OAUTH_TOKEN);
95
+ if (environmentToken) return environmentToken;
96
+
97
+ const now = options.now?.() ?? new Date();
98
+ const profileCredential = await readProfileCredential(options, now);
99
+ if (profileCredential.token) return profileCredential.token;
100
+
101
+ const keychainCredential = await readKeychainCredential(options, now);
102
+ if (keychainCredential.token) return keychainCredential.token;
103
+ if (profileCredential.expired || keychainCredential.expired) {
104
+ throw new Error("Claude OAuth credentials expired");
105
+ }
106
+ throw new Error("Claude OAuth credentials not found or malformed");
107
+ }
108
+
109
+ function credentialFromRoot(root: Record<string, unknown> | null, now: Date): Credential {
110
+ const oauth = record(root?.claudeAiOauth);
111
+ const token = nonEmptyText(oauth?.accessToken);
112
+ if (!token) return { token: null, expired: false };
113
+ const expired =
114
+ typeof oauth?.expiresAt === "number" &&
115
+ Number.isFinite(oauth.expiresAt) &&
116
+ oauth.expiresAt <= now.getTime();
117
+ return { token: expired ? null : token, expired };
118
+ }
119
+
120
+ async function readClaudeKeychain(service: string): Promise<string | null> {
121
+ try {
122
+ const { stdout } = await execFileAsync(
123
+ "/usr/bin/security",
124
+ ["find-generic-password", "-s", service, "-w"],
125
+ { encoding: "utf8", timeout: 1500, maxBuffer: 1024 * 1024 },
126
+ );
127
+ return nonEmptyText(stdout);
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ function resetTime(value: unknown): string | null {
134
+ if (typeof value !== "string") return null;
135
+ const milliseconds = Date.parse(value);
136
+ return Number.isNaN(milliseconds) ? null : new Date(milliseconds).toISOString();
137
+ }
138
+
139
+ function usageWindow(
140
+ value: unknown,
141
+ id: string,
142
+ label: string,
143
+ durationMinutes: number,
144
+ ): CapacityWindow | null {
145
+ const input = record(value);
146
+ if (!input) return null;
147
+ return {
148
+ id,
149
+ label,
150
+ durationMinutes,
151
+ usedPercent: percent(input.utilization),
152
+ resetsAt: resetTime(input.resets_at),
153
+ };
154
+ }
155
+
156
+ function slug(value: string): string {
157
+ return value
158
+ .toLowerCase()
159
+ .replace(/[^a-z0-9]+/g, "-")
160
+ .replace(/^-|-$/g, "");
161
+ }
162
+
163
+ function scopedWindows(value: unknown): CapacityWindow[] {
164
+ if (!Array.isArray(value)) return [];
165
+ const seen = new Set<string>();
166
+ const windows: CapacityWindow[] = [];
167
+ for (const entry of value) {
168
+ const limit = record(entry);
169
+ const model = record(record(limit?.scope)?.model);
170
+ const name = nonEmptyText(model?.display_name);
171
+ const identity = nonEmptyText(model?.id) ?? name;
172
+ if (limit?.kind !== "weekly_scoped" || limit.group !== "weekly" || !name || !identity) {
173
+ continue;
174
+ }
175
+ const modelSlug = slug(identity);
176
+ if (!modelSlug || modelSlug === "all-models" || modelSlug.endsWith("-all-models")) {
177
+ continue;
178
+ }
179
+ const id = `claude:weekly:${modelSlug}`;
180
+ if (seen.has(id)) continue;
181
+ seen.add(id);
182
+ windows.push({
183
+ id,
184
+ label: `${name} weekly`,
185
+ durationMinutes: WEEK_DURATION_MINUTES,
186
+ usedPercent: percent(limit.percent),
187
+ resetsAt: resetTime(limit.resets_at),
188
+ });
189
+ }
190
+ return windows;
191
+ }
192
+
193
+ function finiteNumber(value: unknown): number | null {
194
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
195
+ }
196
+
197
+ function extraUsageWindow(value: unknown): CapacityWindow | null {
198
+ const extra = record(value);
199
+ if (!extra || extra.is_enabled !== true) return null;
200
+ const limitMinor = finiteNumber(extra.monthly_limit);
201
+ const usedMinor = finiteNumber(extra.used_credits);
202
+ if (limitMinor === null || usedMinor === null) return null;
203
+ const total = limitMinor / 100;
204
+ const current = usedMinor / 100;
205
+ const currency = nonEmptyText(extra.currency)?.toUpperCase() ?? "USD";
206
+ return {
207
+ id: "claude:extra-usage",
208
+ label: `Extra usage · ${currency}`,
209
+ limitType: "CREDIT_LIMIT",
210
+ durationMinutes: null,
211
+ usedPercent:
212
+ percent(extra.utilization) ??
213
+ (total > 0 ? Math.max(0, Math.min(100, (current / total) * 100)) : null),
214
+ resetsAt: null,
215
+ total,
216
+ current,
217
+ remaining: Math.max(0, total - current),
218
+ };
219
+ }
220
+
221
+ function availability(windows: CapacityWindow[]): Availability {
222
+ const known = windows.flatMap((window) =>
223
+ window.usedPercent === null ? [] : [window.usedPercent],
224
+ );
225
+ if (known.length === 0) return "unknown";
226
+ return known.some((used) => used < 100) ? "yes" : "no";
227
+ }
228
+
229
+ function retryAfterTime(response: Response, now: Date): string | null {
230
+ const raw = response.headers.get("Retry-After")?.trim();
231
+ if (!raw) return null;
232
+ const seconds = Number(raw);
233
+ if (Number.isFinite(seconds) && seconds >= 0) {
234
+ return new Date(now.getTime() + seconds * 1000).toISOString();
235
+ }
236
+ const milliseconds = Date.parse(raw);
237
+ return Number.isNaN(milliseconds) ? null : new Date(milliseconds).toISOString();
238
+ }
239
+
240
+ export function parseClaudeUsage(raw: unknown, checkedAt: string): CapacityReport {
241
+ const input = record(raw);
242
+ if (!input) throw new Error("Claude usage response is malformed");
243
+ const windows = [
244
+ usageWindow(input.five_hour, "session", "Session", SESSION_DURATION_MINUTES),
245
+ usageWindow(input.seven_day, "weekly", "Weekly", WEEK_DURATION_MINUTES),
246
+ usageWindow(
247
+ input.seven_day_sonnet,
248
+ "claude:sonnet:weekly",
249
+ "Sonnet weekly",
250
+ WEEK_DURATION_MINUTES,
251
+ ),
252
+ usageWindow(input.seven_day_opus, "claude:opus:weekly", "Opus weekly", WEEK_DURATION_MINUTES),
253
+ ...scopedWindows(input.limits),
254
+ extraUsageWindow(input.extra_usage),
255
+ ].filter((window): window is CapacityWindow => window !== null);
256
+ return {
257
+ harness: "claude",
258
+ provider: "anthropic",
259
+ generatedAt: checkedAt,
260
+ authenticated: true,
261
+ available: availability(windows),
262
+ windows,
263
+ creditsRemaining: null,
264
+ };
265
+ }
266
+
267
+ export async function probeClaudeCapacity(options: ClaudeProbeOptions): Promise<CapacityReport> {
268
+ const token = await resolveToken(options);
269
+ const controller = new AbortController();
270
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 5000);
271
+ let response: Response;
272
+ try {
273
+ response = await (options.fetch ?? globalThis.fetch)(CLAUDE_USAGE_URL, {
274
+ method: "GET",
275
+ headers: {
276
+ Accept: "application/json",
277
+ Authorization: `Bearer ${token}`,
278
+ "Content-Type": "application/json",
279
+ "User-Agent": "claude-code/2.1.0",
280
+ "anthropic-beta": "oauth-2025-04-20",
281
+ },
282
+ signal: controller.signal,
283
+ });
284
+ } catch {
285
+ throw new Error("Claude OAuth usage request failed");
286
+ } finally {
287
+ clearTimeout(timer);
288
+ }
289
+ if (response.status === 401) {
290
+ throw new Error("Claude OAuth request unauthorized (HTTP 401)");
291
+ }
292
+ if (response.status === 403) {
293
+ throw new Error("Claude OAuth request forbidden (HTTP 403)");
294
+ }
295
+ if (response.status === 429) {
296
+ const retryAt = retryAfterTime(response, options.now?.() ?? new Date());
297
+ throw new Error(
298
+ retryAt
299
+ ? `Claude OAuth usage rate limited until ${retryAt}`
300
+ : "Claude OAuth usage rate limited (HTTP 429)",
301
+ );
302
+ }
303
+ if (!response.ok) {
304
+ throw new Error(`Claude OAuth usage request failed (HTTP ${response.status})`);
305
+ }
306
+ let raw: unknown;
307
+ try {
308
+ raw = await response.json();
309
+ } catch {
310
+ throw new Error("Claude usage response is malformed");
311
+ }
312
+ return parseClaudeUsage(raw, options.checkedAt);
313
+ }
@@ -1,8 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
+ import { parseWhamUsage, resetTime, safeIdentifier } from "./wham.js";
4
5
  import type { CapacityReport, CapacityWindow } from "./types.js";
5
6
 
7
+ export { toRateWindow } from "./wham.js";
8
+
6
9
  type CodexUsageSource = "pat" | "oauth" | "cli";
7
10
  type UsageSnapshot = {
8
11
  windows: CapacityWindow[];
@@ -15,13 +18,7 @@ type RpcMessage = { id?: number; method: string; params?: UnknownRecord };
15
18
  type CliResponses = { rateLimits: unknown; account: unknown };
16
19
  type CodexRpc = (messages: RpcMessage[]) => Promise<CliResponses>;
17
20
 
18
- export const CODEX_APP_SERVER_ARGS = [
19
- "-s",
20
- "read-only",
21
- "-a",
22
- "untrusted",
23
- "app-server",
24
- ] as const;
21
+ export const CODEX_APP_SERVER_ARGS = ["-s", "read-only", "-a", "untrusted", "app-server"] as const;
25
22
 
26
23
  type CodexProbeOptions = {
27
24
  installed: boolean;
@@ -48,91 +45,17 @@ function nonEmptyText(value: unknown): string | null {
48
45
  return typeof value === "string" && value.length > 0 ? value : null;
49
46
  }
50
47
 
51
- function resetTime(value: unknown): string | null {
52
- const seconds = finiteNumber(value);
53
- if (seconds !== null) return new Date(seconds * 1000).toISOString();
54
- if (typeof value === "string" && !Number.isNaN(Date.parse(value)))
55
- return new Date(value).toISOString();
56
- return null;
57
- }
58
-
59
- function safeIdentifier(value: unknown): string | null {
60
- const candidate = nonEmptyText(value);
61
- if (!candidate || !/^[a-z][a-z0-9_-]{0,63}$/i.test(candidate)) return null;
62
- if (/(?:account|token|secret|key)[_-]?\d{6,}/i.test(candidate)) return null;
63
- return candidate;
64
- }
65
-
66
- export function resolveCodexAuthPath(
67
- env: NodeJS.ProcessEnv = process.env,
68
- ): string {
48
+ export function resolveCodexAuthPath(env: NodeJS.ProcessEnv = process.env): string {
69
49
  const root = env.CODEX_HOME || join(env.HOME || "", ".codex");
70
50
  return join(root, "auth.json");
71
51
  }
72
52
 
73
- export function toRateWindow(
74
- value: unknown,
75
- id: string,
76
- label: string,
77
- ): CapacityWindow | null {
78
- const input = record(value);
79
- if (!input) return null;
80
- const used = finiteNumber(input.used_percent);
81
- const seconds = finiteNumber(input.limit_window_seconds);
82
- return {
83
- id,
84
- label,
85
- durationMinutes: seconds === null ? null : seconds / 60,
86
- usedPercent: used,
87
- resetsAt: resetTime(input.reset_at),
88
- };
89
- }
90
-
91
- function extraWindows(value: unknown): CapacityWindow[] {
92
- if (!Array.isArray(value)) return [];
93
- return value.flatMap((entry, index) => {
94
- const limit = record(entry);
95
- if (!limit) return [];
96
- const scope = safeIdentifier(limit.limit_name) ?? `extra-${index + 1}`;
97
- const windows = record(limit.rate_limit) ?? limit;
98
- return [
99
- toRateWindow(
100
- windows.primary_window,
101
- `${scope}:primary`,
102
- `${scope} primary`,
103
- ),
104
- toRateWindow(
105
- windows.secondary_window,
106
- `${scope}:secondary`,
107
- `${scope} secondary`,
108
- ),
109
- ].filter((window): window is CapacityWindow => window !== null);
110
- });
53
+ export function parseUsage(raw: unknown, source: "pat" | "oauth"): UsageSnapshot {
54
+ const { windows, creditsRemaining } = parseWhamUsage(raw);
55
+ return { windows, creditsRemaining, source };
111
56
  }
112
57
 
113
- export function parseUsage(
114
- raw: unknown,
115
- source: "pat" | "oauth",
116
- ): UsageSnapshot {
117
- const response = record(raw) ?? {};
118
- const limits = record(response.rate_limit) ?? {};
119
- const credits = record(response.credits) ?? {};
120
- return {
121
- windows: [
122
- toRateWindow(limits.primary_window, "session", "Session"),
123
- toRateWindow(limits.secondary_window, "weekly", "Weekly"),
124
- ...extraWindows(response.additional_rate_limits),
125
- ].filter((window): window is CapacityWindow => window !== null),
126
- creditsRemaining: finiteNumber(credits.balance),
127
- source,
128
- };
129
- }
130
-
131
- function cliWindow(
132
- value: unknown,
133
- id: string,
134
- label: string,
135
- ): CapacityWindow | null {
58
+ function cliWindow(value: unknown, id: string, label: string): CapacityWindow | null {
136
59
  const input = record(value);
137
60
  if (!input) return null;
138
61
  return {
@@ -144,14 +67,10 @@ function cliWindow(
144
67
  };
145
68
  }
146
69
 
147
- function cliSnapshotWindows(
148
- value: unknown,
149
- fallbackId: string,
150
- ): CapacityWindow[] {
70
+ function cliSnapshotWindows(value: unknown, fallbackId: string): CapacityWindow[] {
151
71
  const snapshot = record(value);
152
72
  if (!snapshot) return [];
153
- const scope =
154
- safeIdentifier(snapshot.limitId) ?? safeIdentifier(fallbackId) ?? "codex";
73
+ const scope = safeIdentifier(snapshot.limitId) ?? safeIdentifier(fallbackId) ?? "codex";
155
74
  return [
156
75
  cliWindow(snapshot.primary, `${scope}:primary`, `${scope} primary`),
157
76
  cliWindow(snapshot.secondary, `${scope}:secondary`, `${scope} secondary`),
@@ -167,9 +86,7 @@ export function parseCliUsage(raw: unknown): UsageSnapshot {
167
86
  for (const [id, snapshot] of Object.entries(buckets))
168
87
  windows.push(...cliSnapshotWindows(snapshot, id));
169
88
  }
170
- const unique = [
171
- ...new Map(windows.map((window) => [window.id, window])).values(),
172
- ];
89
+ const unique = [...new Map(windows.map((window) => [window.id, window])).values()];
173
90
  return { windows: unique, creditsRemaining: null, source: "cli" };
174
91
  }
175
92
 
@@ -178,14 +95,11 @@ function capacityFromSnapshot(
178
95
  context: CodexProbeOptions,
179
96
  raw?: unknown,
180
97
  ): CapacityReport {
181
- const hasUsage = snapshot.windows.some(
182
- (window) => window.usedPercent !== null,
183
- );
98
+ const hasUsage = snapshot.windows.some((window) => window.usedPercent !== null);
184
99
  const rateLimits = record(record(raw)?.rateLimits);
185
100
  const reached = nonEmptyText(rateLimits?.rateLimitReachedType);
186
101
  const resetCredits =
187
- record(record(raw)?.rateLimitResetCredits) ??
188
- record(record(raw)?.usageLimitResetCredits);
102
+ record(record(raw)?.rateLimitResetCredits) ?? record(record(raw)?.usageLimitResetCredits);
189
103
  return {
190
104
  harness: "codex",
191
105
  provider: "openai",
@@ -193,8 +107,7 @@ function capacityFromSnapshot(
193
107
  authenticated: true,
194
108
  available: reached ? "no" : hasUsage ? "yes" : "unknown",
195
109
  windows: snapshot.windows,
196
- creditsRemaining:
197
- snapshot.creditsRemaining ?? finiteNumber(resetCredits?.availableCount),
110
+ creditsRemaining: snapshot.creditsRemaining ?? finiteNumber(resetCredits?.availableCount),
198
111
  };
199
112
  }
200
113
 
@@ -202,9 +115,7 @@ function jwtExpiry(token: string): number | null {
202
115
  const part = token.split(".")[1];
203
116
  if (!part) return null;
204
117
  try {
205
- return finiteNumber(
206
- record(JSON.parse(Buffer.from(part, "base64url").toString("utf8")))?.exp,
207
- );
118
+ return finiteNumber(record(JSON.parse(Buffer.from(part, "base64url").toString("utf8")))?.exp);
208
119
  } catch {
209
120
  return null;
210
121
  }
@@ -231,10 +142,7 @@ async function fetchJson(
231
142
  const timer = setTimeout(() => controller.abort(), timeoutMs);
232
143
  try {
233
144
  const response = await fetcher(url, { ...init, signal: controller.signal });
234
- if (!response.ok)
235
- throw new Error(
236
- response.status === 401 ? "unauthorized" : "request failed",
237
- );
145
+ if (!response.ok) throw new Error(response.status === 401 ? "unauthorized" : "request failed");
238
146
  return await response.json();
239
147
  } finally {
240
148
  clearTimeout(timer);
@@ -262,10 +170,7 @@ async function apiSnapshot(
262
170
  return parseUsage(raw, source);
263
171
  }
264
172
 
265
- function appServerRpc(
266
- messages: RpcMessage[],
267
- timeoutMs = 5000,
268
- ): Promise<CliResponses> {
173
+ function appServerRpc(messages: RpcMessage[], timeoutMs = 5000): Promise<CliResponses> {
269
174
  return new Promise((resolve, reject) => {
270
175
  const child = spawn("codex", CODEX_APP_SERVER_ARGS, {
271
176
  stdio: ["pipe", "pipe", "ignore"],
@@ -281,13 +186,8 @@ function appServerRpc(
281
186
  if (error) reject(error);
282
187
  else resolve(results as CliResponses);
283
188
  };
284
- const timer = setTimeout(
285
- () => finish(new Error("codex probe timed out")),
286
- timeoutMs,
287
- );
288
- child.once("error", () =>
289
- finish(new Error("codex app-server unavailable")),
290
- );
189
+ const timer = setTimeout(() => finish(new Error("codex probe timed out")), timeoutMs);
190
+ child.once("error", () => finish(new Error("codex app-server unavailable")));
291
191
  child.once("exit", () => {
292
192
  if (!settled) finish(new Error("codex app-server exited"));
293
193
  });
@@ -310,8 +210,7 @@ function appServerRpc(
310
210
  for (const request of messages.slice(1))
311
211
  child.stdin.write(`${JSON.stringify(request)}\n`);
312
212
  } else if (message.id === 2) {
313
- if (message.error)
314
- finish(new Error("codex rate-limit method failed"));
213
+ if (message.error) finish(new Error("codex rate-limit method failed"));
315
214
  else results.rateLimits = message.result;
316
215
  } else if (message.id === 3) {
317
216
  if (message.error) finish(new Error("codex account method failed"));
@@ -340,9 +239,7 @@ function unavailable(options: CodexProbeOptions): CapacityReport {
340
239
  return codexUnavailableReport(options.checkedAt);
341
240
  }
342
241
 
343
- async function cliFallback(
344
- options: CodexProbeOptions,
345
- ): Promise<CapacityReport> {
242
+ async function cliFallback(options: CodexProbeOptions): Promise<CapacityReport> {
346
243
  if (!options.installed) return unavailable(options);
347
244
  const messages: RpcMessage[] = [
348
245
  {
@@ -358,8 +255,7 @@ async function cliFallback(
358
255
  { id: 3, method: "account/read" },
359
256
  ];
360
257
  try {
361
- const rpc =
362
- options.rpc ?? ((requests) => appServerRpc(requests, options.timeoutMs));
258
+ const rpc = options.rpc ?? ((requests) => appServerRpc(requests, options.timeoutMs));
363
259
  const response = await rpc(messages);
364
260
  const result = capacityFromSnapshot(
365
261
  parseCliUsage(response.rateLimits),
@@ -381,9 +277,7 @@ async function cliFallback(
381
277
  }
382
278
  }
383
279
 
384
- export async function probeCodexCapacity(
385
- options: CodexProbeOptions,
386
- ): Promise<CapacityReport> {
280
+ export async function probeCodexCapacity(options: CodexProbeOptions): Promise<CapacityReport> {
387
281
  let parsed: UnknownRecord | null = null;
388
282
  try {
389
283
  const contents = await (options.readFile ?? readFile)(
@@ -410,10 +304,7 @@ export async function probeCodexCapacity(
410
304
  );
411
305
  const accountId = nonEmptyText(whoami?.chatgpt_account_id);
412
306
  if (!accountId) throw new Error("account unavailable");
413
- return capacityFromSnapshot(
414
- await apiSnapshot(pat, accountId, "pat", options),
415
- options,
416
- );
307
+ return capacityFromSnapshot(await apiSnapshot(pat, accountId, "pat", options), options);
417
308
  } catch {
418
309
  // Continue to a separately available OAuth credential before using the CLI.
419
310
  }
@@ -2,10 +2,13 @@ import { constants } from "node:fs";
2
2
  import { access as fsAccess } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { codexUnavailableReport, probeCodexCapacity } from "./codex.js";
5
+ import { probeClaudeCapacity, type ClaudeCapacityOptions } from "./claude.js";
6
+ import { probeOpenAiCapacity } from "./openai.js";
5
7
  import { probeZaiCapacity } from "./zai.js";
6
8
  import type { CapacityReport } from "./types.js";
7
9
 
8
10
  export type { CapacityReport, CapacityWindow } from "./types.js";
11
+ export type { ClaudeCapacityOptions } from "./claude.js";
9
12
 
10
13
  export type CapacityProbeOptions = {
11
14
  now?: () => Date;
@@ -22,6 +25,14 @@ export type ZaiCapacityOptions = {
22
25
  timeoutMs?: number;
23
26
  };
24
27
 
28
+ export type OpenAiCapacityOptions = {
29
+ now?: () => Date;
30
+ env?: NodeJS.ProcessEnv;
31
+ readFile?: (path: string, encoding: BufferEncoding) => Promise<string>;
32
+ fetch?: typeof globalThis.fetch;
33
+ timeoutMs?: number;
34
+ };
35
+
25
36
  async function canAccess(target: string, mode: number): Promise<boolean> {
26
37
  try {
27
38
  await fsAccess(target, mode);
@@ -55,10 +66,7 @@ export async function getCodexCapacityReport(
55
66
  options: CapacityProbeOptions = {},
56
67
  ): Promise<CapacityReport> {
57
68
  const generatedAt = (options.now?.() ?? new Date()).toISOString();
58
- const installed = await isCodexInstalled(
59
- options.path ?? process.env.PATH ?? "",
60
- options.access,
61
- );
69
+ const installed = await isCodexInstalled(options.path ?? process.env.PATH ?? "", options.access);
62
70
  try {
63
71
  return await (options.probe ?? probeCodexCapacity)({
64
72
  installed,
@@ -78,3 +86,24 @@ export async function getZaiCapacityReport(
78
86
  checkedAt: (now?.() ?? new Date()).toISOString(),
79
87
  });
80
88
  }
89
+
90
+ export async function getOpenAiCapacityReport(
91
+ options: OpenAiCapacityOptions = {},
92
+ ): Promise<CapacityReport> {
93
+ const { now, ...probeOptions } = options;
94
+ return probeOpenAiCapacity({
95
+ ...probeOptions,
96
+ checkedAt: (now?.() ?? new Date()).toISOString(),
97
+ });
98
+ }
99
+
100
+ export async function getClaudeCapacityReport(
101
+ options: ClaudeCapacityOptions = {},
102
+ ): Promise<CapacityReport> {
103
+ const { now, ...probeOptions } = options;
104
+ return probeClaudeCapacity({
105
+ ...probeOptions,
106
+ checkedAt: (now?.() ?? new Date()).toISOString(),
107
+ now,
108
+ });
109
+ }