@kunkun.sh/kuota 0.0.0-stage → 0.1.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.
package/dist/bin.mjs ADDED
@@ -0,0 +1,1303 @@
1
+ #!/usr/bin/env node
2
+ import { a as makeObservation, c as windowProfile, i as historyEventsFromSnapshot, n as calculatePace, o as makeUnavailableSnapshot, r as decodeHistoryEvent, s as normalizeWindow, t as buildForecastV1 } from "./dist-CIjerpSd.mjs";
3
+ import { Effect, Either, Option } from "effect";
4
+ import { spawn } from "node:child_process";
5
+ import { mkdir, mkdtemp, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
6
+ import { homedir, tmpdir } from "node:os";
7
+ import { delimiter, join } from "node:path";
8
+ import { accessSync, constants } from "node:fs";
9
+ import { Command, Options } from "@effect/cli";
10
+ import * as NodeContext from "@effect/platform-node/NodeContext";
11
+ import { randomUUID } from "node:crypto";
12
+ // ../history/dist/index.mjs
13
+ const FILE = /^events-(\d{4}-\d{2}-\d{2})(?:\.(\d+))?\.jsonl$/;
14
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
15
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
+ const utcDate = (iso) => new Date(iso).toISOString().slice(0, 10);
17
+ async function listParts(directory, date) {
18
+ let names;
19
+ try {
20
+ names = await readdir(directory);
21
+ } catch {
22
+ return [];
23
+ }
24
+ const parts = [];
25
+ for (const name of names) {
26
+ const match = FILE.exec(name);
27
+ if (!match || !DATE.test(match[1])) continue;
28
+ if (date !== void 0 && match[1] !== date) continue;
29
+ parts.push({
30
+ name,
31
+ date: match[1],
32
+ part: match[2] === void 0 ? 0 : Number(match[2])
33
+ });
34
+ }
35
+ return parts.sort((a, b) => a.date.localeCompare(b.date) || a.part - b.part);
36
+ }
37
+ function partName(date, part) {
38
+ return part === 0 ? `events-${date}.jsonl` : `events-${date}.${part}.jsonl`;
39
+ }
40
+ async function parseFile(path) {
41
+ let text;
42
+ try {
43
+ text = await readFile(path, "utf8");
44
+ } catch {
45
+ return {
46
+ events: [],
47
+ invalid: 0
48
+ };
49
+ }
50
+ const events = [];
51
+ let invalid = 0;
52
+ for (const line of text.split("\n")) {
53
+ if (line.trim() === "") continue;
54
+ try {
55
+ const decoded = decodeHistoryEvent(JSON.parse(line));
56
+ if (Either.isRight(decoded)) events.push(decoded.right);
57
+ else invalid++;
58
+ } catch {
59
+ invalid++;
60
+ }
61
+ }
62
+ return {
63
+ events,
64
+ invalid
65
+ };
66
+ }
67
+ function createLocalJsonlJournal(options) {
68
+ const directory = options.directory;
69
+ const maxFileBytes = options.maxFileBytes ?? 16777216;
70
+ const retentionDays = options.retentionDays ?? 400;
71
+ const doSync = options.fsync ?? true;
72
+ const lockStaleMs = options.lockStaleMs ?? 3e4;
73
+ const lockTimeoutMs = options.lockTimeoutMs ?? 15e3;
74
+ const lockPath = join(directory, ".lock");
75
+ let queue = Promise.resolve();
76
+ async function withLock(fn) {
77
+ await mkdir(directory, {
78
+ recursive: true,
79
+ mode: 448
80
+ });
81
+ const deadline = Date.now() + lockTimeoutMs;
82
+ for (;;) try {
83
+ const handle = await open(lockPath, "wx", 384);
84
+ await handle.writeFile(String(process.pid));
85
+ await handle.close();
86
+ break;
87
+ } catch (error) {
88
+ if (error.code !== "EEXIST") throw error;
89
+ try {
90
+ const info = await stat(lockPath);
91
+ if (Date.now() - info.mtimeMs > lockStaleMs) {
92
+ await rm(lockPath, { force: true });
93
+ continue;
94
+ }
95
+ } catch {
96
+ continue;
97
+ }
98
+ if (Date.now() > deadline) throw new Error("JOURNAL_LOCK_TIMEOUT");
99
+ await sleep(5);
100
+ }
101
+ try {
102
+ return await fn();
103
+ } finally {
104
+ await rm(lockPath, { force: true });
105
+ }
106
+ }
107
+ async function appendDate(date, events) {
108
+ const parts = await listParts(directory, date);
109
+ const known = /* @__PURE__ */ new Set();
110
+ for (const part of parts) for (const event of (await parseFile(join(directory, part.name))).events) known.add(event.eventId);
111
+ const fresh = [];
112
+ let duplicates = 0;
113
+ for (const event of events) if (known.has(event.eventId)) duplicates++;
114
+ else {
115
+ known.add(event.eventId);
116
+ fresh.push(event);
117
+ }
118
+ if (fresh.length === 0) return {
119
+ written: 0,
120
+ duplicates
121
+ };
122
+ let partNumber = parts.length === 0 ? 0 : parts[parts.length - 1].part;
123
+ let path = join(directory, partName(date, partNumber));
124
+ let size = 0;
125
+ let endsWithNewline = true;
126
+ try {
127
+ size = (await stat(path)).size;
128
+ if (size > 0) {
129
+ const handle = await open(path, "r");
130
+ const last = Buffer.alloc(1);
131
+ await handle.read(last, 0, 1, size - 1);
132
+ await handle.close();
133
+ endsWithNewline = last[0] === 10;
134
+ }
135
+ } catch {
136
+ size = 0;
137
+ }
138
+ const buckets = [[]];
139
+ let current = size;
140
+ for (const event of fresh) {
141
+ const line = JSON.stringify(event) + "\n";
142
+ if (current > 0 && current + line.length > maxFileBytes) {
143
+ buckets.push([]);
144
+ current = 0;
145
+ }
146
+ buckets[buckets.length - 1].push(line);
147
+ current += line.length;
148
+ }
149
+ for (const [index, lines] of buckets.entries()) {
150
+ if (index > 0) {
151
+ partNumber += 1;
152
+ path = join(directory, partName(date, partNumber));
153
+ endsWithNewline = true;
154
+ }
155
+ const payload = (endsWithNewline ? "" : "\n") + lines.join("");
156
+ const handle = await open(path, "a", 384);
157
+ try {
158
+ await handle.write(payload);
159
+ if (doSync) await handle.sync();
160
+ } finally {
161
+ await handle.close();
162
+ }
163
+ }
164
+ return {
165
+ written: fresh.length,
166
+ duplicates
167
+ };
168
+ }
169
+ async function readAll(opts) {
170
+ const fromMs = Date.parse(opts.from);
171
+ const toMs = Date.parse(opts.to);
172
+ if (!Number.isFinite(fromMs) || !Number.isFinite(toMs)) throw new Error("INVALID_RANGE");
173
+ const fromDate = utcDate(opts.from);
174
+ const toDate = utcDate(opts.to);
175
+ const events = [];
176
+ let invalidLines = 0;
177
+ for (const part of await listParts(directory)) {
178
+ if (part.date < fromDate || part.date > toDate) continue;
179
+ const parsed = await parseFile(join(directory, part.name));
180
+ invalidLines += parsed.invalid;
181
+ for (const event of parsed.events) {
182
+ const t = Date.parse(event.observedAt);
183
+ if (t < fromMs || t > toMs) continue;
184
+ if (opts.provider !== void 0 && event.provider !== opts.provider) continue;
185
+ if (opts.windowId !== void 0 && event.windowId !== opts.windowId) continue;
186
+ events.push(event);
187
+ }
188
+ }
189
+ return {
190
+ events,
191
+ invalidLines
192
+ };
193
+ }
194
+ return {
195
+ append(events) {
196
+ for (const event of events) if (Either.isLeft(decodeHistoryEvent(event))) return Promise.reject(/* @__PURE__ */ new Error("INVALID_HISTORY_EVENT"));
197
+ const run = queue.then(() => withLock(async () => {
198
+ const byDate = /* @__PURE__ */ new Map();
199
+ for (const event of events) {
200
+ const date = utcDate(event.observedAt);
201
+ byDate.set(date, [...byDate.get(date) ?? [], event]);
202
+ }
203
+ let written = 0;
204
+ let duplicates = 0;
205
+ for (const [date, list] of [...byDate].sort(([a], [b]) => a.localeCompare(b))) {
206
+ const result = await appendDate(date, list);
207
+ written += result.written;
208
+ duplicates += result.duplicates;
209
+ }
210
+ return {
211
+ written,
212
+ duplicates
213
+ };
214
+ }));
215
+ queue = run.catch(() => void 0);
216
+ return run;
217
+ },
218
+ readAll,
219
+ async *read(opts) {
220
+ for (const event of (await readAll(opts)).events) yield event;
221
+ },
222
+ async applyRetention(now = /* @__PURE__ */ new Date()) {
223
+ const cutoffMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) - (Math.max(1, retentionDays) - 1) * 864e5;
224
+ const cutoff = new Date(cutoffMs).toISOString().slice(0, 10);
225
+ return queue.then(() => withLock(async () => {
226
+ let removed = 0;
227
+ for (const part of await listParts(directory)) if (part.date < cutoff) {
228
+ await rm(join(directory, part.name), { force: true });
229
+ removed++;
230
+ }
231
+ return removed;
232
+ }));
233
+ }
234
+ };
235
+ }
236
+ function defaultHistoryDirectory(env = process.env, platform = process.platform) {
237
+ if (env.KUOTA_HISTORY_DIR) return env.KUOTA_HISTORY_DIR;
238
+ if (platform === "darwin") return join(homedir(), "Library", "Application Support", "kuota", "history");
239
+ return join(env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "kuota", "history");
240
+ }
241
+ // ../providers/dist/index.mjs
242
+ function asRecord(value) {
243
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
244
+ }
245
+ /** Parses only the non-sensitive subset of the Codex app-server v2 quota response. */
246
+ function parseCodexRateLimits(raw, fetchedAt = /* @__PURE__ */ new Date()) {
247
+ const response = asRecord(raw);
248
+ const limits = asRecord(response?.rateLimits);
249
+ const common = {
250
+ provider: "codex",
251
+ source: "codex-app-server",
252
+ fetchedAt: fetchedAt.toISOString()
253
+ };
254
+ if (!limits) return {
255
+ ...common,
256
+ status: "unavailable",
257
+ windows: [],
258
+ errorCode: "NO_RATE_LIMITS"
259
+ };
260
+ let malformed = false;
261
+ const windows = [];
262
+ for (const windowId of ["primary", "secondary"]) {
263
+ const window = asRecord(limits[windowId]);
264
+ if (!window) continue;
265
+ const usedPercent = window.usedPercent;
266
+ if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent) || usedPercent < 0 || usedPercent > 100) malformed = true;
267
+ const reset = window.resetsAt;
268
+ const resetAt = typeof reset === "number" && Number.isFinite(reset) && reset >= 0 ? (/* @__PURE__ */ new Date(reset * 1e3)).toISOString() : null;
269
+ windows.push(normalizeWindow({
270
+ windowId,
271
+ usedPercent,
272
+ resetAt
273
+ }));
274
+ }
275
+ const blocked = response?.ordinaryUsageAllowed === false;
276
+ return {
277
+ ...common,
278
+ status: windows.length === 0 ? "unavailable" : malformed || blocked ? "partial" : "ok",
279
+ windows,
280
+ errorCode: windows.length === 0 ? "NO_WINDOWS" : blocked ? "ORDINARY_USAGE_BLOCKED" : malformed ? "INVALID_WINDOW" : null
281
+ };
282
+ }
283
+ /** Fixed, safe error messages: never forward stderr or a raw RPC response. */
284
+ var CodexTransportError = class extends Error {
285
+ code;
286
+ constructor(code) {
287
+ super({
288
+ BINARY_NOT_FOUND: "Codex executable was not found",
289
+ TIMEOUT: "Codex app-server did not respond in time",
290
+ PROTOCOL_ERROR: "Codex app-server protocol failed"
291
+ }[code]);
292
+ this.code = code;
293
+ this.name = "CodexTransportError";
294
+ }
295
+ };
296
+ /** One bounded RPC session. Never spawns a bare Codex TUI and never prints raw output. */
297
+ function requestCodexRateLimits(options = {}) {
298
+ const binary = options.binary ?? "codex";
299
+ const args = options.args ?? ["app-server"];
300
+ const timeoutMs = Math.max(100, Math.min(options.timeoutMs ?? 15e3, 6e4));
301
+ return new Promise((resolve, reject) => {
302
+ const child = spawn(binary, [...args], {
303
+ stdio: [
304
+ "pipe",
305
+ "pipe",
306
+ "pipe"
307
+ ],
308
+ shell: false
309
+ });
310
+ let settled = false;
311
+ let phase = "initializing";
312
+ let buffer = "";
313
+ const finish = (error, result) => {
314
+ if (settled) return;
315
+ settled = true;
316
+ clearTimeout(timeout);
317
+ child.stdin.end();
318
+ if (!child.killed && child.exitCode === null) child.kill("SIGTERM");
319
+ if (error) reject(error);
320
+ else resolve(result);
321
+ };
322
+ const timeout = setTimeout(() => finish(new CodexTransportError("TIMEOUT")), timeoutMs);
323
+ const send = (message) => {
324
+ if (settled) return;
325
+ try {
326
+ child.stdin.write(`${JSON.stringify(message)}\n`);
327
+ } catch {
328
+ finish(new CodexTransportError("PROTOCOL_ERROR"));
329
+ }
330
+ };
331
+ child.once("error", (error) => {
332
+ finish(new CodexTransportError(error.code === "ENOENT" ? "BINARY_NOT_FOUND" : "PROTOCOL_ERROR"));
333
+ });
334
+ child.once("exit", () => finish(new CodexTransportError("PROTOCOL_ERROR")));
335
+ child.stdin.on("error", () => finish(new CodexTransportError("PROTOCOL_ERROR")));
336
+ child.stderr.resume();
337
+ const onMessage = (value) => {
338
+ const msg = value !== null && typeof value === "object" ? value : null;
339
+ if (!msg || typeof msg.id !== "number") return;
340
+ if (phase === "initializing" && msg.id === 1) {
341
+ if (msg.error || !msg.result) return finish(new CodexTransportError("PROTOCOL_ERROR"));
342
+ phase = "reading";
343
+ send({ method: "initialized" });
344
+ send({
345
+ id: 2,
346
+ method: "account/rateLimits/read"
347
+ });
348
+ } else if (phase === "reading" && msg.id === 2) {
349
+ if (msg.error || !msg.result) return finish(new CodexTransportError("PROTOCOL_ERROR"));
350
+ finish(void 0, msg.result);
351
+ }
352
+ };
353
+ child.stdout.on("data", (chunk) => {
354
+ if (settled) return;
355
+ buffer += chunk.toString("utf8");
356
+ if (buffer.length > 2e6) return finish(new CodexTransportError("PROTOCOL_ERROR"));
357
+ while (!settled) {
358
+ const index = buffer.indexOf("\n");
359
+ if (index < 0) break;
360
+ const line = buffer.slice(0, index).trim();
361
+ buffer = buffer.slice(index + 1);
362
+ if (!line) continue;
363
+ try {
364
+ onMessage(JSON.parse(line));
365
+ } catch {
366
+ finish(new CodexTransportError("PROTOCOL_ERROR"));
367
+ }
368
+ }
369
+ });
370
+ send({
371
+ id: 1,
372
+ method: "initialize",
373
+ params: { clientInfo: {
374
+ name: "kuota",
375
+ version: "0.1.0"
376
+ } }
377
+ });
378
+ });
379
+ }
380
+ function readCodex(options = {}) {
381
+ return Effect.promise(async () => {
382
+ try {
383
+ return parseCodexRateLimits(await requestCodexRateLimits(options));
384
+ } catch (error) {
385
+ const code = error instanceof CodexTransportError ? error.code : "UNKNOWN";
386
+ return makeUnavailableSnapshot("codex", code, code === "BINARY_NOT_FOUND" ? "unsupported" : "unavailable");
387
+ }
388
+ });
389
+ }
390
+ function record$1(value) {
391
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
392
+ }
393
+ function slug$1(value) {
394
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "quota";
395
+ }
396
+ function groupId(value) {
397
+ if (/^gemini models?$/i.test(value)) return "gemini";
398
+ if (/^claude and gpt(?: models?)?$/i.test(value)) return "claude-gpt";
399
+ return slug$1(value);
400
+ }
401
+ function validFraction(value) {
402
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
403
+ }
404
+ /** Parses agy >=1.1.11's structured `-p /usage --output-format json` response. */
405
+ function parseAntigravityUsageReport(raw) {
406
+ const data = record$1(raw);
407
+ const command = record$1(data?.command);
408
+ const payload = record$1(command?.data);
409
+ if (data?.status !== "SUCCESS" || command?.name !== "usage" || !Array.isArray(payload?.groups)) throw new Error("INVALID_USAGE_REPORT");
410
+ const windows = [];
411
+ const seen = /* @__PURE__ */ new Set();
412
+ for (const group of payload.groups) {
413
+ const info = record$1(group);
414
+ if (!info || !Array.isArray(info.buckets)) continue;
415
+ const family = groupId(typeof info.name === "string" ? info.name : typeof info.display_name === "string" ? info.display_name : "quota");
416
+ for (const bucket of info.buckets) {
417
+ const value = record$1(bucket);
418
+ if (!value || value.disabled === true) continue;
419
+ const nested = record$1(value.remaining);
420
+ const remaining = value.remaining_fraction ?? nested?.remaining_fraction ?? (nested?.case === "remainingFraction" ? nested.value : void 0);
421
+ if (!validFraction(remaining)) continue;
422
+ const name = `${family}.${typeof value.window === "string" ? slug$1(value.window) : typeof value.id === "string" ? slug$1(value.id) : "quota"}`;
423
+ const windowId = seen.has(name) ? `${name}:${slug$1(String(value.id ?? value.bucket_id ?? "additional"))}` : name;
424
+ seen.add(windowId);
425
+ windows.push(normalizeWindow({
426
+ windowId,
427
+ usedPercent: Number(((1 - remaining) * 100).toFixed(2)),
428
+ resetAt: value.reset_time ?? value.resetTime
429
+ }));
430
+ }
431
+ }
432
+ if (windows.length === 0) throw new Error("NO_MEASURED_QUOTA");
433
+ return {
434
+ provider: "antigravity",
435
+ status: "ok",
436
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
437
+ source: "agy-cli-print",
438
+ windows,
439
+ errorCode: null
440
+ };
441
+ }
442
+ /** Version strings with suffixes are intentionally rejected to avoid guessing support. */
443
+ function parseAgyVersion(value) {
444
+ const match = /^(\d+)\.(\d+)\.(\d+)\s*$/.exec(value.trim());
445
+ if (!match) return false;
446
+ const major = Number(match[1]);
447
+ const minor = Number(match[2]);
448
+ const patch = Number(match[3]);
449
+ return major > 1 || major === 1 && (minor > 1 || minor === 1 && patch >= 11);
450
+ }
451
+ var AgyProbeError = class extends Error {
452
+ code;
453
+ constructor(code) {
454
+ super(code);
455
+ this.code = code;
456
+ }
457
+ };
458
+ /** Bound subprocess output and lifetime; never print raw stdout/stderr or OAuth URLs. */
459
+ function execute(binary, args, options) {
460
+ return new Promise((resolve, reject) => {
461
+ const child = spawn(binary, args, {
462
+ cwd: options.cwd,
463
+ env: options.env,
464
+ detached: process.platform !== "win32",
465
+ windowsHide: true,
466
+ stdio: [
467
+ "ignore",
468
+ "pipe",
469
+ "pipe"
470
+ ]
471
+ });
472
+ const chunks = [];
473
+ let bytes = 0;
474
+ let stderr = "";
475
+ let settled = false;
476
+ const stop = () => {
477
+ if (child.pid && process.platform !== "win32") try {
478
+ process.kill(-child.pid, "SIGTERM");
479
+ } catch {}
480
+ else try {
481
+ child.kill("SIGTERM");
482
+ } catch {}
483
+ };
484
+ const finish = (error) => {
485
+ if (settled) return;
486
+ settled = true;
487
+ clearTimeout(timer);
488
+ stop();
489
+ if (error) reject(error);
490
+ else resolve(Buffer.concat(chunks).toString("utf8"));
491
+ };
492
+ const timer = setTimeout(() => finish(new AgyProbeError("TIMEOUT")), options.timeoutMs);
493
+ child.stdout?.on("data", (chunk) => {
494
+ bytes += chunk.length;
495
+ if (bytes > options.maxOutputBytes) finish(new AgyProbeError("OUTPUT_TOO_LARGE"));
496
+ else if (!settled) chunks.push(chunk);
497
+ });
498
+ child.stderr?.on("data", (chunk) => {
499
+ if (stderr.length < 4096) stderr += chunk.toString("utf8").slice(0, 4096 - stderr.length);
500
+ });
501
+ child.once("error", (error) => finish(new AgyProbeError(error.code === "ENOENT" ? "BINARY_NOT_FOUND" : "CLI_FAILED")));
502
+ child.once("close", (code) => {
503
+ if (code === 0) finish();
504
+ else if (/not logged in|authentication required|sign.?in|please visit/i.test(stderr)) finish(new AgyProbeError("AUTH_REQUIRED"));
505
+ else finish(new AgyProbeError("CLI_FAILED"));
506
+ });
507
+ });
508
+ }
509
+ /** No new account login. Existing agy credentials only, guarded from browser launch and auto-update. */
510
+ function readAntigravity(options = {}) {
511
+ return Effect.promise(async () => {
512
+ const directory = await mkdtemp(join(tmpdir(), "kuota-agy-"));
513
+ try {
514
+ const script = "#!/bin/sh\nexit 1\n";
515
+ for (const name of [
516
+ "open",
517
+ "xdg-open",
518
+ "browser"
519
+ ]) await writeFile(join(directory, name), script, { mode: 448 });
520
+ const env = { ...process.env };
521
+ delete env.ANTIGRAVITY_OAUTH_CREDENTIALS_JSON;
522
+ env.AGY_CLI_DISABLE_AUTO_UPDATE = "true";
523
+ env.PATH = `${directory}${delimiter}${process.env.PATH ?? ""}`;
524
+ env.BROWSER = join(directory, "browser");
525
+ const binary = options.binary ?? "agy";
526
+ const context = {
527
+ cwd: directory,
528
+ env
529
+ };
530
+ if (!parseAgyVersion(await execute(binary, ["--version"], {
531
+ ...context,
532
+ timeoutMs: 3e3,
533
+ maxOutputBytes: 8192
534
+ }))) throw new AgyProbeError("UNSUPPORTED_VERSION");
535
+ const text = await execute(binary, [
536
+ "-p",
537
+ "/usage",
538
+ "--output-format",
539
+ "json",
540
+ "--print-timeout",
541
+ "90s"
542
+ ], {
543
+ ...context,
544
+ timeoutMs: options.timeoutMs ?? 2e4,
545
+ maxOutputBytes: options.maxOutputBytes ?? 1048576
546
+ });
547
+ try {
548
+ return parseAntigravityUsageReport(JSON.parse(text));
549
+ } catch {
550
+ throw new AgyProbeError("PROTOCOL_ERROR");
551
+ }
552
+ } catch (error) {
553
+ const code = error instanceof AgyProbeError ? error.code : "CLI_FAILED";
554
+ return makeUnavailableSnapshot("antigravity", code, code === "AUTH_REQUIRED" ? "unauthenticated" : "unavailable");
555
+ } finally {
556
+ await rm(directory, {
557
+ recursive: true,
558
+ force: true
559
+ });
560
+ }
561
+ });
562
+ }
563
+ function record(value) {
564
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
565
+ }
566
+ function validPercent(value) {
567
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100;
568
+ }
569
+ function slug(value) {
570
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
571
+ }
572
+ /** Only a Claude subscription OAuth token works; MCP-only and API keys are not quota tokens. */
573
+ function extractClaudeOAuthToken(raw, now = Date.now()) {
574
+ const oauth = record(record(raw)?.claudeAiOauth);
575
+ const token = oauth?.accessToken;
576
+ const expiresAt = oauth?.expiresAt;
577
+ if (typeof token !== "string" || !token.trim() || !validExpiry(expiresAt, now)) return null;
578
+ return token.trim();
579
+ }
580
+ function validExpiry(value, now) {
581
+ return typeof value === "number" && Number.isFinite(value) && value > now;
582
+ }
583
+ /** Claude API reports utilization (used percent), not remaining percent. */
584
+ function parseClaudeOAuthUsage(raw) {
585
+ const payload = record(raw);
586
+ if (!payload) throw new Error("INVALID_CLAUDE_USAGE");
587
+ const windows = [];
588
+ const add = (windowId, value) => {
589
+ const data = record(value);
590
+ if (!validPercent(data?.utilization)) return;
591
+ windows.push(normalizeWindow({
592
+ windowId,
593
+ usedPercent: data.utilization,
594
+ resetAt: data.resets_at
595
+ }));
596
+ };
597
+ add("session.5h", payload.five_hour);
598
+ if (validPercent(record(payload.seven_day)?.utilization)) add("weekly.all", payload.seven_day);
599
+ else add("weekly.oauth-apps", payload.seven_day_oauth_apps);
600
+ add("weekly.sonnet", payload.seven_day_sonnet);
601
+ add("weekly.opus", payload.seven_day_opus);
602
+ for (const key of [
603
+ "seven_day_routines",
604
+ "seven_day_claude_routines",
605
+ "claude_routines",
606
+ "routines",
607
+ "seven_day_cowork",
608
+ "cowork"
609
+ ]) if (validPercent(record(payload[key])?.utilization)) {
610
+ add("weekly.routines", payload[key]);
611
+ break;
612
+ }
613
+ if (Array.isArray(payload.limits)) {
614
+ const seen = /* @__PURE__ */ new Set();
615
+ for (const entry of payload.limits) {
616
+ const limit = record(entry);
617
+ const model = record(record(limit?.scope)?.model);
618
+ if (limit?.kind !== "weekly_scoped" || !validPercent(limit.percent)) continue;
619
+ const id = slug(typeof model?.display_name === "string" ? model.display_name : typeof model?.id === "string" ? model.id : "");
620
+ if (!id || id === "all-models") continue;
621
+ const windowId = `weekly.${id}`;
622
+ if (seen.has(windowId) || windows.some((w) => w.windowId === windowId)) continue;
623
+ seen.add(windowId);
624
+ windows.push(normalizeWindow({
625
+ windowId,
626
+ usedPercent: limit.percent,
627
+ resetAt: limit.resets_at
628
+ }));
629
+ }
630
+ }
631
+ return {
632
+ provider: "claude",
633
+ source: "claude-oauth-usage",
634
+ status: windows.length ? "ok" : "unavailable",
635
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
636
+ windows,
637
+ errorCode: windows.length ? null : "NO_MEASURED_QUOTA"
638
+ };
639
+ }
640
+ const ENDPOINT = "https://api.anthropic.com/api/oauth/usage";
641
+ const MAX_BODY = 1048576;
642
+ /** Read at most 1 MiB, without placing raw HTTP error responses in logs or JSON output. */
643
+ async function readBoundedResponse(response) {
644
+ if (!response.body) return "";
645
+ const reader = response.body.getReader();
646
+ const buffers = [];
647
+ let total = 0;
648
+ try {
649
+ while (true) {
650
+ const { done, value } = await reader.read();
651
+ if (done) break;
652
+ total += value.byteLength;
653
+ if (total > MAX_BODY) throw new Error("RESPONSE_TOO_LARGE");
654
+ buffers.push(value);
655
+ }
656
+ const joined = new Uint8Array(total);
657
+ let offset = 0;
658
+ for (const buffer of buffers) {
659
+ joined.set(buffer, offset);
660
+ offset += buffer.byteLength;
661
+ }
662
+ return new TextDecoder().decode(joined);
663
+ } finally {
664
+ reader.releaseLock();
665
+ }
666
+ }
667
+ async function resolveToken(options) {
668
+ if (options.token) return options.token.trim() || null;
669
+ const fromEnvironment = process.env.CLAUDE_CODE_OAUTH_TOKEN;
670
+ if (fromEnvironment?.trim()) return fromEnvironment.trim();
671
+ try {
672
+ const file = options.credentialsFile ?? join(homedir(), ".claude", ".credentials.json");
673
+ return extractClaudeOAuthToken(JSON.parse(await readFile(file, "utf8")));
674
+ } catch {
675
+ return null;
676
+ }
677
+ }
678
+ /** No Keychain UI access, credential refresh, browser sign-in or CLI session side effects. */
679
+ function readClaude(options = {}) {
680
+ return Effect.promise(async () => {
681
+ const token = await resolveToken(options);
682
+ if (!token) return makeUnavailableSnapshot("claude", "AUTH_REQUIRED", "unauthenticated");
683
+ const controller = new AbortController();
684
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 12e3);
685
+ try {
686
+ const request = (url, agent) => (options.fetchImpl ?? fetch)(url, {
687
+ method: "GET",
688
+ redirect: "error",
689
+ cache: "no-store",
690
+ headers: {
691
+ Authorization: `Bearer ${token}`,
692
+ Accept: "application/json",
693
+ "Content-Type": "application/json",
694
+ "anthropic-beta": "oauth-2025-04-20",
695
+ "User-Agent": agent
696
+ },
697
+ signal: controller.signal
698
+ });
699
+ let response = await request(`${ENDPOINT}?cedar_ember=1`, "claude-cli/2.1.0 (external, cli)");
700
+ if (response.status === 400 || response.status === 403) response = await request(ENDPOINT, "claude-code/2.1.0");
701
+ if (response.status === 401) return makeUnavailableSnapshot("claude", "AUTH_REQUIRED", "unauthenticated");
702
+ if (response.status === 429) return makeUnavailableSnapshot("claude", "RATE_LIMITED");
703
+ if (response.status !== 200) return makeUnavailableSnapshot("claude", "HTTP_ERROR");
704
+ const content = await readBoundedResponse(response);
705
+ try {
706
+ return parseClaudeOAuthUsage(JSON.parse(content));
707
+ } catch {
708
+ return makeUnavailableSnapshot("claude", "PROTOCOL_ERROR");
709
+ }
710
+ } catch (error) {
711
+ return makeUnavailableSnapshot("claude", controller.signal.aborted ? "TIMEOUT" : error instanceof Error && error.message === "RESPONSE_TOO_LARGE" ? "OUTPUT_TOO_LARGE" : "NETWORK_ERROR");
712
+ } finally {
713
+ clearTimeout(timer);
714
+ }
715
+ });
716
+ }
717
+ const PROVIDER_METADATA = [
718
+ {
719
+ id: "codex",
720
+ label: "OpenAI Codex",
721
+ implemented: true,
722
+ source: "codex-app-server"
723
+ },
724
+ {
725
+ id: "claude",
726
+ label: "Claude Code",
727
+ implemented: true,
728
+ source: "claude-oauth-usage"
729
+ },
730
+ {
731
+ id: "antigravity",
732
+ label: "Google Antigravity",
733
+ implemented: true,
734
+ source: "agy-cli-print"
735
+ }
736
+ ];
737
+ /** Static names are extensible to runtime manifests in the later plugin phase. */
738
+ function isProviderId(value) {
739
+ return PROVIDER_METADATA.some((provider) => provider.id === value);
740
+ }
741
+ function builtInProviders() {
742
+ return [
743
+ {
744
+ id: "codex",
745
+ label: "OpenAI Codex",
746
+ read: () => readCodex()
747
+ },
748
+ {
749
+ id: "claude",
750
+ label: "Claude Code",
751
+ read: () => readClaude()
752
+ },
753
+ {
754
+ id: "antigravity",
755
+ label: "Google Antigravity",
756
+ read: () => readAntigravity()
757
+ }
758
+ ];
759
+ }
760
+ /** One failed adapter never prevents other providers from reporting. */
761
+ function readAll(ids, providers = builtInProviders()) {
762
+ const byId = new Map(providers.map((provider) => [provider.id, provider]));
763
+ return Effect.all(ids.map((id) => {
764
+ const provider = byId.get(id);
765
+ if (!provider) return Effect.succeed(makeUnavailableSnapshot(id, "NOT_REGISTERED", "unsupported"));
766
+ return provider.read().pipe(Effect.catchAllCause(() => Effect.succeed(makeUnavailableSnapshot(id, "PROVIDER_FAILED"))));
767
+ }), { concurrency: 3 });
768
+ }
769
+ const COMMON = {
770
+ errorRetryMs: 6e4,
771
+ authRetryMs: 3e5,
772
+ rateLimitCooldownMs: 9e5,
773
+ staleMaxAgeMs: 864e5
774
+ };
775
+ const DEFAULT_POLICIES = {
776
+ codex: {
777
+ ...COMMON,
778
+ successTtlMs: 6e4
779
+ },
780
+ claude: {
781
+ ...COMMON,
782
+ successTtlMs: 9e5
783
+ },
784
+ antigravity: {
785
+ ...COMMON,
786
+ successTtlMs: 3e5
787
+ }
788
+ };
789
+ const emptySlot = (generation) => ({
790
+ lastGood: null,
791
+ lastFailure: null,
792
+ lastCheckedAtMs: 0,
793
+ nextAttemptAtMs: 0,
794
+ inflight: null,
795
+ generation
796
+ });
797
+ const isMeasurable = (s) => (s.status === "ok" || s.status === "partial") && s.windows.length > 0;
798
+ /**
799
+ * Process-local TTL cache with per-provider singleflight, failure cooldowns and honest
800
+ * stale labelling. Only normalized snapshots are retained.
801
+ */
802
+ function createQuotaService(options = {}) {
803
+ const now = options.now ?? Date.now;
804
+ const providers = new Map((options.providers ?? builtInProviders()).map((provider) => [provider.id, provider]));
805
+ const slots = /* @__PURE__ */ new Map();
806
+ const generations = /* @__PURE__ */ new Map();
807
+ const policyFor = (id) => ({
808
+ ...DEFAULT_POLICIES[id],
809
+ ...options.policies?.[id]
810
+ });
811
+ const slotFor = (id) => {
812
+ let slot = slots.get(id);
813
+ if (!slot) {
814
+ slot = emptySlot(generations.get(id) ?? 0);
815
+ slots.set(id, slot);
816
+ }
817
+ return slot;
818
+ };
819
+ const view = (id, freshProbe) => {
820
+ const slot = slots.get(id);
821
+ if (!slot) return null;
822
+ const t = now();
823
+ const policy = policyFor(id);
824
+ const good = slot.lastGood;
825
+ const goodMs = good ? Date.parse(good.fetchedAt) : NaN;
826
+ const goodUsable = good !== null && !(t - (Number.isFinite(goodMs) ? goodMs : slot.lastCheckedAtMs) > policy.staleMaxAgeMs);
827
+ const nextAttemptAtMs = slot.nextAttemptAtMs > t ? slot.nextAttemptAtMs : null;
828
+ if (good && goodUsable) {
829
+ const failed = slot.lastFailure;
830
+ return makeObservation({
831
+ snapshot: good,
832
+ freshness: failed ? "stale" : freshProbe ? "live" : "cached",
833
+ checkedAtMs: slot.lastCheckedAtMs,
834
+ nowMs: t,
835
+ nextAttemptAtMs,
836
+ latestErrorCode: failed?.errorCode ?? null
837
+ });
838
+ }
839
+ if (slot.lastFailure) return makeObservation({
840
+ snapshot: slot.lastFailure,
841
+ freshness: freshProbe ? "live" : "cached",
842
+ checkedAtMs: slot.lastCheckedAtMs,
843
+ nowMs: t,
844
+ nextAttemptAtMs,
845
+ latestErrorCode: slot.lastFailure.errorCode
846
+ });
847
+ return null;
848
+ };
849
+ const probe = (id, provider, slot) => {
850
+ const attemptAt = now();
851
+ const settled = Effect.runPromise(provider.read().pipe(Effect.catchAllCause(() => Effect.succeed({
852
+ ...makeUnavailableSnapshot(id, "PROVIDER_FAILED"),
853
+ fetchedAt: new Date(attemptAt).toISOString()
854
+ })))).catch(() => ({
855
+ ...makeUnavailableSnapshot(id, "PROVIDER_FAILED"),
856
+ fetchedAt: new Date(attemptAt).toISOString()
857
+ })).then((snapshot) => {
858
+ if (slots.get(id) !== slot || slot.generation !== (generations.get(id) ?? 0)) return;
859
+ const policy = policyFor(id);
860
+ const t = now();
861
+ slot.lastCheckedAtMs = attemptAt;
862
+ if (isMeasurable(snapshot)) {
863
+ slot.lastGood = snapshot;
864
+ slot.lastFailure = null;
865
+ slot.nextAttemptAtMs = 0;
866
+ return;
867
+ }
868
+ slot.lastFailure = snapshot;
869
+ if (snapshot.errorCode === "RATE_LIMITED") slot.nextAttemptAtMs = t + policy.rateLimitCooldownMs;
870
+ else if (snapshot.errorCode === "AUTH_REQUIRED" || snapshot.status === "unauthenticated") {
871
+ slot.lastGood = null;
872
+ slot.nextAttemptAtMs = t + policy.authRetryMs;
873
+ } else slot.nextAttemptAtMs = t + policy.errorRetryMs;
874
+ }).finally(() => {
875
+ if (slot.inflight === settled) slot.inflight = null;
876
+ });
877
+ slot.inflight = settled;
878
+ return settled;
879
+ };
880
+ const readOne = async (id, force) => {
881
+ const provider = providers.get(id);
882
+ if (!provider) {
883
+ const t = now();
884
+ return makeObservation({
885
+ snapshot: {
886
+ ...makeUnavailableSnapshot(id, "NOT_REGISTERED", "unsupported"),
887
+ fetchedAt: new Date(t).toISOString()
888
+ },
889
+ freshness: "live",
890
+ checkedAtMs: t,
891
+ nowMs: t,
892
+ latestErrorCode: "NOT_REGISTERED"
893
+ });
894
+ }
895
+ const slot = slotFor(id);
896
+ if (slot.inflight) {
897
+ await slot.inflight;
898
+ return view(id, true) ?? await readOne(id, force);
899
+ }
900
+ const t = now();
901
+ const coolingDown = slot.nextAttemptAtMs > t;
902
+ const goodMs = slot.lastGood ? Date.parse(slot.lastGood.fetchedAt) : NaN;
903
+ const fresh = slot.lastGood !== null && slot.lastFailure === null && Number.isFinite(goodMs) && t - goodMs < policyFor(id).successTtlMs;
904
+ if (coolingDown || fresh && !force) {
905
+ const cached = view(id, false);
906
+ if (cached) return cached;
907
+ }
908
+ await probe(id, provider, slot);
909
+ const result = view(id, true);
910
+ if (result) return result;
911
+ const t2 = now();
912
+ return makeObservation({
913
+ snapshot: makeUnavailableSnapshot(id, "INVALIDATED"),
914
+ freshness: "live",
915
+ checkedAtMs: t2,
916
+ nowMs: t2,
917
+ latestErrorCode: "INVALIDATED"
918
+ });
919
+ };
920
+ return {
921
+ read: (ids, opts) => Effect.promise(() => Promise.all(ids.map((id) => readOne(id, opts?.force === true)))),
922
+ peek: (id) => view(id, false),
923
+ invalidate: (id) => {
924
+ for (const target of id ? [id] : [
925
+ "codex",
926
+ "claude",
927
+ "antigravity"
928
+ ]) {
929
+ generations.set(target, (generations.get(target) ?? 0) + 1);
930
+ slots.delete(target);
931
+ }
932
+ }
933
+ };
934
+ }
935
+ // ../cli/src/history-commands.ts
936
+ async function loadDeviceId(directory) {
937
+ const path = join(directory, "device-id");
938
+ try {
939
+ const existing = (await readFile(path, "utf8")).trim();
940
+ if (/^[0-9a-f-]{36}$/.test(existing)) return existing;
941
+ } catch {}
942
+ await mkdir(directory, {
943
+ recursive: true,
944
+ mode: 448
945
+ });
946
+ const id = randomUUID();
947
+ await writeFile(path, id, { mode: 384 });
948
+ return id;
949
+ }
950
+ const DAY = 864e5;
951
+ function historyCommands(output, providers, runtime, setExit) {
952
+ const now = () => (runtime.now ?? (() => /* @__PURE__ */ new Date()))();
953
+ const directory = runtime.directory ?? defaultHistoryDirectory();
954
+ const journal = () => createLocalJsonlJournal({ directory });
955
+ const emit = (json, value, human) => output.writeStdout(json ? JSON.stringify({
956
+ schemaVersion: 1,
957
+ generatedAt: now().toISOString(),
958
+ ...value
959
+ }) : human());
960
+ const fail = (json, code, exit = 2) => {
961
+ setExit(exit);
962
+ if (json) emit(true, { errors: [{ code }] }, () => "");
963
+ else output.writeStderr(`Error: ${code}`);
964
+ };
965
+ const provSelector = (provider, json) => {
966
+ const value = Option.getOrUndefined(provider);
967
+ if (value === void 0) return null;
968
+ if (!isProviderId(value)) {
969
+ fail(json, "UNKNOWN_PROVIDER");
970
+ return "bad";
971
+ }
972
+ return value;
973
+ };
974
+ const parseDate = (value, fallback, json) => {
975
+ const raw = Option.getOrUndefined(value);
976
+ if (raw === void 0) return fallback;
977
+ const parsed = new Date(raw);
978
+ if (!Number.isFinite(parsed.getTime())) {
979
+ fail(json, "INVALID_DATE");
980
+ return null;
981
+ }
982
+ return parsed;
983
+ };
984
+ const provider = Options.optional(Options.text("provider"));
985
+ const windowOpt = Options.optional(Options.text("window"));
986
+ const json = Options.boolean("json");
987
+ const collect = Command.make("collect", {
988
+ once: Options.boolean("once"),
989
+ provider,
990
+ json
991
+ }, ({ once, provider: p, json: asJson }) => Effect.gen(function* () {
992
+ if (!once) return fail(asJson, "ONCE_REQUIRED");
993
+ const selected = provSelector(p, asJson);
994
+ if (selected === "bad") return;
995
+ const snapshots = yield* readAll(selected ? [selected] : PROVIDER_METADATA.map((m) => m.id), providers);
996
+ const store = journal();
997
+ const deviceId = runtime.deviceId ?? (yield* Effect.promise(() => loadDeviceId(directory)));
998
+ const recordedAt = now().toISOString();
999
+ const collected = [];
1000
+ const errors = [];
1001
+ for (const snapshot of snapshots) {
1002
+ const events = historyEventsFromSnapshot(snapshot, {
1003
+ deviceId,
1004
+ accountScope: null,
1005
+ recordedAt
1006
+ });
1007
+ try {
1008
+ const result = yield* Effect.promise(() => store.append(events));
1009
+ collected.push({
1010
+ provider: snapshot.provider,
1011
+ status: snapshot.status,
1012
+ ...result
1013
+ });
1014
+ } catch {
1015
+ errors.push({ code: "JOURNAL_WRITE_FAILED" });
1016
+ }
1017
+ }
1018
+ yield* Effect.promise(() => store.applyRetention(now())).pipe(Effect.catchAllDefect(() => Effect.void));
1019
+ if (errors.length > 0) setExit(1);
1020
+ emit(asJson, {
1021
+ collected,
1022
+ errors
1023
+ }, () => collected.map((c) => `${c.provider}: ${c.status}, ${c.written} written, ${c.duplicates} duplicate`).join("\n"));
1024
+ }));
1025
+ const history = Command.make("history", {
1026
+ provider,
1027
+ window: windowOpt,
1028
+ from: Options.optional(Options.text("from")),
1029
+ to: Options.optional(Options.text("to")),
1030
+ json
1031
+ }, ({ provider: p, window: w, from, to, json: asJson }) => Effect.gen(function* () {
1032
+ const selected = provSelector(p, asJson);
1033
+ if (selected === "bad") return;
1034
+ const end = parseDate(to, now(), asJson);
1035
+ const start = end && parseDate(from, /* @__PURE__ */ new Date(end.getTime() - 7 * DAY), asJson);
1036
+ if (!end || !start) return;
1037
+ const result = yield* Effect.promise(() => journal().readAll({
1038
+ from: start.toISOString(),
1039
+ to: end.toISOString(),
1040
+ ...selected ? { provider: selected } : {},
1041
+ ...Option.isSome(w) ? { windowId: w.value } : {}
1042
+ }));
1043
+ emit(asJson, {
1044
+ from: start.toISOString(),
1045
+ to: end.toISOString(),
1046
+ events: result.events,
1047
+ invalidLines: result.invalidLines
1048
+ }, () => `${result.events.length} events (${result.invalidLines} invalid lines skipped)`);
1049
+ }));
1050
+ const forecast = Command.make("forecast", {
1051
+ provider,
1052
+ window: windowOpt,
1053
+ mode: Options.withDefault(Options.text("mode"), "all-window"),
1054
+ json
1055
+ }, ({ provider: p, window: w, mode, json: asJson }) => Effect.gen(function* () {
1056
+ const selected = provSelector(p, asJson);
1057
+ if (selected === "bad") return;
1058
+ if (mode !== "all-window" && mode !== "recent-activity") return fail(asJson, "INVALID_MODE");
1059
+ const end = now();
1060
+ const result = yield* Effect.promise(() => journal().readAll({
1061
+ from: (/* @__PURE__ */ new Date(end.getTime() - 30 * DAY)).toISOString(),
1062
+ to: end.toISOString(),
1063
+ ...selected ? { provider: selected } : {},
1064
+ ...Option.isSome(w) ? { windowId: w.value } : {}
1065
+ }));
1066
+ const keys = /* @__PURE__ */ new Map();
1067
+ for (const e of result.events) if (e.kind === "quota_observed") keys.set(`${e.provider}\u0000${e.windowId}`, e);
1068
+ const forecasts = [...keys.values()].sort((a, b) => a.provider.localeCompare(b.provider) || a.windowId.localeCompare(b.windowId)).map((k) => buildForecastV1({
1069
+ now: end,
1070
+ provider: k.provider,
1071
+ windowId: k.windowId,
1072
+ observations: result.events,
1073
+ mode
1074
+ }));
1075
+ emit(asJson, {
1076
+ forecasts,
1077
+ invalidLines: result.invalidLines
1078
+ }, () => forecasts.map((f) => `${f.provider}/${f.windowId}: ${f.quality}${f.exhaustedAt ? `, exhausts ${f.exhaustedAt}` : ""}`).join("\n") || "no history");
1079
+ }));
1080
+ const pace = Command.make("pace", {
1081
+ provider,
1082
+ json
1083
+ }, ({ provider: p, json: asJson }) => Effect.gen(function* () {
1084
+ const selected = provSelector(p, asJson);
1085
+ if (selected === "bad") return;
1086
+ const snapshots = yield* readAll(selected ? [selected] : PROVIDER_METADATA.map((m) => m.id), providers);
1087
+ const at = now();
1088
+ const rows = snapshots.flatMap((snapshot) => snapshot.windows.map((window) => {
1089
+ const profile = windowProfile(snapshot.provider, window.windowId, window.resetAt, at);
1090
+ const reset = window.resetAt ? new Date(window.resetAt) : null;
1091
+ const start = reset && profile.durationMs !== null ? new Date(reset.getTime() - profile.durationMs) : null;
1092
+ const result = calculatePace({
1093
+ now: at,
1094
+ startAt: start,
1095
+ resetAt: reset,
1096
+ usedPercent: window.usedPercent,
1097
+ semantics: profile.semantics,
1098
+ startSource: start ? "inferred" : "unknown"
1099
+ });
1100
+ return {
1101
+ provider: snapshot.provider,
1102
+ windowId: window.windowId,
1103
+ semantics: profile.semantics,
1104
+ pace: result,
1105
+ reason: result ? null : "WINDOW_NOT_PROVEN_FIXED"
1106
+ };
1107
+ }));
1108
+ emit(asJson, { pace: rows }, () => rows.map((r) => `${r.provider}/${r.windowId}: ${r.pace ? `ideal ${r.pace.idealUsedPercent.toFixed(1)}% used, actual ${r.pace.actualUsedPercent}%` : r.reason}`).join("\n"));
1109
+ }));
1110
+ const inspectOpt = Options.boolean("inspect");
1111
+ const limitOpt = Options.withDefault(Options.integer("limit"), 20);
1112
+ return [
1113
+ collect,
1114
+ history,
1115
+ forecast,
1116
+ pace,
1117
+ Command.make("snapshot", {
1118
+ provider,
1119
+ inspect: inspectOpt,
1120
+ json
1121
+ }, ({ provider: p, inspect: asInspect, json: asJson }) => Effect.gen(function* () {
1122
+ const selected = provSelector(p, asJson);
1123
+ if (selected === "bad") return;
1124
+ const snapshots = yield* readAll(selected ? [selected] : PROVIDER_METADATA.map((m) => m.id), providers);
1125
+ if (asJson) {
1126
+ emit(true, {
1127
+ ...asInspect ? { inspect: true } : {},
1128
+ snapshots
1129
+ }, () => "");
1130
+ return;
1131
+ }
1132
+ const lines = [];
1133
+ for (const s of snapshots) {
1134
+ lines.push(`Provider: ${s.provider} [${s.status}]`);
1135
+ lines.push(` Source: ${s.source} · Fetched: ${s.fetchedAt}`);
1136
+ if (s.errorCode) lines.push(` Error Code: ${s.errorCode}`);
1137
+ for (const w of s.windows) {
1138
+ lines.push(` - Window: ${w.windowId} · ${w.usedPercent !== null ? `${w.usedPercent}% used` : "unknown"} (${w.remainingPercent !== null ? `${w.remainingPercent}% remaining` : "unknown"})${w.resetAt ? ` · resets ${w.resetAt}` : ""}`);
1139
+ if (asInspect) {
1140
+ const profile = windowProfile(s.provider, w.windowId, w.resetAt);
1141
+ lines.push(` [inspect] semantics: ${profile.semantics} | duration: ${profile.durationMs ? `${profile.durationMs / 36e5}h` : "unspecified"}`);
1142
+ }
1143
+ }
1144
+ }
1145
+ output.writeStdout(lines.join("\n"));
1146
+ })),
1147
+ Command.make("snapshots", {
1148
+ limit: limitOpt,
1149
+ provider,
1150
+ window: windowOpt,
1151
+ json
1152
+ }, ({ limit, provider: p, window: w, json: asJson }) => Effect.gen(function* () {
1153
+ const selected = provSelector(p, asJson);
1154
+ if (selected === "bad") return;
1155
+ const end = now();
1156
+ const sorted = [...(yield* Effect.promise(() => journal().readAll({
1157
+ from: (/* @__PURE__ */ new Date(end.getTime() - 30 * DAY)).toISOString(),
1158
+ to: end.toISOString(),
1159
+ ...selected ? { provider: selected } : {},
1160
+ ...Option.isSome(w) ? { windowId: w.value } : {}
1161
+ }))).events].sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime()).slice(0, Math.max(1, limit));
1162
+ if (asJson) {
1163
+ emit(true, {
1164
+ total: sorted.length,
1165
+ snapshots: sorted
1166
+ }, () => "");
1167
+ return;
1168
+ }
1169
+ if (sorted.length === 0) {
1170
+ output.writeStdout("No recorded snapshot events found.");
1171
+ return;
1172
+ }
1173
+ const lines = [
1174
+ "RECORDED AT (UTC) PROVIDER WINDOW USED % KIND",
1175
+ "-------------------------------------------------------------------------",
1176
+ ...sorted.map((e) => {
1177
+ return `${e.recordedAt.padEnd(26)} ${e.provider.padEnd(12)} ${("windowId" in e ? e.windowId : "-").padEnd(12)} ${("usedPercent" in e && e.usedPercent !== null ? `${e.usedPercent}%` : "-").padEnd(9)} ${e.kind}`;
1178
+ })
1179
+ ];
1180
+ output.writeStdout(lines.join("\n"));
1181
+ }))
1182
+ ];
1183
+ }
1184
+ // ../cli/src/commands.ts
1185
+ const processOutput = {
1186
+ writeStdout: (value) => process.stdout.write(`${value}\n`),
1187
+ writeStderr: (value) => process.stderr.write(`${value}\n`)
1188
+ };
1189
+ function envelope(providers, errors = []) {
1190
+ return {
1191
+ schemaVersion: 1,
1192
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1193
+ providers,
1194
+ errors
1195
+ };
1196
+ }
1197
+ function showHuman(snapshot) {
1198
+ if (snapshot.status !== "ok" && snapshot.status !== "partial") return `${snapshot.provider}: ${snapshot.status} (${snapshot.errorCode ?? "unknown"})`;
1199
+ const windows = snapshot.windows.map((window) => `${window.windowId}: ${window.remainingPercent === null ? "unknown" : `${window.remainingPercent}% remaining`}${window.resetAt ? ` · resets ${window.resetAt}` : ""}`);
1200
+ return `${snapshot.provider}: ${snapshot.status}\n ${windows.join("\n ") || "quota unknown"}`;
1201
+ }
1202
+ /** An executable presence check, not a logged-in/account-readiness claim. */
1203
+ function hasBinary(name) {
1204
+ for (const folder of (process.env.PATH ?? "").split(delimiter)) {
1205
+ if (!folder) continue;
1206
+ try {
1207
+ accessSync(join(folder, name), constants.X_OK);
1208
+ return true;
1209
+ } catch {}
1210
+ }
1211
+ return false;
1212
+ }
1213
+ async function runCli(args, output = processOutput, providers = builtInProviders(), startTui = async () => {
1214
+ return (await import("./main-B-hZbKK1.mjs")).startTui();
1215
+ }, runtime = {}) {
1216
+ let exitCode = 0;
1217
+ const status = Command.make("status", {
1218
+ provider: Options.optional(Options.text("provider")),
1219
+ json: Options.boolean("json")
1220
+ }, ({ provider, json }) => Effect.gen(function* () {
1221
+ const selector = Option.getOrUndefined(provider);
1222
+ if (selector !== void 0 && !isProviderId(selector)) {
1223
+ exitCode = 2;
1224
+ if (json) output.writeStdout(JSON.stringify(envelope([], [{ code: "UNKNOWN_PROVIDER" }])));
1225
+ else output.writeStderr("Unknown provider. Run `kuota providers` to see supported IDs.");
1226
+ return;
1227
+ }
1228
+ const snapshots = yield* readAll(selector ? [selector] : PROVIDER_METADATA.map((item) => item.id), providers);
1229
+ if (json) output.writeStdout(JSON.stringify(envelope(snapshots)));
1230
+ else output.writeStdout(snapshots.map(showHuman).join("\n"));
1231
+ }));
1232
+ const providerList = Command.make("providers", { json: Options.boolean("json") }, ({ json }) => Effect.sync(() => {
1233
+ if (json) output.writeStdout(JSON.stringify({
1234
+ schemaVersion: 1,
1235
+ providers: PROVIDER_METADATA
1236
+ }));
1237
+ else output.writeStdout(PROVIDER_METADATA.map((item) => `${item.id.padEnd(12)} ${item.label} ${item.implemented ? "implemented" : "planned"}`).join("\n"));
1238
+ }));
1239
+ const doctor = Command.make("doctor", { json: Options.boolean("json") }, ({ json }) => Effect.sync(() => {
1240
+ const tools = PROVIDER_METADATA.map((item) => ({
1241
+ provider: item.id,
1242
+ executable: item.id === "antigravity" ? "agy" : item.id,
1243
+ installed: hasBinary(item.id === "antigravity" ? "agy" : item.id),
1244
+ adapterImplemented: item.implemented
1245
+ }));
1246
+ if (json) output.writeStdout(JSON.stringify({
1247
+ schemaVersion: 1,
1248
+ tools
1249
+ }));
1250
+ else output.writeStdout(tools.map((tool) => `${tool.provider}: executable ${tool.installed ? "present" : "not found"}, adapter ${tool.adapterImplemented ? "ready" : "planned"}`).join("\n"));
1251
+ }));
1252
+ const tui = Command.make("tui", {}, () => Effect.promise(startTui));
1253
+ const makeWebCommand = (name) => Command.make(name, {
1254
+ port: Options.optional(Options.integer("port")),
1255
+ host: Options.optional(Options.text("host")),
1256
+ noOpen: Options.boolean("no-open")
1257
+ }, ({ port, host, noOpen }) => Effect.promise(async () => {
1258
+ const { startWebServer } = await import("./web-server-DkdA5Ixj.mjs");
1259
+ await startWebServer({
1260
+ port: Option.getOrUndefined(port),
1261
+ host: Option.getOrUndefined(host),
1262
+ open: !noOpen
1263
+ }, output, providers);
1264
+ }));
1265
+ const web = makeWebCommand("web");
1266
+ const serve = makeWebCommand("serve");
1267
+ const root = Command.make("kuota", {}, () => Effect.promise(async () => {
1268
+ if (process.stdout.isTTY) await startTui();
1269
+ else output.writeStdout("Kuota — Multi-provider AI Quota & Usage Monitor\nRun `kuota --help` for available commands, or `kuota web` to launch the web UI.");
1270
+ })).pipe(Command.withSubcommands([
1271
+ status,
1272
+ providerList,
1273
+ doctor,
1274
+ tui,
1275
+ web,
1276
+ serve,
1277
+ ...historyCommands(output, providers, runtime, (code) => {
1278
+ exitCode = code;
1279
+ })
1280
+ ]));
1281
+ const cli = Command.run(root, {
1282
+ name: "Kuota",
1283
+ version: "0.1.0"
1284
+ });
1285
+ try {
1286
+ await Effect.runPromise(cli([
1287
+ process.execPath,
1288
+ "kuota",
1289
+ ...args
1290
+ ]).pipe(Effect.provide(NodeContext.layer)));
1291
+ } catch (error) {
1292
+ exitCode = 1;
1293
+ if (args.includes("--json")) output.writeStdout(JSON.stringify(envelope([], [{ code: "CLI_ERROR" }])));
1294
+ else if (args[0] === "tui" && String(error).includes("TUI requires an interactive terminal")) output.writeStderr("Kuota TUI requires an interactive terminal; use `kuota status --json` in non-TTY contexts.");
1295
+ else output.writeStderr("Unable to execute command. Run `kuota --help` for usage.");
1296
+ }
1297
+ return exitCode;
1298
+ }
1299
+ // src/bin.ts
1300
+ runCli(process.argv.slice(2)).then((code) => {
1301
+ process.exitCode = code;
1302
+ });
1303
+ export { createLocalJsonlJournal as a, readAll as i, builtInProviders as n, defaultHistoryDirectory as o, createQuotaService as r, PROVIDER_METADATA as t };