@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/LICENSE +21 -0
- package/README.md +130 -2
- package/bin/kuota.mjs +3 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +1303 -0
- package/dist/dist-CIjerpSd.mjs +576 -0
- package/dist/history-G4o4HVvW.d.mts +36 -0
- package/dist/history-oOXsjdWR.mjs +236 -0
- package/dist/history.d.mts +3 -0
- package/dist/history.mjs +3 -0
- package/dist/index-2k6FkhpT.d.mts +81 -0
- package/dist/index-NHzh2TGn.d.mts +168 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +7 -0
- package/dist/main-B-hZbKK1.mjs +340 -0
- package/dist/metrics-Bzmrfgn-.mjs +307 -0
- package/dist/metrics-D7oNul_9.d.mts +30 -0
- package/dist/metrics.d.mts +3 -0
- package/dist/metrics.mjs +3 -0
- package/dist/parsers.d.mts +13 -0
- package/dist/parsers.mjs +19 -0
- package/dist/providers.d.mts +2 -0
- package/dist/providers.mjs +1 -0
- package/dist/src-C48Ew_L9.mjs +707 -0
- package/dist/src-y_QKmTyj.mjs +309 -0
- package/dist/web/assets/index-ihoe7C_u.js +27 -0
- package/dist/web/index.html +30 -0
- package/dist/web-server-DkdA5Ixj.mjs +154 -0
- package/package.json +73 -4
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 };
|