@junctionpanel/server 0.1.49 → 0.1.51

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 (29) hide show
  1. package/dist/server/client/daemon-client.d.ts +4 -1
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +11 -0
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-sdk-types.d.ts +7 -0
  6. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  7. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +25 -0
  8. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  9. package/dist/server/server/agent/providers/codex-app-server-agent.js +45 -0
  10. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  11. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +21 -0
  12. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  13. package/dist/server/server/daemon-provider-rate-limits.d.ts +72 -0
  14. package/dist/server/server/daemon-provider-rate-limits.d.ts.map +1 -0
  15. package/dist/server/server/daemon-provider-rate-limits.js +1174 -0
  16. package/dist/server/server/daemon-provider-rate-limits.js.map +1 -0
  17. package/dist/server/server/session.d.ts +1 -0
  18. package/dist/server/server/session.d.ts.map +1 -1
  19. package/dist/server/server/session.js +39 -1
  20. package/dist/server/server/session.js.map +1 -1
  21. package/dist/server/server/tool-call-preview.d.ts +6 -0
  22. package/dist/server/server/tool-call-preview.d.ts.map +1 -0
  23. package/dist/server/server/tool-call-preview.js +174 -0
  24. package/dist/server/server/tool-call-preview.js.map +1 -0
  25. package/dist/server/shared/messages.d.ts +1135 -20
  26. package/dist/server/shared/messages.d.ts.map +1 -1
  27. package/dist/server/shared/messages.js +44 -0
  28. package/dist/server/shared/messages.js.map +1 -1
  29. package/package.json +2 -2
