letmecode 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@ npx -y letmecode -- -h
|
|
|
8
8
|
npx -y letmecode -- --log-to log.txt
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
`--log-to` now records Claude binary discovery, session-root selection, parsed transcript file summaries, entrypoint matching, raw `/usage` output, and live-window event matching so zero-token windows are diagnosable.
|
|
12
|
+
|
|
11
13
|
<img width="2308" height="1491" alt="image" src="https://github.com/user-attachments/assets/f3f52d79-00e3-4ff5-bf2f-65f8be632aaa" />
|
|
12
14
|
|
|
13
15
|
## Controls
|
|
@@ -62,8 +62,9 @@ export function buildHelpText() {
|
|
|
62
62
|
"",
|
|
63
63
|
"Trace logging:",
|
|
64
64
|
" --log-to PATH writes Claude CLI SDK and Claude VSCode detection details,",
|
|
65
|
+
" session root selection, parsed session file summaries, entrypoint matching,",
|
|
65
66
|
" every candidate binary path check, the final found/not-found result,",
|
|
66
|
-
" and the raw /usage command output."
|
|
67
|
+
" and the raw /usage command output plus live window matching details."
|
|
67
68
|
].join("\n");
|
|
68
69
|
}
|
|
69
70
|
export function createFileTraceLogger(logPath) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import https from "node:https";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import os from "node:os";
|
|
@@ -66,6 +67,21 @@ const RATE_CARD = {
|
|
|
66
67
|
const UNPRICED_MODELS = new Set([
|
|
67
68
|
"gpt-oss-120b"
|
|
68
69
|
]);
|
|
70
|
+
const ANTIGRAVITY_PRIMARY_WINDOW_MINUTES = 5 * 60;
|
|
71
|
+
const ANTIGRAVITY_WEEKLY_WINDOW_MINUTES = 7 * 24 * 60;
|
|
72
|
+
const ANTIGRAVITY_QUOTA_SUMMARY_PATH = "/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary";
|
|
73
|
+
const ANTIGRAVITY_DEBUG_LOG_PATH = process.env.LETMECODE_ANTIGRAVITY_DEBUG_LOG ??
|
|
74
|
+
path.join(os.tmpdir(), "letmecode-antigravity-debug.jsonl");
|
|
75
|
+
const GEMINI_QUOTA_MODELS = [
|
|
76
|
+
"gemini-3.5-flash",
|
|
77
|
+
"gemini-3.1-pro",
|
|
78
|
+
"gemini-3-flash"
|
|
79
|
+
];
|
|
80
|
+
const THIRD_PARTY_QUOTA_MODELS = [
|
|
81
|
+
"claude-opus-4-6",
|
|
82
|
+
"claude-sonnet-4-6",
|
|
83
|
+
"gpt-oss-120b"
|
|
84
|
+
];
|
|
69
85
|
const MODEL_ALIASES = {
|
|
70
86
|
"gemini-3-flash-a": "gemini-3-flash",
|
|
71
87
|
"gemini-3-flash-preview": "gemini-3-flash",
|
|
@@ -80,15 +96,33 @@ export class AntigravityUsageProvider extends UsageProviderBase {
|
|
|
80
96
|
this.collectUsage =
|
|
81
97
|
options.collectUsage ??
|
|
82
98
|
collectAntigravityUsageFromTokscale;
|
|
99
|
+
this.collectQuota =
|
|
100
|
+
options.collectQuota ??
|
|
101
|
+
collectAntigravityQuotaFromLocalRpc;
|
|
83
102
|
}
|
|
84
103
|
async getStats(_options = {}) {
|
|
85
104
|
const warnings = [];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
const [usageResult, quotaResult] = await Promise.allSettled([
|
|
106
|
+
this.collectUsage(),
|
|
107
|
+
this.collectQuota()
|
|
108
|
+
]);
|
|
109
|
+
const records = usageResult.status === "fulfilled"
|
|
110
|
+
? usageResult.value
|
|
111
|
+
: [];
|
|
112
|
+
const quotaSnapshot = quotaResult.status === "fulfilled"
|
|
113
|
+
? quotaResult.value
|
|
114
|
+
: null;
|
|
115
|
+
if (usageResult.status === "rejected") {
|
|
116
|
+
warnings.push("Could not synchronize Antigravity token usage through Tokscale.");
|
|
117
|
+
}
|
|
118
|
+
if (quotaResult.status === "rejected") {
|
|
119
|
+
warnings.push("Live Antigravity quota is unavailable. Ensure the Antigravity IDE is running.");
|
|
89
120
|
}
|
|
90
|
-
|
|
91
|
-
warnings.push("
|
|
121
|
+
else if (quotaResult.value.entries.length === 0) {
|
|
122
|
+
warnings.push("Antigravity local quota RPC responded, but no recognized model quota windows were found.");
|
|
123
|
+
}
|
|
124
|
+
if (isAntigravityDebugEnabled()) {
|
|
125
|
+
warnings.push(`Antigravity debug log: ${ANTIGRAVITY_DEBUG_LOG_PATH}`);
|
|
92
126
|
}
|
|
93
127
|
const selectedRecords = deduplicateRecords(records);
|
|
94
128
|
const duplicateEvents = records.length - selectedRecords.length;
|
|
@@ -116,6 +150,7 @@ export class AntigravityUsageProvider extends UsageProviderBase {
|
|
|
116
150
|
if (unknownPricedModels.length > 0) {
|
|
117
151
|
warnings.push(`No Antigravity estimated API-equivalent rate configured for: ${unknownPricedModels.join(", ")}.`);
|
|
118
152
|
}
|
|
153
|
+
const limitWindows = quotaSnapshot?.entries.map((quota) => buildAntigravityLimitWindow(quota, selectedRecords, quotaSnapshot.fetchedAt)) ?? [];
|
|
119
154
|
return {
|
|
120
155
|
providerId: this.id,
|
|
121
156
|
providerLabel: this.label,
|
|
@@ -125,14 +160,16 @@ export class AntigravityUsageProvider extends UsageProviderBase {
|
|
|
125
160
|
tokenEvents: selectedRecords.length,
|
|
126
161
|
totals: sumUsageTotals(modelUsage.map((row) => row.totals)),
|
|
127
162
|
distinctModels: modelUsage.map((row) => row.modelId),
|
|
128
|
-
distinctPlanTypes: [
|
|
129
|
-
|
|
130
|
-
|
|
163
|
+
distinctPlanTypes: [
|
|
164
|
+
...new Set(limitWindows.map((window) => window.planType))
|
|
165
|
+
],
|
|
166
|
+
rootLabel: "Tokscale usage + Antigravity local quota",
|
|
167
|
+
rootPath: getAntigravityCacheRoot()
|
|
131
168
|
},
|
|
132
169
|
modelUsage,
|
|
133
170
|
dayUsage: buildDailyUsageRows(byDay),
|
|
134
|
-
primaryLimitWindows:
|
|
135
|
-
secondaryLimitWindows:
|
|
171
|
+
primaryLimitWindows: limitWindows.filter((window) => window.scope === "primary"),
|
|
172
|
+
secondaryLimitWindows: limitWindows.filter((window) => window.scope === "secondary"),
|
|
136
173
|
warnings
|
|
137
174
|
};
|
|
138
175
|
}
|
|
@@ -140,6 +177,9 @@ export class AntigravityUsageProvider extends UsageProviderBase {
|
|
|
140
177
|
export async function collectAntigravityUsage() {
|
|
141
178
|
return collectAntigravityUsageFromTokscale();
|
|
142
179
|
}
|
|
180
|
+
export async function collectAntigravityQuota() {
|
|
181
|
+
return collectAntigravityQuotaFromLocalRpc();
|
|
182
|
+
}
|
|
143
183
|
async function collectAntigravityUsageFromTokscale() {
|
|
144
184
|
await runTokscale([
|
|
145
185
|
"antigravity",
|
|
@@ -153,11 +193,459 @@ async function runTokscale(args) {
|
|
|
153
193
|
maxBuffer: 32 * 1024 * 1024
|
|
154
194
|
});
|
|
155
195
|
}
|
|
196
|
+
async function collectAntigravityQuotaFromLocalRpc() {
|
|
197
|
+
const server = await findAntigravityLocalServer();
|
|
198
|
+
if (!server) {
|
|
199
|
+
throw new Error("Antigravity local language server was not found.");
|
|
200
|
+
}
|
|
201
|
+
const fetchedAt = Date.now();
|
|
202
|
+
const payload = await readAntigravityQuotaSummary(server);
|
|
203
|
+
const entries = parseAntigravityQuotaEntries(payload);
|
|
204
|
+
await writeAntigravityDebugEvent("quota-rpc-response", {
|
|
205
|
+
port: server.port,
|
|
206
|
+
path: ANTIGRAVITY_QUOTA_SUMMARY_PATH,
|
|
207
|
+
entries: entries.map((entry) => ({
|
|
208
|
+
limitId: entry.limitId,
|
|
209
|
+
remainingFraction: entry.remainingFraction,
|
|
210
|
+
resetAt: new Date(entry.resetAt).toISOString(),
|
|
211
|
+
windowMinutes: entry.windowMinutes,
|
|
212
|
+
scope: entry.scope,
|
|
213
|
+
modelIds: entry.modelIds
|
|
214
|
+
})),
|
|
215
|
+
...(isAntigravityRawDebugEnabled() ? { payload } : {})
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
entries,
|
|
219
|
+
fetchedAt
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function recordsForQuotaWindow(quota, records) {
|
|
223
|
+
if (quota.modelIds.length === 0) {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
const endMs = quota.resetAt;
|
|
227
|
+
const startMs = endMs - quota.windowMinutes * 60000;
|
|
228
|
+
const modelIds = new Set(quota.modelIds.map(resolveModelId));
|
|
229
|
+
return records.filter((record) => {
|
|
230
|
+
const modelId = resolveModelId(record.modelId);
|
|
231
|
+
return (record.timestamp >= startMs &&
|
|
232
|
+
record.timestamp < endMs &&
|
|
233
|
+
modelIds.has(modelId));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function buildAntigravityLimitWindow(quota, records, fetchedAt) {
|
|
237
|
+
const matchingRecords = recordsForQuotaWindow(quota, records);
|
|
238
|
+
const byModel = new Map();
|
|
239
|
+
for (const record of matchingRecords) {
|
|
240
|
+
const modelId = resolveModelId(record.modelId);
|
|
241
|
+
addModelUsage(byModel, modelId, usageRecordToTotals(modelId, record));
|
|
242
|
+
}
|
|
243
|
+
const modelUsage = [...byModel.entries()]
|
|
244
|
+
.map(([modelId, totals]) => ({
|
|
245
|
+
modelId,
|
|
246
|
+
totals
|
|
247
|
+
}))
|
|
248
|
+
.sort((left, right) => right.totals.estimatedCredits -
|
|
249
|
+
left.totals.estimatedCredits);
|
|
250
|
+
const totals = sumUsageTotals(modelUsage.map((row) => row.totals));
|
|
251
|
+
const usedPercent = clampPercent((1 - quota.remainingFraction) * 100);
|
|
252
|
+
// Quota percentage is authoritative from Antigravity RPC. Token totals are
|
|
253
|
+
// reconstructed from locally available Tokscale events inside the same time
|
|
254
|
+
// window and may not match Antigravity's internal quota accounting exactly.
|
|
255
|
+
return {
|
|
256
|
+
scope: quota.scope,
|
|
257
|
+
planType: quota.planType,
|
|
258
|
+
limitId: quota.limitId,
|
|
259
|
+
windowMinutes: quota.windowMinutes,
|
|
260
|
+
startTimeUtcIso: new Date(quota.resetAt - quota.windowMinutes * 60000).toISOString(),
|
|
261
|
+
endTimeUtcIso: new Date(quota.resetAt).toISOString(),
|
|
262
|
+
firstSeenUtcIso: new Date(fetchedAt).toISOString(),
|
|
263
|
+
lastSeenUtcIso: new Date(fetchedAt).toISOString(),
|
|
264
|
+
minUsedPercent: usedPercent,
|
|
265
|
+
maxUsedPercent: usedPercent,
|
|
266
|
+
totals,
|
|
267
|
+
modelUsage,
|
|
268
|
+
eventCount: totals.eventCount
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const antigravityHttpsAgent = new https.Agent({
|
|
272
|
+
rejectUnauthorized: false
|
|
273
|
+
});
|
|
274
|
+
async function findAntigravityLocalServer() {
|
|
275
|
+
const process = await findAntigravityProcess();
|
|
276
|
+
if (!process) {
|
|
277
|
+
await writeAntigravityDebugEvent("process-not-found", {});
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const ports = await findListeningLoopbackPorts(process.pid);
|
|
281
|
+
await writeAntigravityDebugEvent("process-found", {
|
|
282
|
+
pid: process.pid,
|
|
283
|
+
ports
|
|
284
|
+
});
|
|
285
|
+
return probeAntigravityPorts(ports, process.csrfToken);
|
|
286
|
+
}
|
|
287
|
+
async function findAntigravityProcess() {
|
|
288
|
+
const fromProc = await findAntigravityProcessFromProc();
|
|
289
|
+
if (fromProc) {
|
|
290
|
+
return fromProc;
|
|
291
|
+
}
|
|
292
|
+
return findAntigravityProcessFromPs();
|
|
293
|
+
}
|
|
294
|
+
async function findAntigravityProcessFromProc() {
|
|
295
|
+
let entries;
|
|
296
|
+
try {
|
|
297
|
+
entries = await fs.promises.readdir("/proc", { withFileTypes: true });
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
for (const entry of entries) {
|
|
303
|
+
if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const pid = Number(entry.name);
|
|
307
|
+
const cmdline = await readProcCmdline(path.join("/proc", entry.name, "cmdline"));
|
|
308
|
+
const process = parseAntigravityProcessFromArgs(pid, cmdline);
|
|
309
|
+
if (process) {
|
|
310
|
+
return process;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
async function readProcCmdline(filePath) {
|
|
316
|
+
try {
|
|
317
|
+
const content = await fs.promises.readFile(filePath);
|
|
318
|
+
return content
|
|
319
|
+
.toString("utf8")
|
|
320
|
+
.split("\0")
|
|
321
|
+
.filter(Boolean);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function findAntigravityProcessFromPs() {
|
|
328
|
+
try {
|
|
329
|
+
const { stdout } = await execFileAsync("ps", ["-eo", "pid=,args="], {
|
|
330
|
+
encoding: "utf8",
|
|
331
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
332
|
+
timeout: 5000
|
|
333
|
+
});
|
|
334
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
335
|
+
const match = line.match(/^\s*(\d+)\s+(.+)$/);
|
|
336
|
+
if (!match) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const process = parseAntigravityProcessFromArgs(Number(match[1]), splitCommandLineForDiscovery(match[2]));
|
|
340
|
+
if (process) {
|
|
341
|
+
return process;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
function splitCommandLineForDiscovery(value) {
|
|
351
|
+
return value.match(/"[^"]*"|'[^']*'|\S+/g)?.map((part) => part.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
352
|
+
}
|
|
353
|
+
function parseAntigravityProcessFromArgs(pid, args) {
|
|
354
|
+
if (!Number.isInteger(pid) || pid <= 0 || !isAntigravityLanguageServerCommand(args)) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const csrfToken = readNamedArg(args, "--csrf_token")?.trim();
|
|
358
|
+
if (!csrfToken) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
return { pid, csrfToken };
|
|
362
|
+
}
|
|
363
|
+
function isAntigravityLanguageServerCommand(args) {
|
|
364
|
+
const normalized = args.join(" ").toLowerCase();
|
|
365
|
+
return (normalized.includes("antigravity") &&
|
|
366
|
+
(normalized.includes("language-server") ||
|
|
367
|
+
normalized.includes("language_server") ||
|
|
368
|
+
normalized.includes("extension-server") ||
|
|
369
|
+
normalized.includes("extension_server")));
|
|
370
|
+
}
|
|
371
|
+
function readNamedArg(args, name) {
|
|
372
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
373
|
+
const arg = args[index];
|
|
374
|
+
if (arg === name) {
|
|
375
|
+
return args[index + 1] ?? null;
|
|
376
|
+
}
|
|
377
|
+
if (arg.startsWith(`${name}=`)) {
|
|
378
|
+
return arg.slice(name.length + 1);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
async function findListeningLoopbackPorts(pid) {
|
|
384
|
+
const parsers = [
|
|
385
|
+
() => findListeningLoopbackPortsWithSs(pid),
|
|
386
|
+
() => findListeningLoopbackPortsWithLsof(pid)
|
|
387
|
+
];
|
|
388
|
+
for (const parse of parsers) {
|
|
389
|
+
const ports = await parse();
|
|
390
|
+
if (ports.length > 0) {
|
|
391
|
+
return ports;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
async function findListeningLoopbackPortsWithSs(pid) {
|
|
397
|
+
try {
|
|
398
|
+
const { stdout } = await execFileAsync("ss", ["-H", "-ltnp"], {
|
|
399
|
+
encoding: "utf8",
|
|
400
|
+
maxBuffer: 1024 * 1024,
|
|
401
|
+
timeout: 5000
|
|
402
|
+
});
|
|
403
|
+
return uniquePorts(stdout
|
|
404
|
+
.split(/\r?\n/)
|
|
405
|
+
.filter((line) => line.includes(`pid=${pid},`) && isLoopbackListenLine(line))
|
|
406
|
+
.map(extractPortFromListenLine));
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async function findListeningLoopbackPortsWithLsof(pid) {
|
|
413
|
+
try {
|
|
414
|
+
const { stdout } = await execFileAsync("lsof", ["-Pan", "-p", String(pid), "-iTCP", "-sTCP:LISTEN"], {
|
|
415
|
+
encoding: "utf8",
|
|
416
|
+
maxBuffer: 1024 * 1024,
|
|
417
|
+
timeout: 5000
|
|
418
|
+
});
|
|
419
|
+
return uniquePorts(stdout
|
|
420
|
+
.split(/\r?\n/)
|
|
421
|
+
.filter(isLoopbackListenLine)
|
|
422
|
+
.map(extractPortFromListenLine));
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function isLoopbackListenLine(line) {
|
|
429
|
+
return /(?:127\.0\.0\.1|localhost|\[::1\]|::1):\d+\b/.test(line);
|
|
430
|
+
}
|
|
431
|
+
function extractPortFromListenLine(line) {
|
|
432
|
+
const matches = [...line.matchAll(/(?:127\.0\.0\.1|localhost|\[::1\]|::1):(\d+)/g)];
|
|
433
|
+
const value = matches.at(-1)?.[1];
|
|
434
|
+
const port = value ? Number(value) : NaN;
|
|
435
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
|
|
436
|
+
}
|
|
437
|
+
function uniquePorts(ports) {
|
|
438
|
+
return [...new Set(ports.filter((port) => port !== null))];
|
|
439
|
+
}
|
|
440
|
+
async function probeAntigravityPorts(ports, csrfToken) {
|
|
441
|
+
for (const port of ports) {
|
|
442
|
+
const server = { port, csrfToken };
|
|
443
|
+
try {
|
|
444
|
+
await readAntigravityQuotaSummary(server);
|
|
445
|
+
await writeAntigravityDebugEvent("port-probe-ok", { port });
|
|
446
|
+
return server;
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
await writeAntigravityDebugEvent("port-probe-failed", {
|
|
450
|
+
port,
|
|
451
|
+
error: error instanceof Error ? error.message : String(error)
|
|
452
|
+
});
|
|
453
|
+
// Try the next loopback listener owned by the same Antigravity process.
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
async function readAntigravityQuotaSummary(server) {
|
|
459
|
+
return requestAntigravityQuotaSummary(server);
|
|
460
|
+
}
|
|
461
|
+
function requestAntigravityQuotaSummary(server) {
|
|
462
|
+
const body = "{}";
|
|
463
|
+
return new Promise((resolve, reject) => {
|
|
464
|
+
const request = https.request({
|
|
465
|
+
hostname: "127.0.0.1",
|
|
466
|
+
port: server.port,
|
|
467
|
+
path: ANTIGRAVITY_QUOTA_SUMMARY_PATH,
|
|
468
|
+
method: "POST",
|
|
469
|
+
timeout: 5000,
|
|
470
|
+
agent: antigravityHttpsAgent,
|
|
471
|
+
headers: {
|
|
472
|
+
"X-Codeium-Csrf-Token": server.csrfToken,
|
|
473
|
+
"Content-Type": "application/json",
|
|
474
|
+
"Connect-Protocol-Version": "1",
|
|
475
|
+
Accept: "application/json",
|
|
476
|
+
"Content-Length": Buffer.byteLength(body)
|
|
477
|
+
}
|
|
478
|
+
}, (response) => {
|
|
479
|
+
const chunks = [];
|
|
480
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
481
|
+
response.on("end", () => {
|
|
482
|
+
const responseBody = Buffer.concat(chunks).toString("utf8");
|
|
483
|
+
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
|
|
484
|
+
reject(new Error(`Unexpected Antigravity quota summary response: ${response.statusCode ?? "unknown"}`));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
resolve(responseBody ? JSON.parse(responseBody) : {});
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
reject(error);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
request.on("timeout", () => {
|
|
496
|
+
request.destroy(new Error("Timed out reading Antigravity quota summary."));
|
|
497
|
+
});
|
|
498
|
+
request.on("error", reject);
|
|
499
|
+
request.end(body);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
export function parseAntigravityQuotaEntries(payload) {
|
|
503
|
+
const root = asRecord(payload);
|
|
504
|
+
const response = asRecord(root?.response);
|
|
505
|
+
const groups = asArray(response?.groups);
|
|
506
|
+
const entries = [];
|
|
507
|
+
for (const groupValue of groups) {
|
|
508
|
+
const group = asRecord(groupValue);
|
|
509
|
+
if (!group) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const displayName = asString(group.displayName) ?? "";
|
|
513
|
+
const description = asString(group.description) ?? "";
|
|
514
|
+
const modelIds = resolveQuotaGroupModelIds(displayName, description);
|
|
515
|
+
if (modelIds.length === 0) {
|
|
516
|
+
void writeAntigravityDebugEvent("quota-group-skipped", {
|
|
517
|
+
displayName,
|
|
518
|
+
description
|
|
519
|
+
});
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
for (const bucketValue of asArray(group.buckets)) {
|
|
523
|
+
const bucket = asRecord(bucketValue);
|
|
524
|
+
if (!bucket) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const bucketId = asString(bucket.bucketId);
|
|
528
|
+
const windowConfig = resolveQuotaWindow(asString(bucket.window));
|
|
529
|
+
const remainingFraction = asFiniteNumber(bucket.remainingFraction);
|
|
530
|
+
const resetTime = asString(bucket.resetTime);
|
|
531
|
+
const resetAt = resetTime === null ? NaN : Date.parse(resetTime);
|
|
532
|
+
if (!bucketId ||
|
|
533
|
+
remainingFraction === null ||
|
|
534
|
+
remainingFraction < 0 ||
|
|
535
|
+
remainingFraction > 1 ||
|
|
536
|
+
!Number.isFinite(resetAt) ||
|
|
537
|
+
!windowConfig) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
entries.push({
|
|
541
|
+
limitId: bucketId,
|
|
542
|
+
modelIds,
|
|
543
|
+
remainingFraction,
|
|
544
|
+
resetAt,
|
|
545
|
+
windowMinutes: windowConfig.windowMinutes,
|
|
546
|
+
scope: windowConfig.scope,
|
|
547
|
+
planType: "unknown"
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return entries;
|
|
552
|
+
}
|
|
553
|
+
function resolveQuotaWindow(window) {
|
|
554
|
+
switch (window) {
|
|
555
|
+
case "5h":
|
|
556
|
+
return {
|
|
557
|
+
scope: "primary",
|
|
558
|
+
windowMinutes: ANTIGRAVITY_PRIMARY_WINDOW_MINUTES
|
|
559
|
+
};
|
|
560
|
+
case "weekly":
|
|
561
|
+
return {
|
|
562
|
+
scope: "secondary",
|
|
563
|
+
windowMinutes: ANTIGRAVITY_WEEKLY_WINDOW_MINUTES
|
|
564
|
+
};
|
|
565
|
+
default:
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function resolveQuotaGroupModelIds(displayName, description) {
|
|
570
|
+
const text = `${displayName} ${description}`.toLowerCase();
|
|
571
|
+
if (text.includes("gemini")) {
|
|
572
|
+
return GEMINI_QUOTA_MODELS;
|
|
573
|
+
}
|
|
574
|
+
if (text.includes("claude") || text.includes("gpt")) {
|
|
575
|
+
return THIRD_PARTY_QUOTA_MODELS;
|
|
576
|
+
}
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
function asRecord(value) {
|
|
580
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
581
|
+
? value
|
|
582
|
+
: null;
|
|
583
|
+
}
|
|
584
|
+
function asArray(value) {
|
|
585
|
+
return Array.isArray(value) ? value : [];
|
|
586
|
+
}
|
|
587
|
+
function asString(value) {
|
|
588
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
589
|
+
}
|
|
590
|
+
function asFiniteNumber(value) {
|
|
591
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
592
|
+
}
|
|
593
|
+
function isAntigravityDebugEnabled() {
|
|
594
|
+
const value = process.env.LETMECODE_DEBUG_ANTIGRAVITY;
|
|
595
|
+
return value === "1" || value === "true" || value === "yes";
|
|
596
|
+
}
|
|
597
|
+
function isAntigravityRawDebugEnabled() {
|
|
598
|
+
const value = process.env.LETMECODE_DEBUG_ANTIGRAVITY_RAW;
|
|
599
|
+
return value === "1" || value === "true" || value === "yes";
|
|
600
|
+
}
|
|
601
|
+
async function writeAntigravityDebugEvent(event, data) {
|
|
602
|
+
if (!isAntigravityDebugEnabled()) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const line = JSON.stringify({
|
|
606
|
+
timestamp: new Date().toISOString(),
|
|
607
|
+
event,
|
|
608
|
+
data: redactDebugValue(data)
|
|
609
|
+
});
|
|
610
|
+
try {
|
|
611
|
+
await fs.promises.mkdir(path.dirname(ANTIGRAVITY_DEBUG_LOG_PATH), {
|
|
612
|
+
recursive: true
|
|
613
|
+
});
|
|
614
|
+
await fs.promises.appendFile(ANTIGRAVITY_DEBUG_LOG_PATH, `${line}\n`, "utf8");
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
// Debug logging must never break provider stats collection.
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function redactDebugValue(value, key = "") {
|
|
621
|
+
if (isSensitiveDebugKey(key)) {
|
|
622
|
+
return "[redacted]";
|
|
623
|
+
}
|
|
624
|
+
if (Array.isArray(value)) {
|
|
625
|
+
return value.map((item) => redactDebugValue(item));
|
|
626
|
+
}
|
|
627
|
+
if (!value || typeof value !== "object") {
|
|
628
|
+
return value;
|
|
629
|
+
}
|
|
630
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
631
|
+
entryKey,
|
|
632
|
+
redactDebugValue(entryValue, entryKey)
|
|
633
|
+
]));
|
|
634
|
+
}
|
|
635
|
+
function isSensitiveDebugKey(key) {
|
|
636
|
+
return /token|csrf|authorization|cookie|email/i.test(key);
|
|
637
|
+
}
|
|
156
638
|
function numberOrZero(value) {
|
|
157
639
|
return typeof value === "number" && Number.isFinite(value)
|
|
158
640
|
? value
|
|
159
641
|
: 0;
|
|
160
642
|
}
|
|
643
|
+
function clampPercent(value) {
|
|
644
|
+
if (!Number.isFinite(value)) {
|
|
645
|
+
return 0;
|
|
646
|
+
}
|
|
647
|
+
return Math.min(100, Math.max(0, value));
|
|
648
|
+
}
|
|
161
649
|
async function readAntigravityUsageCache(cacheRoot) {
|
|
162
650
|
const sessionsRoot = path.join(cacheRoot, "sessions");
|
|
163
651
|
const records = [];
|
|
@@ -58,7 +58,8 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
58
58
|
this.now = options.now ?? (() => new Date());
|
|
59
59
|
}
|
|
60
60
|
async getStats(options = {}) {
|
|
61
|
-
|
|
61
|
+
traceClaude(options.traceLogger, this.usageCommandKind, `Starting stats collection with root=${this.root} entrypoints=[${[...this.entrypoints].join(", ")}].`);
|
|
62
|
+
const resolvedSessionsRoot = await resolveClaudeSessionsRoot(this.root, this.usageCommandKind, options.traceLogger);
|
|
62
63
|
const sessionsRoot = resolvedSessionsRoot.rootPath;
|
|
63
64
|
const agentName = normalizeAnalyticsAgentName(this.label);
|
|
64
65
|
const userIdHash = await readClaudeUserIdHash(this.root, this.usageCommandKind, this.readAuthStatusOutput, agentName, options.traceLogger);
|
|
@@ -74,9 +75,20 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
74
75
|
tokenEvents: 0,
|
|
75
76
|
malformedLines: 0
|
|
76
77
|
};
|
|
77
|
-
const parsedSessionFiles = await loadParsedClaudeSessionFiles(sessionsRoot);
|
|
78
|
+
const parsedSessionFiles = await loadParsedClaudeSessionFiles(sessionsRoot, this.usageCommandKind, options.traceLogger);
|
|
79
|
+
traceClaude(options.traceLogger, this.usageCommandKind, `Loaded ${parsedSessionFiles.length} parsed session file(s) from ${sessionsRoot}.`);
|
|
78
80
|
for (const file of parsedSessionFiles) {
|
|
79
|
-
const matchingEvents = file.events.filter((event) => this.entrypoints.
|
|
81
|
+
const matchingEvents = file.events.filter((event) => matchesClaudeProviderEvent(event, file, this.entrypoints, this.usageCommandKind));
|
|
82
|
+
traceClaude(options.traceLogger, this.usageCommandKind, [
|
|
83
|
+
`Session file ${describeSessionFilePath(sessionsRoot, file.filePath)}:`,
|
|
84
|
+
`lines=${file.linesRead}`,
|
|
85
|
+
`malformed=${file.malformedLines}`,
|
|
86
|
+
`assistantUsageEvents=${file.events.length}`,
|
|
87
|
+
`matchingEvents=${matchingEvents.length}`,
|
|
88
|
+
`source=${file.sourceKind}`,
|
|
89
|
+
`entrypoints=${summarizeEventCounts(file.events.map((event) => event.entrypoint || "<empty>"))}`,
|
|
90
|
+
`models=${summarizeDistinctValues(file.events.map((event) => event.modelId || "unknown"))}`
|
|
91
|
+
].join(" "));
|
|
80
92
|
if (matchingEvents.length === 0) {
|
|
81
93
|
continue;
|
|
82
94
|
}
|
|
@@ -91,6 +103,16 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
91
103
|
...parsedEvents.keyedEvents.values(),
|
|
92
104
|
...parsedEvents.unkeyedEvents.values()
|
|
93
105
|
];
|
|
106
|
+
traceClaude(options.traceLogger, this.usageCommandKind, [
|
|
107
|
+
`Transcript selection summary: filesWithMatches=${parseTotals.filesScanned}/${parsedSessionFiles.length}`,
|
|
108
|
+
`selectedEvents=${selectedEvents.length}`,
|
|
109
|
+
`duplicateUsageKeys=${parsedEvents.duplicateUsageKeys}`,
|
|
110
|
+
`duplicateUsageKeyCollisions=${parsedEvents.duplicateUsageKeyCollisions}`,
|
|
111
|
+
`duplicateUnkeyedEvents=${parsedEvents.duplicateUnkeyedEvents}`
|
|
112
|
+
].join(" "));
|
|
113
|
+
if (selectedEvents.length === 0 && parsedSessionFiles.length > 0) {
|
|
114
|
+
traceClaude(options.traceLogger, this.usageCommandKind, `No transcript usage matched entrypoints [${[...this.entrypoints].join(", ")}]. Observed entrypoints=${summarizeEventCounts(collectEntryPoints(parsedSessionFiles))}.`);
|
|
115
|
+
}
|
|
94
116
|
for (const event of selectedEvents) {
|
|
95
117
|
addModelUsage(byModel, event.modelId, event.totals);
|
|
96
118
|
const planType = typeof event.rateLimits?.plan_type === "string" ? event.rateLimits.plan_type : undefined;
|
|
@@ -141,6 +163,19 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
141
163
|
const secondaryLimitWindows = liveLimitWindows.secondaryLimitWindows.length > 0
|
|
142
164
|
? liveLimitWindows.secondaryLimitWindows
|
|
143
165
|
: fallbackSecondaryLimitWindows;
|
|
166
|
+
traceClaude(options.traceLogger, this.usageCommandKind, [
|
|
167
|
+
`Finished stats collection:`,
|
|
168
|
+
`filesScanned=${parseTotals.filesScanned}`,
|
|
169
|
+
`linesRead=${parseTotals.linesRead}`,
|
|
170
|
+
`tokenEvents=${parseTotals.tokenEvents}`,
|
|
171
|
+
`models=${modelUsage.length}`,
|
|
172
|
+
`primaryWindows=${primaryLimitWindows.length}`,
|
|
173
|
+
`secondaryWindows=${secondaryLimitWindows.length}`,
|
|
174
|
+
`input=${summaryTotals.inputTokens}`,
|
|
175
|
+
`output=${summaryTotals.outputTokens}`,
|
|
176
|
+
`cacheRead=${summaryTotals.cacheReadInputTokens}`,
|
|
177
|
+
`cacheWrite=${summaryTotals.cacheWriteInputTokens}`
|
|
178
|
+
].join(" "));
|
|
144
179
|
return {
|
|
145
180
|
providerId: this.id,
|
|
146
181
|
providerLabel: this.label,
|
|
@@ -240,17 +275,23 @@ function resolveClaudeCacheWriteBreakdown(usage) {
|
|
|
240
275
|
function isSessionFile(filePath) {
|
|
241
276
|
return filePath.endsWith(".jsonl");
|
|
242
277
|
}
|
|
243
|
-
async function resolveClaudeSessionsRoot(root) {
|
|
278
|
+
async function resolveClaudeSessionsRoot(root, usageCommandKind, traceLogger) {
|
|
244
279
|
const candidates = buildClaudeSessionsRootCandidates(root);
|
|
280
|
+
traceClaude(traceLogger, usageCommandKind, `Checking ${candidates.length} Claude session root candidate(s).`);
|
|
245
281
|
for (const candidate of candidates) {
|
|
246
|
-
|
|
282
|
+
const exists = await isDirectory(candidate.rootPath);
|
|
283
|
+
traceClaude(traceLogger, usageCommandKind, `Session root candidate ${candidate.rootLabel} -> ${candidate.rootPath} (${exists ? "exists" : "missing"}).`);
|
|
284
|
+
if (exists) {
|
|
285
|
+
traceClaude(traceLogger, usageCommandKind, `Selected session root ${candidate.rootLabel} -> ${candidate.rootPath}.`);
|
|
247
286
|
return candidate;
|
|
248
287
|
}
|
|
249
288
|
}
|
|
250
|
-
|
|
289
|
+
const fallbackCandidate = candidates[0] ?? {
|
|
251
290
|
rootLabel: "~/.claude/projects",
|
|
252
291
|
rootPath: path.join(path.resolve(root), ".claude", "projects")
|
|
253
292
|
};
|
|
293
|
+
traceClaude(traceLogger, usageCommandKind, `No session root candidate exists yet; defaulting to ${fallbackCandidate.rootLabel} -> ${fallbackCandidate.rootPath}.`);
|
|
294
|
+
return fallbackCandidate;
|
|
254
295
|
}
|
|
255
296
|
function buildClaudeSessionsRootCandidates(root) {
|
|
256
297
|
const resolvedRoot = path.resolve(root);
|
|
@@ -338,28 +379,38 @@ async function* walkSessionFiles(directory) {
|
|
|
338
379
|
}
|
|
339
380
|
}
|
|
340
381
|
}
|
|
341
|
-
async function loadParsedClaudeSessionFiles(sessionsRoot) {
|
|
382
|
+
async function loadParsedClaudeSessionFiles(sessionsRoot, usageCommandKind, traceLogger) {
|
|
342
383
|
const cacheKey = path.resolve(sessionsRoot);
|
|
343
384
|
const cached = parsedClaudeSessionFilesCache.get(cacheKey);
|
|
344
385
|
if (cached) {
|
|
345
|
-
|
|
386
|
+
const files = await cached;
|
|
387
|
+
traceClaude(traceLogger, usageCommandKind, `Session parse cache hit for ${sessionsRoot} (${files.length} file(s)).`);
|
|
388
|
+
return files;
|
|
346
389
|
}
|
|
347
390
|
const pending = (async () => {
|
|
348
391
|
const files = [];
|
|
392
|
+
traceClaude(traceLogger, usageCommandKind, `Scanning session files under ${sessionsRoot}.`);
|
|
349
393
|
for await (const filePath of walkSessionFiles(sessionsRoot)) {
|
|
350
|
-
files.push(await parseSessionFile(filePath));
|
|
394
|
+
files.push(await parseSessionFile(filePath, sessionsRoot));
|
|
351
395
|
}
|
|
396
|
+
inferClaudeSessionFileSources(files);
|
|
397
|
+
traceClaude(traceLogger, usageCommandKind, `Completed session file scan under ${sessionsRoot}: ${files.length} file(s) parsed.`);
|
|
352
398
|
return files;
|
|
353
399
|
})();
|
|
354
400
|
parsedClaudeSessionFilesCache.set(cacheKey, pending);
|
|
355
401
|
return pending;
|
|
356
402
|
}
|
|
357
|
-
async function parseSessionFile(filePath) {
|
|
403
|
+
async function parseSessionFile(filePath, sessionsRoot) {
|
|
358
404
|
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
359
405
|
const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
360
406
|
let linesRead = 0;
|
|
361
407
|
let malformedLines = 0;
|
|
362
408
|
const events = [];
|
|
409
|
+
const assistantEntryPoints = new Set();
|
|
410
|
+
let hasIdeOpenedFileAttachment = false;
|
|
411
|
+
let hasIdeOpenedFileMarker = false;
|
|
412
|
+
let hasIdeTooling = false;
|
|
413
|
+
let hasQueueOperations = false;
|
|
363
414
|
for await (const line of lineReader) {
|
|
364
415
|
linesRead += 1;
|
|
365
416
|
if (!line.trim()) {
|
|
@@ -373,6 +424,19 @@ async function parseSessionFile(filePath) {
|
|
|
373
424
|
malformedLines += 1;
|
|
374
425
|
continue;
|
|
375
426
|
}
|
|
427
|
+
if (payloadObject.type === "queue-operation") {
|
|
428
|
+
hasQueueOperations = true;
|
|
429
|
+
}
|
|
430
|
+
if (messageContainsIdeOpenedFileMarker(asRecord(payloadObject.message))) {
|
|
431
|
+
hasIdeOpenedFileMarker = true;
|
|
432
|
+
}
|
|
433
|
+
const attachment = asRecord(payloadObject.attachment);
|
|
434
|
+
if (attachment?.type === "opened_file_in_ide") {
|
|
435
|
+
hasIdeOpenedFileAttachment = true;
|
|
436
|
+
}
|
|
437
|
+
if (attachmentHasIdeTooling(attachment)) {
|
|
438
|
+
hasIdeTooling = true;
|
|
439
|
+
}
|
|
376
440
|
if (payloadObject.type !== "assistant") {
|
|
377
441
|
continue;
|
|
378
442
|
}
|
|
@@ -383,12 +447,14 @@ async function parseSessionFile(filePath) {
|
|
|
383
447
|
}
|
|
384
448
|
const modelId = String(message?.model ?? "unknown");
|
|
385
449
|
const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
|
|
450
|
+
const entrypoint = typeof payloadObject.entrypoint === "string" ? payloadObject.entrypoint : "";
|
|
386
451
|
const rateLimits = extractRateLimits(payloadObject, message);
|
|
387
452
|
const normalizedUsage = normalizeUsage(usage);
|
|
388
453
|
const usageKey = buildUsageEventKey(payloadObject, message);
|
|
389
454
|
const usageSignature = buildUsageSignature(payloadObject, modelId, normalizedUsage);
|
|
455
|
+
assistantEntryPoints.add(entrypoint);
|
|
390
456
|
events.push({
|
|
391
|
-
entrypoint
|
|
457
|
+
entrypoint,
|
|
392
458
|
usageKey,
|
|
393
459
|
usageSignature,
|
|
394
460
|
timestampMs: eventTimeMs,
|
|
@@ -397,7 +463,101 @@ async function parseSessionFile(filePath) {
|
|
|
397
463
|
rateLimits
|
|
398
464
|
});
|
|
399
465
|
}
|
|
400
|
-
return {
|
|
466
|
+
return {
|
|
467
|
+
filePath,
|
|
468
|
+
sessionGroupKey: buildClaudeSessionGroupKey(sessionsRoot, filePath),
|
|
469
|
+
linesRead,
|
|
470
|
+
malformedLines,
|
|
471
|
+
sourceKind: "unknown",
|
|
472
|
+
sourceReason: "unclassified",
|
|
473
|
+
signals: {
|
|
474
|
+
assistantEntryPoints: [...assistantEntryPoints].sort(),
|
|
475
|
+
hasIdeOpenedFileAttachment,
|
|
476
|
+
hasIdeOpenedFileMarker,
|
|
477
|
+
hasIdeTooling,
|
|
478
|
+
hasQueueOperations
|
|
479
|
+
},
|
|
480
|
+
events
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function buildClaudeSessionGroupKey(sessionsRoot, filePath) {
|
|
484
|
+
const relativePath = path.relative(sessionsRoot, filePath);
|
|
485
|
+
if (!relativePath || relativePath.startsWith("..")) {
|
|
486
|
+
return filePath;
|
|
487
|
+
}
|
|
488
|
+
const normalizedRelativePath = relativePath.split(path.sep).join("/");
|
|
489
|
+
const subagentMatch = normalizedRelativePath.match(/^(.*\/[^/]+)\/subagents\/[^/]+\.jsonl$/);
|
|
490
|
+
if (subagentMatch?.[1]) {
|
|
491
|
+
return subagentMatch[1];
|
|
492
|
+
}
|
|
493
|
+
return normalizedRelativePath.replace(/\.jsonl$/i, "");
|
|
494
|
+
}
|
|
495
|
+
function inferClaudeSessionFileSources(files) {
|
|
496
|
+
const groups = new Map();
|
|
497
|
+
for (const file of files) {
|
|
498
|
+
const group = groups.get(file.sessionGroupKey) ?? {
|
|
499
|
+
assistantEntryPoints: new Set(),
|
|
500
|
+
hasIdeHints: false
|
|
501
|
+
};
|
|
502
|
+
for (const entrypoint of file.signals.assistantEntryPoints) {
|
|
503
|
+
group.assistantEntryPoints.add(entrypoint);
|
|
504
|
+
}
|
|
505
|
+
group.hasIdeHints =
|
|
506
|
+
group.hasIdeHints ||
|
|
507
|
+
file.signals.hasIdeOpenedFileAttachment ||
|
|
508
|
+
file.signals.hasIdeOpenedFileMarker ||
|
|
509
|
+
file.signals.hasIdeTooling ||
|
|
510
|
+
file.signals.hasQueueOperations;
|
|
511
|
+
groups.set(file.sessionGroupKey, group);
|
|
512
|
+
}
|
|
513
|
+
for (const file of files) {
|
|
514
|
+
const group = groups.get(file.sessionGroupKey);
|
|
515
|
+
const { kind, reason } = classifyClaudeSessionGroup(group);
|
|
516
|
+
file.sourceKind = kind;
|
|
517
|
+
file.sourceReason = reason;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function classifyClaudeSessionGroup(group) {
|
|
521
|
+
if (!group) {
|
|
522
|
+
return { kind: "unknown", reason: "missing session group signals" };
|
|
523
|
+
}
|
|
524
|
+
if (group.assistantEntryPoints.has("claude-vscode")) {
|
|
525
|
+
return { kind: "vscode", reason: "explicit claude-vscode entrypoint" };
|
|
526
|
+
}
|
|
527
|
+
if (group.assistantEntryPoints.has("sdk-cli") || group.assistantEntryPoints.has("claude")) {
|
|
528
|
+
return { kind: "cli", reason: "explicit sdk-cli/claude entrypoint" };
|
|
529
|
+
}
|
|
530
|
+
if (group.assistantEntryPoints.has("cli")) {
|
|
531
|
+
return group.hasIdeHints
|
|
532
|
+
? { kind: "vscode", reason: "generic cli entrypoint with IDE session hints" }
|
|
533
|
+
: { kind: "cli", reason: "generic cli entrypoint without IDE session hints" };
|
|
534
|
+
}
|
|
535
|
+
return { kind: "unknown", reason: "no assistant entrypoints" };
|
|
536
|
+
}
|
|
537
|
+
function attachmentHasIdeTooling(attachment) {
|
|
538
|
+
if (attachment?.type !== "deferred_tools_delta") {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
return extractStringArray(attachment.addedNames).some((name) => name.startsWith("mcp__ide__"));
|
|
542
|
+
}
|
|
543
|
+
function messageContainsIdeOpenedFileMarker(message) {
|
|
544
|
+
const content = message?.content;
|
|
545
|
+
if (typeof content === "string") {
|
|
546
|
+
return content.includes("<ide_opened_file>");
|
|
547
|
+
}
|
|
548
|
+
if (!Array.isArray(content)) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
return content.some((item) => {
|
|
552
|
+
const contentItem = asRecord(item);
|
|
553
|
+
return typeof contentItem?.text === "string" && contentItem.text.includes("<ide_opened_file>");
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
function extractStringArray(value) {
|
|
557
|
+
if (!Array.isArray(value)) {
|
|
558
|
+
return [];
|
|
559
|
+
}
|
|
560
|
+
return value.filter((item) => typeof item === "string");
|
|
401
561
|
}
|
|
402
562
|
function buildUsageEventKey(payloadObject, message) {
|
|
403
563
|
const sessionId = String(payloadObject.sessionId ?? "");
|
|
@@ -465,6 +625,18 @@ function shouldReplaceUsageEvent(previous, next) {
|
|
|
465
625
|
}
|
|
466
626
|
return false;
|
|
467
627
|
}
|
|
628
|
+
function matchesClaudeProviderEvent(event, file, entrypoints, usageCommandKind) {
|
|
629
|
+
if (entrypoints.has(event.entrypoint)) {
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
if (event.entrypoint !== "cli") {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
if (usageCommandKind === "vscode") {
|
|
636
|
+
return file.sourceKind === "vscode";
|
|
637
|
+
}
|
|
638
|
+
return file.sourceKind === "cli";
|
|
639
|
+
}
|
|
468
640
|
function normalizeTimestamp(value) {
|
|
469
641
|
return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
|
|
470
642
|
}
|
|
@@ -493,6 +665,41 @@ function describeUsageOutput(output) {
|
|
|
493
665
|
}
|
|
494
666
|
return output.trim() ? output : "<empty>";
|
|
495
667
|
}
|
|
668
|
+
function describeSessionFilePath(sessionsRoot, filePath) {
|
|
669
|
+
const relativePath = path.relative(sessionsRoot, filePath);
|
|
670
|
+
if (!relativePath || relativePath.startsWith("..")) {
|
|
671
|
+
return filePath;
|
|
672
|
+
}
|
|
673
|
+
return relativePath;
|
|
674
|
+
}
|
|
675
|
+
function summarizeEventCounts(values) {
|
|
676
|
+
const counts = new Map();
|
|
677
|
+
for (const value of values) {
|
|
678
|
+
const normalizedValue = value || "<empty>";
|
|
679
|
+
counts.set(normalizedValue, (counts.get(normalizedValue) ?? 0) + 1);
|
|
680
|
+
}
|
|
681
|
+
if (counts.size === 0) {
|
|
682
|
+
return "<none>";
|
|
683
|
+
}
|
|
684
|
+
return [...counts.entries()]
|
|
685
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
686
|
+
.map(([value, count]) => `${value}:${count}`)
|
|
687
|
+
.join(", ");
|
|
688
|
+
}
|
|
689
|
+
function summarizeDistinctValues(values, limit = 5) {
|
|
690
|
+
const distinctValues = [...new Set([...values].filter(Boolean))].sort();
|
|
691
|
+
if (distinctValues.length === 0) {
|
|
692
|
+
return "<none>";
|
|
693
|
+
}
|
|
694
|
+
const visibleValues = distinctValues.slice(0, limit);
|
|
695
|
+
const remainder = distinctValues.length - visibleValues.length;
|
|
696
|
+
return remainder > 0
|
|
697
|
+
? `${visibleValues.join(", ")} (+${remainder} more)`
|
|
698
|
+
: visibleValues.join(", ");
|
|
699
|
+
}
|
|
700
|
+
function collectEntryPoints(files) {
|
|
701
|
+
return files.flatMap((file) => file.events.map((event) => event.entrypoint || "<empty>"));
|
|
702
|
+
}
|
|
496
703
|
function buildClaudeCommandEnvironment() {
|
|
497
704
|
return {
|
|
498
705
|
...process.env,
|
|
@@ -506,19 +713,47 @@ async function buildLiveLimitWindows(options) {
|
|
|
506
713
|
]);
|
|
507
714
|
const snapshots = parseLiveUsageWindowSnapshots(usageOutput, options.now);
|
|
508
715
|
traceClaude(options.traceLogger, options.usageCommandKind, `Parsed ${snapshots.length} live usage snapshot(s) from /usage output.`);
|
|
716
|
+
if (snapshots.length === 0) {
|
|
717
|
+
traceClaude(options.traceLogger, options.usageCommandKind, "No live usage snapshots matched the expected /usage format.");
|
|
718
|
+
}
|
|
509
719
|
const resolvedPlanType = subscriptionType || "live";
|
|
720
|
+
traceClaude(options.traceLogger, options.usageCommandKind, `Resolved live plan type ${resolvedPlanType}.`);
|
|
721
|
+
const primaryLimitWindows = snapshots
|
|
722
|
+
.filter((snapshot) => snapshot.scope === "primary")
|
|
723
|
+
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now));
|
|
724
|
+
const secondaryLimitWindows = snapshots
|
|
725
|
+
.filter((snapshot) => snapshot.scope === "secondary")
|
|
726
|
+
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now));
|
|
727
|
+
for (let index = 0; index < snapshots.length; index += 1) {
|
|
728
|
+
const snapshot = snapshots[index];
|
|
729
|
+
const row = snapshot.scope === "primary"
|
|
730
|
+
? primaryLimitWindows.filter((window) => window.limitId === `current-${snapshot.label}`)[0]
|
|
731
|
+
: secondaryLimitWindows.filter((window) => window.limitId === `current-${snapshot.label}`)[0];
|
|
732
|
+
if (!row) {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
traceClaude(options.traceLogger, options.usageCommandKind, [
|
|
736
|
+
`Live window ${snapshot.scope}/${snapshot.label}:`,
|
|
737
|
+
`used=${snapshot.usedPercent}%`,
|
|
738
|
+
`range=${row.startTimeUtcIso}->${row.endTimeUtcIso}`,
|
|
739
|
+
`matchedEvents=${row.eventCount}`,
|
|
740
|
+
`input=${row.totals.inputTokens}`,
|
|
741
|
+
`output=${row.totals.outputTokens}`,
|
|
742
|
+
`cacheRead=${row.totals.cacheReadInputTokens}`,
|
|
743
|
+
`cacheWrite=${row.totals.cacheWriteInputTokens}`
|
|
744
|
+
].join(" "));
|
|
745
|
+
}
|
|
510
746
|
return {
|
|
511
|
-
primaryLimitWindows
|
|
512
|
-
|
|
513
|
-
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now)),
|
|
514
|
-
secondaryLimitWindows: snapshots
|
|
515
|
-
.filter((snapshot) => snapshot.scope === "secondary")
|
|
516
|
-
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now))
|
|
747
|
+
primaryLimitWindows,
|
|
748
|
+
secondaryLimitWindows
|
|
517
749
|
};
|
|
518
750
|
}
|
|
519
751
|
async function readClaudeSubscriptionType(root, usageCommandKind, override, traceLogger) {
|
|
520
752
|
const output = await readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger);
|
|
521
753
|
const subscriptionType = parseClaudeSubscriptionType(output);
|
|
754
|
+
if (output && !subscriptionType) {
|
|
755
|
+
traceClaude(traceLogger, usageCommandKind, "Could not parse subscription type from auth status output.");
|
|
756
|
+
}
|
|
522
757
|
traceClaude(traceLogger, usageCommandKind, `Subscription type result: ${subscriptionType ?? "<none>"}.`);
|
|
523
758
|
return subscriptionType;
|
|
524
759
|
}
|
|
@@ -602,6 +837,7 @@ async function readClaudeUserIdHash(root, usageCommandKind, override, agentName,
|
|
|
602
837
|
const authStatusOutput = await readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger);
|
|
603
838
|
const snapshot = parseClaudeAuthStatusSnapshot(authStatusOutput);
|
|
604
839
|
if (!snapshot) {
|
|
840
|
+
traceClaude(traceLogger, usageCommandKind, "Auth status output did not yield an analytics identity snapshot.");
|
|
605
841
|
return null;
|
|
606
842
|
}
|
|
607
843
|
return buildUserIdHash([agentName, snapshot.email, snapshot.orgId, snapshot.orgName]);
|
|
@@ -16,7 +16,7 @@ export function createProviders() {
|
|
|
16
16
|
new AntigravityUsageProvider()
|
|
17
17
|
];
|
|
18
18
|
}
|
|
19
|
-
export { AntigravityUsageProvider } from "./antigravity.js";
|
|
19
|
+
export { AntigravityUsageProvider, collectAntigravityQuota, collectAntigravityUsage } from "./antigravity.js";
|
|
20
20
|
export { ClaudeUsageProvider } from "./claude.js";
|
|
21
21
|
export { CodexUsageProvider } from "./codex.js";
|
|
22
22
|
export { CopilotUsageProvider, configureCopilotVsCodeLogging } from "./copilot.js";
|