@@ -0,0 +1,1174 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile, readdir, stat } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import path from "node:path";
6
+ import { applyProviderEnv, resolveProviderCommandPrefix, } from "./agent/provider-launch-config.js";
7
+ import { readCodexAppServerRateLimits, } from "./agent/providers/codex-app-server-agent.js";
8
+ import { loadPersistedConfig } from "./persisted-config.js";
9
+ const RATE_LIMIT_COMMAND_TIMEOUT_MS = 5000;
10
+ const RATE_LIMIT_COMMAND_MAX_BUFFER_BYTES = 256 * 1024;
11
+ const RATE_LIMIT_READER_TIMEOUT_MS = 5000;
12
+ const KEYCHAIN_COMMAND_MAX_BUFFER_BYTES = 2 * 1024 * 1024;
13
+ const CLAUDE_ENV_SOURCE_LABEL = "daemon environment";
14
+ const CLAUDE_WEB_SOURCE_LABEL = "claude.ai usage endpoint";
15
+ const CLAUDE_OAUTH_SOURCE_LABEL = "claude oauth rate-limit headers";
16
+ const CLAUDE_KEYCHAIN_SERVICE_NAME = "Claude Code-credentials";
17
+ const CODEX_AUTH_SOURCE_LABEL = ".codex/auth.json";
18
+ const GEMINI_ACCOUNTS_SOURCE_LABEL = ".gemini/google_accounts.json";
19
+ const GEMINI_SETTINGS_SOURCE_LABEL = ".gemini/settings.json";
20
+ const GEMINI_ENV_SOURCE_LABEL = "daemon environment";
21
+ async function runBufferedCommand(command, args, options) {
22
+ return await new Promise((resolve) => {
23
+ let stdout = "";
24
+ let stderr = "";
25
+ let resolved = false;
26
+ let timedOut = false;
27
+ let exceededMaxBuffer = false;
28
+ const child = spawn(command, args, {
29
+ env: options.env,
30
+ stdio: ["ignore", "pipe", "pipe"],
31
+ });
32
+ const timer = setTimeout(() => {
33
+ timedOut = true;
34
+ child.kill("SIGKILL");
35
+ }, options.timeoutMs);
36
+ const finalize = (input) => {
37
+ if (resolved) {
38
+ return;
39
+ }
40
+ resolved = true;
41
+ clearTimeout(timer);
42
+ const finalStderr = input.stderr ?? stderr;
43
+ resolve({
44
+ status: input.status,
45
+ stdout,
46
+ stderr: finalStderr,
47
+ signal: input.signal,
48
+ pid: child.pid,
49
+ output: [stdout, finalStderr],
50
+ });
51
+ };
52
+ const appendChunk = (current, chunk) => {
53
+ const next = current + chunk;
54
+ if (Buffer.byteLength(next, "utf8") <= options.maxBufferBytes) {
55
+ return next;
56
+ }
57
+ exceededMaxBuffer = true;
58
+ child.kill("SIGKILL");
59
+ return current;
60
+ };
61
+ child.stdout?.setEncoding("utf8");
62
+ child.stderr?.setEncoding("utf8");
63
+ child.stdout?.on("data", (chunk) => {
64
+ stdout = appendChunk(stdout, chunk);
65
+ });
66
+ child.stderr?.on("data", (chunk) => {
67
+ stderr = appendChunk(stderr, chunk);
68
+ });
69
+ child.on("error", (error) => {
70
+ finalize({
71
+ status: 1,
72
+ signal: null,
73
+ stderr: error.message || `Failed to launch ${command}.`,
74
+ });
75
+ });
76
+ child.on("close", (status, signal) => {
77
+ if (timedOut) {
78
+ finalize({
79
+ status: 124,
80
+ signal,
81
+ stderr: stderr || `${command} timed out after ${options.timeoutMs}ms.`,
82
+ });
83
+ return;
84
+ }
85
+ if (exceededMaxBuffer) {
86
+ finalize({
87
+ status: 1,
88
+ signal,
89
+ stderr: stderr ||
90
+ `${command} output exceeded ${options.maxBufferBytes} bytes while loading provider rate limits.`,
91
+ });
92
+ return;
93
+ }
94
+ finalize({
95
+ status,
96
+ signal,
97
+ });
98
+ });
99
+ });
100
+ }
101
+ async function defaultCommandRunner(command, args, options) {
102
+ return await runBufferedCommand(command, args, {
103
+ env: options.env,
104
+ maxBufferBytes: RATE_LIMIT_COMMAND_MAX_BUFFER_BYTES,
105
+ timeoutMs: RATE_LIMIT_COMMAND_TIMEOUT_MS,
106
+ });
107
+ }
108
+ function createUnavailableWindow(input) {
109
+ return {
110
+ ...input,
111
+ unitLabel: null,
112
+ limit: null,
113
+ used: null,
114
+ remaining: null,
115
+ };
116
+ }
117
+ function titleCase(value) {
118
+ if (!value) {
119
+ return null;
120
+ }
121
+ return value
122
+ .split(/[\s_-]+/)
123
+ .filter((part) => part.length > 0)
124
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
125
+ .join(" ");
126
+ }
127
+ function toObject(value) {
128
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
129
+ return null;
130
+ }
131
+ return value;
132
+ }
133
+ function toNumber(value) {
134
+ if (typeof value === "number" && Number.isFinite(value)) {
135
+ return value;
136
+ }
137
+ if (typeof value === "string") {
138
+ const parsed = Number(value);
139
+ return Number.isFinite(parsed) ? parsed : null;
140
+ }
141
+ return null;
142
+ }
143
+ function toIsoString(value) {
144
+ if (typeof value === "string") {
145
+ const parsed = new Date(value);
146
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
147
+ }
148
+ if (typeof value === "number" && Number.isFinite(value)) {
149
+ const normalized = value > 10000000000 ? value : value * 1000;
150
+ const parsed = new Date(normalized);
151
+ return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
152
+ }
153
+ return null;
154
+ }
155
+ function formatDurationLabel(minutes) {
156
+ if (minutes === 300) {
157
+ return "5 hour window";
158
+ }
159
+ if (minutes === 1440) {
160
+ return "24 hour window";
161
+ }
162
+ if (minutes === 10080) {
163
+ return "7 day window";
164
+ }
165
+ if (minutes !== null && minutes > 0) {
166
+ if (minutes % 1440 === 0) {
167
+ const days = minutes / 1440;
168
+ return `${days} day window`;
169
+ }
170
+ if (minutes % 60 === 0) {
171
+ const hours = minutes / 60;
172
+ return `${hours} hour window`;
173
+ }
174
+ return `${minutes} minute window`;
175
+ }
176
+ return "Usage window";
177
+ }
178
+ function formatBucketLabel(limitId, limitName) {
179
+ if (limitName && limitName.trim()) {
180
+ return limitName.trim();
181
+ }
182
+ if (!limitId || !limitId.trim()) {
183
+ return "Codex";
184
+ }
185
+ if (limitId === "codex") {
186
+ return "Codex";
187
+ }
188
+ return titleCase(limitId) ?? "Codex";
189
+ }
190
+ function parseCodexRateLimitWindow(value) {
191
+ const object = toObject(value);
192
+ if (!object) {
193
+ return null;
194
+ }
195
+ return {
196
+ usedPercent: toNumber(object.usedPercent ?? object.used_percent),
197
+ windowDurationMins: toNumber(object.windowDurationMins ??
198
+ object.window_duration_mins ??
199
+ object.windowMinutes ??
200
+ object.window_minutes),
201
+ resetsAt: (typeof object.resetsAt === "string" || typeof object.resetsAt === "number"
202
+ ? object.resetsAt
203
+ : null) ??
204
+ (typeof object.resets_at === "string" || typeof object.resets_at === "number"
205
+ ? object.resets_at
206
+ : null) ??
207
+ (typeof object.resetAt === "string" || typeof object.resetAt === "number"
208
+ ? object.resetAt
209
+ : null) ??
210
+ (typeof object.reset_at === "string" || typeof object.reset_at === "number"
211
+ ? object.reset_at
212
+ : null),
213
+ };
214
+ }
215
+ function parseCodexRateLimitBucket(limitId, value) {
216
+ const object = toObject(value);
217
+ if (!object) {
218
+ return null;
219
+ }
220
+ const primary = parseCodexRateLimitWindow(object.primary ?? object.primary_window);
221
+ const secondary = parseCodexRateLimitWindow(object.secondary ?? object.secondary_window);
222
+ if (!primary && !secondary) {
223
+ return null;
224
+ }
225
+ return {
226
+ limitId: (typeof object.limitId === "string" && object.limitId.trim()) ||
227
+ (typeof object.limit_id === "string" && object.limit_id.trim()) ||
228
+ limitId ||
229
+ null,
230
+ limitName: (typeof object.limitName === "string" && object.limitName.trim()) ||
231
+ (typeof object.limit_name === "string" && object.limit_name.trim()) ||
232
+ null,
233
+ primary,
234
+ secondary,
235
+ planType: (typeof object.planType === "string" && object.planType.trim()) ||
236
+ (typeof object.plan_type === "string" && object.plan_type.trim()) ||
237
+ null,
238
+ };
239
+ }
240
+ function parseCodexRateLimitBuckets(result) {
241
+ if (!result) {
242
+ return [];
243
+ }
244
+ const keyedBuckets = toObject(result.rateLimitsByLimitId);
245
+ if (keyedBuckets) {
246
+ const buckets = Object.entries(keyedBuckets)
247
+ .map(([limitId, bucket]) => parseCodexRateLimitBucket(limitId, bucket))
248
+ .filter((bucket) => bucket !== null);
249
+ if (buckets.length > 0) {
250
+ return buckets;
251
+ }
252
+ }
253
+ const directBucket = parseCodexRateLimitBucket("codex", result.rateLimits);
254
+ return directBucket ? [directBucket] : [];
255
+ }
256
+ function buildCodexWindow(bucket, kind, payload) {
257
+ const usedPercent = Math.max(0, Math.min(100, Math.round(payload.usedPercent ?? 0)));
258
+ const duration = payload.windowDurationMins ?? null;
259
+ const bucketLabel = formatBucketLabel(bucket.limitId, bucket.limitName);
260
+ const durationLabel = formatDurationLabel(duration);
261
+ return {
262
+ id: `${bucket.limitId ?? bucketLabel}:${kind}`,
263
+ label: `${bucketLabel} ${durationLabel}`,
264
+ windowLabel: kind === "secondary"
265
+ ? `${durationLabel} long-window cap reported by Codex app-server.`
266
+ : `${durationLabel} short-window cap reported by Codex app-server.`,
267
+ unitLabel: "%",
268
+ limit: 100,
269
+ used: usedPercent,
270
+ remaining: 100 - usedPercent,
271
+ resetsAt: toIsoString(payload.resetsAt),
272
+ message: "Codex reports live usage as percentages, so Junction shows percent used and remaining rather than raw request counts.",
273
+ };
274
+ }
275
+ async function readJsonFile(filePath, sourceLabel) {
276
+ if (!existsSync(filePath)) {
277
+ return null;
278
+ }
279
+ try {
280
+ return JSON.parse(await readFile(filePath, "utf8"));
281
+ }
282
+ catch (error) {
283
+ const detail = error instanceof Error ? error.message : "Unknown read error";
284
+ throw new Error(`Failed to read ${sourceLabel}: ${detail}`);
285
+ }
286
+ }
287
+ function decodeJwtPayload(token) {
288
+ if (!token) {
289
+ return null;
290
+ }
291
+ const segments = token.split(".");
292
+ if (segments.length < 2 || !segments[1]) {
293
+ return null;
294
+ }
295
+ try {
296
+ const json = Buffer.from(segments[1], "base64url").toString("utf8");
297
+ return JSON.parse(json);
298
+ }
299
+ catch {
300
+ return null;
301
+ }
302
+ }
303
+ function parseClaudeAuthStatus(stdout) {
304
+ try {
305
+ return JSON.parse(stdout.trim());
306
+ }
307
+ catch {
308
+ return null;
309
+ }
310
+ }
311
+ function parseClaudeOAuthCredentials(value) {
312
+ const root = toObject(value);
313
+ const oauth = toObject(root?.claudeAiOauth);
314
+ const accessToken = typeof oauth?.accessToken === "string" ? oauth.accessToken.trim() : "";
315
+ if (!accessToken) {
316
+ return null;
317
+ }
318
+ return {
319
+ accessToken,
320
+ expiresAt: toNumber(oauth?.expiresAt),
321
+ rateLimitTier: typeof oauth?.rateLimitTier === "string" && oauth.rateLimitTier.trim()
322
+ ? oauth.rateLimitTier.trim()
323
+ : null,
324
+ subscriptionType: typeof oauth?.subscriptionType === "string" && oauth.subscriptionType.trim()
325
+ ? oauth.subscriptionType.trim()
326
+ : null,
327
+ };
328
+ }
329
+ function isClaudeOAuthCredentialExpired(credentials) {
330
+ if (credentials.expiresAt === null) {
331
+ return false;
332
+ }
333
+ return credentials.expiresAt <= Date.now();
334
+ }
335
+ async function resolveClaudeKeychainServiceName() {
336
+ if (process.platform !== "darwin") {
337
+ return null;
338
+ }
339
+ const account = process.env.USER?.trim();
340
+ if (!account) {
341
+ return null;
342
+ }
343
+ const legacyLookup = await runBufferedCommand("security", ["find-generic-password", "-a", account, "-s", CLAUDE_KEYCHAIN_SERVICE_NAME], {
344
+ env: process.env,
345
+ maxBufferBytes: RATE_LIMIT_COMMAND_MAX_BUFFER_BYTES,
346
+ timeoutMs: RATE_LIMIT_COMMAND_TIMEOUT_MS,
347
+ });
348
+ if (legacyLookup.status === 0) {
349
+ return CLAUDE_KEYCHAIN_SERVICE_NAME;
350
+ }
351
+ const dumpResult = await runBufferedCommand("security", ["dump-keychain"], {
352
+ env: process.env,
353
+ maxBufferBytes: KEYCHAIN_COMMAND_MAX_BUFFER_BYTES,
354
+ timeoutMs: RATE_LIMIT_COMMAND_TIMEOUT_MS,
355
+ });
356
+ if (dumpResult.status !== 0) {
357
+ return null;
358
+ }
359
+ const match = dumpResult.stdout.match(new RegExp(`"svce"<blob>="(${CLAUDE_KEYCHAIN_SERVICE_NAME.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}-[^"]+)"`));
360
+ return match?.[1] ?? null;
361
+ }
362
+ async function readClaudeOAuthCredentialsFromKeychain() {
363
+ const account = process.env.USER?.trim();
364
+ if (process.platform !== "darwin" || !account) {
365
+ return null;
366
+ }
367
+ const serviceName = await resolveClaudeKeychainServiceName();
368
+ if (!serviceName) {
369
+ return null;
370
+ }
371
+ const result = await runBufferedCommand("security", ["find-generic-password", "-a", account, "-w", "-s", serviceName], {
372
+ env: process.env,
373
+ maxBufferBytes: KEYCHAIN_COMMAND_MAX_BUFFER_BYTES,
374
+ timeoutMs: RATE_LIMIT_COMMAND_TIMEOUT_MS,
375
+ });
376
+ if (result.status !== 0 || !result.stdout.trim()) {
377
+ return null;
378
+ }
379
+ return parseClaudeOAuthCredentials(JSON.parse(result.stdout.trim()));
380
+ }
381
+ async function readClaudeOAuthCredentials(claudeConfigDir, options) {
382
+ const dotCredentialsPath = path.join(claudeConfigDir, ".credentials.json");
383
+ const dotCredentials = await readJsonFile(dotCredentialsPath, ".claude/.credentials.json");
384
+ const dotCredentialsParsed = parseClaudeOAuthCredentials(dotCredentials);
385
+ if (dotCredentialsParsed) {
386
+ return dotCredentialsParsed;
387
+ }
388
+ const credentialsPath = path.join(claudeConfigDir, "credentials.json");
389
+ const credentials = await readJsonFile(credentialsPath, ".claude/credentials.json");
390
+ const credentialsParsed = parseClaudeOAuthCredentials(credentials);
391
+ if (credentialsParsed) {
392
+ return credentialsParsed;
393
+ }
394
+ if (!options.allowKeychain) {
395
+ return null;
396
+ }
397
+ return await readClaudeOAuthCredentialsFromKeychain();
398
+ }
399
+ async function readClaudeSessionKey(homeDir, env) {
400
+ const envSessionKey = env.CLAUDE_SESSION_KEY?.trim();
401
+ if (envSessionKey) {
402
+ return envSessionKey;
403
+ }
404
+ const sessionKeyPath = path.join(resolveProviderHomeDir(homeDir, env), ".claude-session-key");
405
+ if (!existsSync(sessionKeyPath)) {
406
+ return null;
407
+ }
408
+ const sessionKey = (await readFile(sessionKeyPath, "utf8")).trim();
409
+ return sessionKey || null;
410
+ }
411
+ function parseClaudeOAuthUtilizationPercent(value) {
412
+ const raw = toNumber(value);
413
+ if (raw === null) {
414
+ return null;
415
+ }
416
+ if (raw >= 0 && raw <= 1) {
417
+ return Math.max(0, Math.min(100, raw * 100));
418
+ }
419
+ return Math.max(0, Math.min(100, raw));
420
+ }
421
+ async function readClaudeOAuthRateLimits(input) {
422
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
423
+ method: "POST",
424
+ headers: {
425
+ Authorization: `Bearer ${input.accessToken}`,
426
+ "anthropic-beta": "oauth-2025-04-20",
427
+ "anthropic-version": "2023-06-01",
428
+ "content-type": "application/json",
429
+ "user-agent": "claude-code/2.1.5",
430
+ },
431
+ body: JSON.stringify({
432
+ model: "claude-haiku-4-5-20251001",
433
+ max_tokens: 1,
434
+ messages: [{ role: "user", content: "hi" }],
435
+ }),
436
+ signal: AbortSignal.timeout(input.timeoutMs ?? RATE_LIMIT_READER_TIMEOUT_MS),
437
+ });
438
+ const snapshot = {
439
+ fiveHourUtilization: parseClaudeOAuthUtilizationPercent(response.headers.get("anthropic-ratelimit-unified-5h-utilization")),
440
+ fiveHourResetsAt: toIsoString(response.headers.get("anthropic-ratelimit-unified-5h-reset")),
441
+ sevenDayUtilization: parseClaudeOAuthUtilizationPercent(response.headers.get("anthropic-ratelimit-unified-7d-utilization")),
442
+ sevenDayResetsAt: toIsoString(response.headers.get("anthropic-ratelimit-unified-7d-reset")),
443
+ };
444
+ if (snapshot.fiveHourUtilization === null &&
445
+ snapshot.sevenDayUtilization === null &&
446
+ snapshot.fiveHourResetsAt === null &&
447
+ snapshot.sevenDayResetsAt === null) {
448
+ if (!response.ok) {
449
+ throw new Error(`Claude OAuth rate-limit request failed with status ${response.status}.`);
450
+ }
451
+ throw new Error("Claude OAuth response did not include live rate-limit headers.");
452
+ }
453
+ return snapshot;
454
+ }
455
+ function parseClaudeLiveRateLimitWindow(value) {
456
+ const object = toObject(value);
457
+ if (!object) {
458
+ return {
459
+ fiveHourResetsAt: null,
460
+ fiveHourUtilization: null,
461
+ };
462
+ }
463
+ return {
464
+ fiveHourUtilization: parseClaudeOAuthUtilizationPercent(typeof object.utilization === "string" || typeof object.utilization === "number"
465
+ ? String(object.utilization)
466
+ : typeof object.utilization_pct === "string" || typeof object.utilization_pct === "number"
467
+ ? String(object.utilization_pct)
468
+ : null),
469
+ fiveHourResetsAt: toIsoString(typeof object.resets_at === "string" || typeof object.resets_at === "number"
470
+ ? object.resets_at
471
+ : typeof object.reset_at === "string" || typeof object.reset_at === "number"
472
+ ? object.reset_at
473
+ : typeof object.resetsAt === "string" || typeof object.resetsAt === "number"
474
+ ? object.resetsAt
475
+ : null),
476
+ };
477
+ }
478
+ async function readClaudeWebRateLimits(input) {
479
+ const response = await fetch(`https://claude.ai/api/organizations/${input.organizationId}/usage`, {
480
+ method: "GET",
481
+ headers: {
482
+ Accept: "application/json",
483
+ Cookie: `sessionKey=${input.sessionKey}`,
484
+ },
485
+ signal: AbortSignal.timeout(input.timeoutMs ?? RATE_LIMIT_READER_TIMEOUT_MS),
486
+ });
487
+ if (!response.ok) {
488
+ throw new Error(`Claude web usage request failed with status ${response.status}.`);
489
+ }
490
+ const json = toObject(await response.json());
491
+ if (!json) {
492
+ throw new Error("Claude web usage response was not an object.");
493
+ }
494
+ const fiveHour = parseClaudeLiveRateLimitWindow(json.five_hour);
495
+ const sevenDay = parseClaudeLiveRateLimitWindow(json.seven_day);
496
+ const snapshot = {
497
+ fiveHourUtilization: fiveHour.fiveHourUtilization,
498
+ fiveHourResetsAt: fiveHour.fiveHourResetsAt,
499
+ sevenDayUtilization: sevenDay.fiveHourUtilization,
500
+ sevenDayResetsAt: sevenDay.fiveHourResetsAt,
501
+ };
502
+ if (snapshot.fiveHourUtilization === null &&
503
+ snapshot.sevenDayUtilization === null &&
504
+ snapshot.fiveHourResetsAt === null &&
505
+ snapshot.sevenDayResetsAt === null) {
506
+ throw new Error("Claude web usage endpoint did not include live rate-limit fields.");
507
+ }
508
+ return snapshot;
509
+ }
510
+ function getPacificDayKey(date) {
511
+ const formatter = new Intl.DateTimeFormat("en-CA", {
512
+ timeZone: "America/Los_Angeles",
513
+ year: "numeric",
514
+ month: "2-digit",
515
+ day: "2-digit",
516
+ });
517
+ return formatter.format(date);
518
+ }
519
+ function getNextPacificMidnightIso(now) {
520
+ const currentKey = getPacificDayKey(now);
521
+ let low = now.getTime();
522
+ let high = low + 36 * 60 * 60 * 1000;
523
+ while (getPacificDayKey(new Date(high)) === currentKey) {
524
+ high += 12 * 60 * 60 * 1000;
525
+ }
526
+ while (high - low > 1000) {
527
+ const midpoint = low + Math.floor((high - low) / 2);
528
+ if (getPacificDayKey(new Date(midpoint)) === currentKey) {
529
+ low = midpoint;
530
+ }
531
+ else {
532
+ high = midpoint;
533
+ }
534
+ }
535
+ return new Date(high).toISOString();
536
+ }
537
+ function resolveProviderHomeDir(fallbackHomeDir, env) {
538
+ const envHome = env.HOME?.trim();
539
+ if (envHome && envHome !== process.env.HOME) {
540
+ return envHome;
541
+ }
542
+ const envUserProfile = env.USERPROFILE?.trim();
543
+ if (envUserProfile && envUserProfile !== process.env.USERPROFILE) {
544
+ return envUserProfile;
545
+ }
546
+ return fallbackHomeDir;
547
+ }
548
+ function getClaudeEnvAuthVariable(env) {
549
+ if (env.CLAUDE_CODE_OAUTH_TOKEN) {
550
+ return "CLAUDE_CODE_OAUTH_TOKEN";
551
+ }
552
+ if (env.ANTHROPIC_API_KEY) {
553
+ return "ANTHROPIC_API_KEY";
554
+ }
555
+ return null;
556
+ }
557
+ function resolveClaudeConfigDir(fallbackHomeDir, env) {
558
+ return env.CLAUDE_CONFIG_DIR?.trim() || path.join(resolveProviderHomeDir(fallbackHomeDir, env), ".claude");
559
+ }
560
+ function buildUnavailableProvider(provider, refreshedAt, message, windows, sourceLabel = null) {
561
+ return {
562
+ provider,
563
+ status: "unavailable",
564
+ sourceLabel,
565
+ planLabel: null,
566
+ accountLabel: null,
567
+ message,
568
+ refreshedAt,
569
+ windows,
570
+ };
571
+ }
572
+ function buildErrorProvider(provider, refreshedAt, message, sourceLabel = null) {
573
+ return {
574
+ provider,
575
+ status: "error",
576
+ sourceLabel,
577
+ planLabel: null,
578
+ accountLabel: null,
579
+ message,
580
+ refreshedAt,
581
+ windows: [],
582
+ };
583
+ }
584
+ const CLAUDE_SESSION_WINDOW_MS = 5 * 60 * 60 * 1000;
585
+ function buildClaudeUnavailableWindows() {
586
+ return [
587
+ createUnavailableWindow({
588
+ id: "rolling-5h",
589
+ label: "5 hour token estimate",
590
+ windowLabel: "Estimated Claude Code usage over the last 5 hours based on local session telemetry.",
591
+ resetsAt: null,
592
+ message: "Junction could not derive a local Claude usage estimate yet.",
593
+ }),
594
+ createUnavailableWindow({
595
+ id: "rolling-7d",
596
+ label: "7 day window",
597
+ windowLabel: "Long-window Claude caps vary by plan and demand.",
598
+ resetsAt: null,
599
+ message: "Junction does not yet have a reliable local source for Claude's long-window remaining quota.",
600
+ }),
601
+ ];
602
+ }
603
+ function buildClaudeLiveWindow(input) {
604
+ if (input.utilizationPercent === null) {
605
+ return createUnavailableWindow({
606
+ id: input.id,
607
+ label: input.label,
608
+ windowLabel: input.windowLabel,
609
+ resetsAt: input.resetsAt,
610
+ message: "Anthropic reported reset timing, but did not include a live utilization percentage for this window.",
611
+ });
612
+ }
613
+ const usedPercent = Math.max(0, Math.min(100, input.utilizationPercent));
614
+ return {
615
+ id: input.id,
616
+ label: input.label,
617
+ windowLabel: input.windowLabel,
618
+ unitLabel: "%",
619
+ limit: 100,
620
+ used: usedPercent,
621
+ remaining: Math.max(0, 100 - usedPercent),
622
+ resetsAt: input.resetsAt,
623
+ message: "Live utilization reported by Anthropic OAuth rate-limit headers. Claude exposes percentages here, not token counts.",
624
+ };
625
+ }
626
+ function buildClaudeLiveWindows(snapshot) {
627
+ return [
628
+ buildClaudeLiveWindow({
629
+ id: "rolling-5h",
630
+ label: "5 hour window",
631
+ windowLabel: "Rolling short-window Claude usage reported by Anthropic OAuth headers.",
632
+ resetsAt: snapshot.fiveHourResetsAt,
633
+ utilizationPercent: snapshot.fiveHourUtilization,
634
+ }),
635
+ buildClaudeLiveWindow({
636
+ id: "rolling-7d",
637
+ label: "7 day window",
638
+ windowLabel: "Rolling long-window Claude usage reported by Anthropic OAuth headers.",
639
+ resetsAt: snapshot.sevenDayResetsAt,
640
+ utilizationPercent: snapshot.sevenDayUtilization,
641
+ }),
642
+ ];
643
+ }
644
+ function parseClaudePlanTokenLimit(subscriptionType) {
645
+ const normalized = subscriptionType?.trim().toLowerCase() ?? "";
646
+ if (!normalized) {
647
+ return null;
648
+ }
649
+ if (normalized.includes("max20")) {
650
+ return 220000;
651
+ }
652
+ if (normalized.includes("max5") || normalized === "max") {
653
+ return 88000;
654
+ }
655
+ if (normalized.includes("pro")) {
656
+ return 19000;
657
+ }
658
+ return null;
659
+ }
660
+ function safeParseIsoTimestamp(value) {
661
+ if (typeof value !== "string" || !value.trim()) {
662
+ return null;
663
+ }
664
+ const parsed = new Date(value);
665
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
666
+ }
667
+ function extractClaudeUsageTokens(entry) {
668
+ const message = toObject(entry.message);
669
+ const usage = toObject(message?.usage);
670
+ const inputTokens = toNumber(usage?.input_tokens) ?? 0;
671
+ const outputTokens = toNumber(usage?.output_tokens) ?? 0;
672
+ return Math.max(0, Math.round(inputTokens + outputTokens));
673
+ }
674
+ function extractClaudeResetSignal(entry, now) {
675
+ const observedAt = safeParseIsoTimestamp(entry.timestamp);
676
+ const rootContent = typeof entry.content === "string" ? entry.content : null;
677
+ if (rootContent && observedAt) {
678
+ const waitMatch = rootContent.toLowerCase().match(/wait\s+(\d+)\s+minutes?/);
679
+ if (waitMatch?.[1]) {
680
+ const waitMinutes = Number(waitMatch[1]);
681
+ if (Number.isFinite(waitMinutes) && waitMinutes > 0) {
682
+ const resetAt = new Date(observedAt.getTime() + waitMinutes * 60 * 1000);
683
+ if (resetAt.getTime() > now.getTime()) {
684
+ return resetAt.toISOString();
685
+ }
686
+ }
687
+ }
688
+ }
689
+ const message = toObject(entry.message);
690
+ const content = Array.isArray(message?.content) ? message.content : [];
691
+ for (const item of content) {
692
+ const block = toObject(item);
693
+ const toolResultText = typeof block?.content === "string" ? block.content : null;
694
+ if (!toolResultText) {
695
+ continue;
696
+ }
697
+ const resetMatch = toolResultText.match(/limit reached\|(\d+)/i);
698
+ if (!resetMatch?.[1]) {
699
+ continue;
700
+ }
701
+ const rawTimestamp = Number(resetMatch[1]);
702
+ if (!Number.isFinite(rawTimestamp)) {
703
+ continue;
704
+ }
705
+ const resetAt = rawTimestamp > 10000000000 ? rawTimestamp : rawTimestamp * 1000;
706
+ const parsed = new Date(resetAt);
707
+ if (!Number.isNaN(parsed.getTime()) && parsed.getTime() > now.getTime()) {
708
+ return parsed.toISOString();
709
+ }
710
+ }
711
+ return null;
712
+ }
713
+ async function collectRecentClaudeUsage(projectsDir, now) {
714
+ const windowStartMs = now.getTime() - CLAUDE_SESSION_WINDOW_MS;
715
+ const eligibleFileMtimeFloor = windowStartMs - 60 * 60 * 1000;
716
+ const pendingDirs = [projectsDir];
717
+ let usedTokens = 0;
718
+ let latestEntryAtMs = null;
719
+ let limitResetAtMs = null;
720
+ let entryCount = 0;
721
+ while (pendingDirs.length > 0) {
722
+ const currentDir = pendingDirs.pop();
723
+ if (!currentDir) {
724
+ continue;
725
+ }
726
+ let entries;
727
+ try {
728
+ entries = await readdir(currentDir, { withFileTypes: true });
729
+ }
730
+ catch {
731
+ continue;
732
+ }
733
+ for (const entry of entries) {
734
+ const fullPath = path.join(currentDir, entry.name);
735
+ if (entry.isDirectory()) {
736
+ pendingDirs.push(fullPath);
737
+ continue;
738
+ }
739
+ if (!entry.isFile() || !entry.name.endsWith(".jsonl")) {
740
+ continue;
741
+ }
742
+ let stats;
743
+ try {
744
+ stats = await stat(fullPath);
745
+ }
746
+ catch {
747
+ continue;
748
+ }
749
+ if (stats.mtimeMs < eligibleFileMtimeFloor) {
750
+ continue;
751
+ }
752
+ let content;
753
+ try {
754
+ content = await readFile(fullPath, "utf8");
755
+ }
756
+ catch {
757
+ continue;
758
+ }
759
+ for (const line of content.split(/\r?\n/)) {
760
+ const trimmed = line.trim();
761
+ if (!trimmed) {
762
+ continue;
763
+ }
764
+ let parsed;
765
+ try {
766
+ parsed = JSON.parse(trimmed);
767
+ }
768
+ catch {
769
+ continue;
770
+ }
771
+ const record = toObject(parsed);
772
+ if (!record) {
773
+ continue;
774
+ }
775
+ const timestamp = safeParseIsoTimestamp(record.timestamp);
776
+ if (!timestamp) {
777
+ continue;
778
+ }
779
+ const resetSignal = extractClaudeResetSignal(record, now);
780
+ if (resetSignal) {
781
+ const resetMs = new Date(resetSignal).getTime();
782
+ if (!Number.isNaN(resetMs)) {
783
+ limitResetAtMs =
784
+ limitResetAtMs === null ? resetMs : Math.min(limitResetAtMs, resetMs);
785
+ }
786
+ }
787
+ if (timestamp.getTime() < windowStartMs) {
788
+ continue;
789
+ }
790
+ if (record.type !== "assistant") {
791
+ continue;
792
+ }
793
+ const tokens = extractClaudeUsageTokens(record);
794
+ if (tokens <= 0) {
795
+ continue;
796
+ }
797
+ usedTokens += tokens;
798
+ entryCount += 1;
799
+ latestEntryAtMs =
800
+ latestEntryAtMs === null ? timestamp.getTime() : Math.max(latestEntryAtMs, timestamp.getTime());
801
+ }
802
+ }
803
+ }
804
+ return {
805
+ usedTokens,
806
+ latestEntryAt: latestEntryAtMs === null ? null : new Date(latestEntryAtMs).toISOString(),
807
+ limitResetAt: limitResetAtMs === null ? null : new Date(limitResetAtMs).toISOString(),
808
+ entryCount,
809
+ };
810
+ }
811
+ function buildCodexWindows(subscriptionUntil) {
812
+ return [
813
+ createUnavailableWindow({
814
+ id: "rolling-5h",
815
+ label: "5 hour window",
816
+ windowLabel: "Rolling window for short-term Codex usage.",
817
+ resetsAt: null,
818
+ message: "Codex does not currently publish live remaining request counts through the local CLI.",
819
+ }),
820
+ createUnavailableWindow({
821
+ id: "rolling-7d",
822
+ label: "7 day window",
823
+ windowLabel: "Long-window cap associated with your ChatGPT-linked Codex access.",
824
+ resetsAt: null,
825
+ message: subscriptionUntil
826
+ ? `Weekly remaining usage is not exposed locally. ChatGPT subscription metadata is active until ${subscriptionUntil}.`
827
+ : "Weekly remaining usage is not exposed locally.",
828
+ }),
829
+ ];
830
+ }
831
+ function buildGeminiWindows(now) {
832
+ return [
833
+ createUnavailableWindow({
834
+ id: "daily",
835
+ label: "24 hour reset window",
836
+ windowLabel: "Daily Gemini reset timing. Google resets requests-per-day at midnight Pacific time.",
837
+ resetsAt: getNextPacificMidnightIso(now),
838
+ message: "Junction can show the next Gemini reset time, but Gemini does not expose live remaining usage locally.",
839
+ }),
840
+ ];
841
+ }
842
+ async function loadClaudeRateLimits(refreshedAt, homeDir, now, options) {
843
+ const commandPrefix = resolveProviderCommandPrefix(options.runtimeSettings?.command, () => "claude");
844
+ const claudeConfigDir = resolveClaudeConfigDir(homeDir, options.env);
845
+ const envAuthVariable = getClaudeEnvAuthVariable(options.env);
846
+ const result = await options.commandRunner(commandPrefix.command, [...commandPrefix.args, "auth", "status"], {
847
+ env: options.env,
848
+ });
849
+ const parsed = result.status === 0 ? parseClaudeAuthStatus(result.stdout ?? "") : null;
850
+ const sessionKey = parsed?.orgId && parsed.authMethod === "claude.ai"
851
+ ? await readClaudeSessionKey(homeDir, options.env)
852
+ : null;
853
+ let oauthCredentials = null;
854
+ try {
855
+ oauthCredentials = await readClaudeOAuthCredentials(claudeConfigDir, {
856
+ allowKeychain: !envAuthVariable,
857
+ });
858
+ }
859
+ catch (error) {
860
+ if (!envAuthVariable && !parsed?.loggedIn) {
861
+ throw error;
862
+ }
863
+ }
864
+ if (parsed?.orgId && sessionKey) {
865
+ try {
866
+ const liveSnapshot = await readClaudeWebRateLimits({
867
+ organizationId: parsed.orgId,
868
+ sessionKey,
869
+ timeoutMs: options.timeoutMs,
870
+ });
871
+ return {
872
+ provider: "claude",
873
+ status: "available",
874
+ sourceLabel: CLAUDE_WEB_SOURCE_LABEL,
875
+ planLabel: titleCase(parsed.subscriptionType) ?? "Claude account",
876
+ accountLabel: parsed.email ?? "Signed in",
877
+ message: "Live usage windows loaded from Claude's web usage endpoint.",
878
+ refreshedAt,
879
+ windows: buildClaudeLiveWindows(liveSnapshot),
880
+ };
881
+ }
882
+ catch {
883
+ // Fall back to Claude OAuth headers or local telemetry.
884
+ }
885
+ }
886
+ if (options.allowClaudeOAuthProbe &&
887
+ oauthCredentials &&
888
+ !isClaudeOAuthCredentialExpired(oauthCredentials)) {
889
+ try {
890
+ const liveSnapshot = await options.claudeRateLimitReader({
891
+ accessToken: oauthCredentials.accessToken,
892
+ timeoutMs: options.timeoutMs,
893
+ });
894
+ return {
895
+ provider: "claude",
896
+ status: "available",
897
+ sourceLabel: CLAUDE_OAUTH_SOURCE_LABEL,
898
+ planLabel: titleCase(parsed?.subscriptionType) ??
899
+ titleCase(oauthCredentials.subscriptionType) ??
900
+ "Claude account",
901
+ accountLabel: parsed?.email ??
902
+ (envAuthVariable ? `Configured via ${envAuthVariable}` : "Signed in via Claude Code"),
903
+ message: "Live usage windows loaded from Claude OAuth rate-limit headers.",
904
+ refreshedAt,
905
+ windows: buildClaudeLiveWindows(liveSnapshot),
906
+ };
907
+ }
908
+ catch {
909
+ // Fall back to local telemetry when live OAuth headers are unavailable.
910
+ }
911
+ }
912
+ if (result.status !== 0) {
913
+ if (envAuthVariable) {
914
+ const usageSnapshot = await collectRecentClaudeUsage(path.join(claudeConfigDir, "projects"), now);
915
+ const rollingResetAt = usageSnapshot.limitResetAt ??
916
+ (usageSnapshot.latestEntryAt
917
+ ? new Date(new Date(usageSnapshot.latestEntryAt).getTime() + CLAUDE_SESSION_WINDOW_MS).toISOString()
918
+ : null);
919
+ return {
920
+ provider: "claude",
921
+ status: "available",
922
+ sourceLabel: CLAUDE_ENV_SOURCE_LABEL,
923
+ planLabel: "Environment auth",
924
+ accountLabel: `Configured via ${envAuthVariable}`,
925
+ message: "Claude auth was detected from the daemon environment. Plan details are unavailable until `claude auth status` succeeds.",
926
+ refreshedAt,
927
+ windows: [
928
+ {
929
+ id: "rolling-5h",
930
+ label: "5 hour token estimate",
931
+ windowLabel: "Estimated Claude Code usage over the last 5 hours based on local session telemetry.",
932
+ unitLabel: null,
933
+ limit: null,
934
+ used: usageSnapshot.usedTokens,
935
+ remaining: null,
936
+ resetsAt: rollingResetAt,
937
+ message: "Anthropic does not expose an official local remaining counter. Junction is showing recent usage only.",
938
+ },
939
+ createUnavailableWindow({
940
+ id: "rolling-7d",
941
+ label: "7 day window",
942
+ windowLabel: "Long-window Claude caps vary by plan and demand.",
943
+ resetsAt: null,
944
+ message: "Junction does not yet have a reliable local source for Claude's long-window remaining quota.",
945
+ }),
946
+ ],
947
+ };
948
+ }
949
+ const message = result.stderr?.trim() || result.stdout?.trim() || "Claude auth status is unavailable.";
950
+ return buildUnavailableProvider("claude", refreshedAt, message, buildClaudeUnavailableWindows(), "claude auth status");
951
+ }
952
+ if (!parsed) {
953
+ return buildUnavailableProvider("claude", refreshedAt, "Claude auth status returned an unreadable response.", buildClaudeUnavailableWindows(), "claude auth status");
954
+ }
955
+ if (!parsed.loggedIn) {
956
+ return buildUnavailableProvider("claude", refreshedAt, "Claude is not logged in on this daemon.", buildClaudeUnavailableWindows(), "claude auth status");
957
+ }
958
+ const estimatedLimit = parseClaudePlanTokenLimit(parsed.subscriptionType ?? null);
959
+ const usageSnapshot = await collectRecentClaudeUsage(path.join(claudeConfigDir, "projects"), now);
960
+ const rollingResetAt = usageSnapshot.limitResetAt ??
961
+ (usageSnapshot.latestEntryAt
962
+ ? new Date(new Date(usageSnapshot.latestEntryAt).getTime() + CLAUDE_SESSION_WINDOW_MS).toISOString()
963
+ : null);
964
+ const rollingWindow = {
965
+ id: "rolling-5h",
966
+ label: "5 hour token estimate",
967
+ windowLabel: "Estimated Claude Code usage over the last 5 hours based on local session telemetry.",
968
+ unitLabel: null,
969
+ limit: estimatedLimit,
970
+ used: usageSnapshot.usedTokens,
971
+ remaining: estimatedLimit === null
972
+ ? null
973
+ : Math.max(0, estimatedLimit - usageSnapshot.usedTokens),
974
+ resetsAt: rollingResetAt,
975
+ message: estimatedLimit === null
976
+ ? "Anthropic does not expose an official local remaining counter. Junction is showing recent usage only."
977
+ : "Estimated from Claude Code JSONL usage plus plan-default token ceilings; Anthropic does not expose an official local remaining counter.",
978
+ };
979
+ return {
980
+ provider: "claude",
981
+ status: "available",
982
+ sourceLabel: "claude auth status + local session telemetry",
983
+ planLabel: titleCase(parsed.subscriptionType) ?? "Unknown",
984
+ accountLabel: parsed.email ?? "Signed in",
985
+ message: usageSnapshot.entryCount > 0
986
+ ? "Local Claude usage estimate loaded from recent session JSONL data."
987
+ : "Claude is signed in. No recent local session usage was detected in the last 5 hours.",
988
+ refreshedAt,
989
+ windows: [
990
+ rollingWindow,
991
+ createUnavailableWindow({
992
+ id: "rolling-7d",
993
+ label: "7 day window",
994
+ windowLabel: "Long-window Claude caps vary by plan and demand.",
995
+ resetsAt: null,
996
+ message: "Junction does not yet have a reliable local source for Claude's long-window remaining quota.",
997
+ }),
998
+ ],
999
+ };
1000
+ }
1001
+ async function loadCodexRateLimits(refreshedAt, homeDir, options) {
1002
+ const providerHomeDir = resolveProviderHomeDir(homeDir, options.env);
1003
+ const authPath = path.join(providerHomeDir, ".codex", "auth.json");
1004
+ const parsed = await readJsonFile(authPath, CODEX_AUTH_SOURCE_LABEL);
1005
+ const payload = decodeJwtPayload(parsed?.tokens?.id_token) ?? decodeJwtPayload(parsed?.tokens?.access_token);
1006
+ const planType = payload?.["https://api.openai.com/auth"]?.chatgpt_plan_type ?? null;
1007
+ const subscriptionUntil = payload?.["https://api.openai.com/auth"]?.chatgpt_subscription_active_until ?? null;
1008
+ const accountEmail = payload?.["https://api.openai.com/profile"]?.email ?? payload?.email ?? null;
1009
+ const commandPrefix = resolveProviderCommandPrefix(options.runtimeSettings?.command, () => "codex");
1010
+ try {
1011
+ const liveRateLimits = await options.codexRateLimitReader({
1012
+ runtimeSettings: options.runtimeSettings,
1013
+ timeoutMs: options.timeoutMs,
1014
+ });
1015
+ const buckets = parseCodexRateLimitBuckets(liveRateLimits);
1016
+ const windows = buckets.flatMap((bucket) => {
1017
+ const nextWindows = [];
1018
+ if (bucket.primary) {
1019
+ nextWindows.push(buildCodexWindow(bucket, "primary", bucket.primary));
1020
+ }
1021
+ if (bucket.secondary) {
1022
+ nextWindows.push(buildCodexWindow(bucket, "secondary", bucket.secondary));
1023
+ }
1024
+ return nextWindows;
1025
+ });
1026
+ if (windows.length > 0) {
1027
+ const livePlanType = buckets.find((bucket) => typeof bucket.planType === "string" && bucket.planType.trim())?.planType ??
1028
+ planType;
1029
+ const includesNamedBuckets = buckets.some((bucket) => {
1030
+ const bucketLabel = formatBucketLabel(bucket.limitId, bucket.limitName);
1031
+ return bucketLabel !== "Codex";
1032
+ });
1033
+ return {
1034
+ provider: "codex",
1035
+ status: "available",
1036
+ sourceLabel: "codex app-server",
1037
+ planLabel: titleCase(livePlanType) ?? titleCase(planType) ?? "ChatGPT account",
1038
+ accountLabel: accountEmail ?? "Signed in",
1039
+ message: includesNamedBuckets
1040
+ ? "Live usage windows loaded from Codex, including model-specific buckets when available."
1041
+ : "Live usage windows loaded from Codex.",
1042
+ refreshedAt,
1043
+ windows,
1044
+ };
1045
+ }
1046
+ }
1047
+ catch {
1048
+ // Fall back to auth metadata when live rate-limit polling is unavailable.
1049
+ }
1050
+ const result = await options.commandRunner(commandPrefix.command, [...commandPrefix.args, "login", "status"], {
1051
+ env: options.env,
1052
+ });
1053
+ if (result.status !== 0) {
1054
+ const message = result.stderr?.trim() || result.stdout?.trim() || "Codex login status is unavailable.";
1055
+ return buildUnavailableProvider("codex", refreshedAt, message, buildCodexWindows(null), CODEX_AUTH_SOURCE_LABEL);
1056
+ }
1057
+ return {
1058
+ provider: "codex",
1059
+ status: "available",
1060
+ sourceLabel: CODEX_AUTH_SOURCE_LABEL,
1061
+ planLabel: titleCase(planType) ?? "ChatGPT account",
1062
+ accountLabel: accountEmail ?? "Signed in",
1063
+ message: "Live ChatGPT account metadata is available. Codex does not expose exact remaining request counts locally.",
1064
+ refreshedAt,
1065
+ windows: buildCodexWindows(subscriptionUntil),
1066
+ };
1067
+ }
1068
+ function getGeminiEnvAuthVariable(env) {
1069
+ if (env.GEMINI_API_KEY) {
1070
+ return "GEMINI_API_KEY";
1071
+ }
1072
+ if (env.GOOGLE_API_KEY) {
1073
+ return "GOOGLE_API_KEY";
1074
+ }
1075
+ if (env.GOOGLE_GENERATIVE_AI_API_KEY) {
1076
+ return "GOOGLE_GENERATIVE_AI_API_KEY";
1077
+ }
1078
+ return null;
1079
+ }
1080
+ async function loadGeminiRateLimits(refreshedAt, homeDir, now, options) {
1081
+ const providerHomeDir = resolveProviderHomeDir(homeDir, options.env ?? process.env);
1082
+ const envAuthVariable = getGeminiEnvAuthVariable(options.env ?? process.env);
1083
+ if (envAuthVariable) {
1084
+ return {
1085
+ provider: "gemini",
1086
+ status: "available",
1087
+ sourceLabel: GEMINI_ENV_SOURCE_LABEL,
1088
+ planLabel: "API key",
1089
+ accountLabel: `Configured via ${envAuthVariable}`,
1090
+ message: "Gemini auth was detected from the daemon environment. Junction can show reset timing, but Gemini does not expose live usage or remaining quota for API-key auth.",
1091
+ refreshedAt,
1092
+ windows: buildGeminiWindows(now),
1093
+ };
1094
+ }
1095
+ const googleAccountsPath = path.join(providerHomeDir, ".gemini", "google_accounts.json");
1096
+ const settingsPath = path.join(providerHomeDir, ".gemini", "settings.json");
1097
+ const accounts = await readJsonFile(googleAccountsPath, GEMINI_ACCOUNTS_SOURCE_LABEL);
1098
+ const settings = await readJsonFile(settingsPath, GEMINI_SETTINGS_SOURCE_LABEL);
1099
+ const activeAccount = accounts?.active?.trim() ?? "";
1100
+ const authType = settings?.security?.auth?.selectedType ?? settings?.selectedAuthType ?? null;
1101
+ if (!activeAccount) {
1102
+ return buildUnavailableProvider("gemini", refreshedAt, "Gemini is not logged in on this daemon.", buildGeminiWindows(now), GEMINI_ACCOUNTS_SOURCE_LABEL);
1103
+ }
1104
+ return {
1105
+ provider: "gemini",
1106
+ status: "available",
1107
+ sourceLabel: GEMINI_ACCOUNTS_SOURCE_LABEL,
1108
+ planLabel: titleCase(authType) ?? "Google account",
1109
+ accountLabel: activeAccount,
1110
+ message: "Live account status is available. Junction can show the next Gemini reset, but Gemini does not expose live usage or remaining quota locally.",
1111
+ refreshedAt,
1112
+ windows: buildGeminiWindows(now),
1113
+ };
1114
+ }
1115
+ /**
1116
+ * Loads daemon-scoped rate-limit snapshots for Claude, Codex, and Gemini.
1117
+ *
1118
+ * Each provider is probed independently so a failure in one provider degrades
1119
+ * only that provider's snapshot instead of aborting the whole response.
1120
+ */
1121
+ export async function loadDaemonProviderRateLimits(options = {}) {
1122
+ const homeDir = options.homeDir ?? homedir();
1123
+ const junctionHome = options.junctionHome ?? homeDir;
1124
+ const rootDir = path.parse(homeDir).root || (options.platform === "win32" ? "C:\\" : "/");
1125
+ const refreshedAt = (options.now ?? new Date()).toISOString();
1126
+ const persistedConfig = loadPersistedConfig(junctionHome);
1127
+ const claudeRuntimeSettings = persistedConfig.agents?.providers?.claude;
1128
+ const codexRuntimeSettings = persistedConfig.agents?.providers?.codex;
1129
+ const geminiRuntimeSettings = persistedConfig.agents?.providers?.gemini;
1130
+ const env = options.env ?? process.env;
1131
+ const now = options.now ?? new Date();
1132
+ const commandRunner = options.commandRunner ?? defaultCommandRunner;
1133
+ const claudeRateLimitReader = options.claudeRateLimitReader ?? readClaudeOAuthRateLimits;
1134
+ const codexRateLimitReader = options.codexRateLimitReader ?? readCodexAppServerRateLimits;
1135
+ const timeoutMs = options.timeoutMs ?? RATE_LIMIT_READER_TIMEOUT_MS;
1136
+ const allowClaudeOAuthProbe = options.allowClaudeOAuthProbe ?? false;
1137
+ const providers = {
1138
+ claude: buildErrorProvider("claude", refreshedAt, "Not loaded yet."),
1139
+ codex: buildErrorProvider("codex", refreshedAt, "Not loaded yet."),
1140
+ gemini: buildErrorProvider("gemini", refreshedAt, "Not loaded yet."),
1141
+ };
1142
+ const claudePromise = loadClaudeRateLimits(refreshedAt, homeDir, now, {
1143
+ env: applyProviderEnv(env, claudeRuntimeSettings),
1144
+ claudeRateLimitReader,
1145
+ commandRunner,
1146
+ timeoutMs,
1147
+ allowClaudeOAuthProbe,
1148
+ runtimeSettings: claudeRuntimeSettings,
1149
+ }).catch((error) => buildErrorProvider("claude", refreshedAt, error instanceof Error ? error.message : String(error), "claude auth status"));
1150
+ const codexPromise = loadCodexRateLimits(refreshedAt, homeDir, {
1151
+ env: applyProviderEnv(env, codexRuntimeSettings),
1152
+ commandRunner,
1153
+ codexRateLimitReader,
1154
+ timeoutMs,
1155
+ runtimeSettings: codexRuntimeSettings,
1156
+ }).catch((error) => buildErrorProvider("codex", refreshedAt, error instanceof Error ? error.message : String(error), CODEX_AUTH_SOURCE_LABEL));
1157
+ const geminiPromise = loadGeminiRateLimits(refreshedAt, homeDir, now, {
1158
+ env: applyProviderEnv(env, geminiRuntimeSettings),
1159
+ }).catch((error) => buildErrorProvider("gemini", refreshedAt, error instanceof Error ? error.message : String(error), GEMINI_ACCOUNTS_SOURCE_LABEL));
1160
+ const [claude, codex, gemini] = await Promise.all([
1161
+ claudePromise,
1162
+ codexPromise,
1163
+ geminiPromise,
1164
+ ]);
1165
+ providers.claude = claude;
1166
+ providers.codex = codex;
1167
+ providers.gemini = gemini;
1168
+ return {
1169
+ homeDir,
1170
+ rootDir,
1171
+ providers,
1172
+ };
1173
+ }
1174
+ //# sourceMappingURL=daemon-provider-rate-limits.js.map