letmecode 0.1.10 → 0.1.12
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.
|
@@ -61,8 +61,8 @@ export function buildHelpText() {
|
|
|
61
61
|
" q or Esc Quit",
|
|
62
62
|
"",
|
|
63
63
|
"Trace logging:",
|
|
64
|
-
" --log-to PATH writes Claude
|
|
65
|
-
" session root selection, parsed session file summaries,
|
|
64
|
+
" --log-to PATH writes Claude detection details,",
|
|
65
|
+
" session root selection, parsed session file summaries, aggregated usage selection,",
|
|
66
66
|
" every candidate binary path check, the final found/not-found result,",
|
|
67
67
|
" and the raw /usage command output plus live window matching details."
|
|
68
68
|
].join("\n");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import https from "node:https";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import fs from "node:fs";
|
|
@@ -70,6 +71,7 @@ const UNPRICED_MODELS = new Set([
|
|
|
70
71
|
const ANTIGRAVITY_PRIMARY_WINDOW_MINUTES = 5 * 60;
|
|
71
72
|
const ANTIGRAVITY_WEEKLY_WINDOW_MINUTES = 7 * 24 * 60;
|
|
72
73
|
const ANTIGRAVITY_QUOTA_SUMMARY_PATH = "/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary";
|
|
74
|
+
const ANTIGRAVITY_USER_STATUS_PATH = "/exa.language_server_pb.LanguageServerService/GetUserStatus";
|
|
73
75
|
const ANTIGRAVITY_DEBUG_LOG_PATH = process.env.LETMECODE_ANTIGRAVITY_DEBUG_LOG ??
|
|
74
76
|
path.join(os.tmpdir(), "letmecode-antigravity-debug.jsonl");
|
|
75
77
|
const GEMINI_QUOTA_MODELS = [
|
|
@@ -170,7 +172,13 @@ export class AntigravityUsageProvider extends UsageProviderBase {
|
|
|
170
172
|
dayUsage: buildDailyUsageRows(byDay),
|
|
171
173
|
primaryLimitWindows: limitWindows.filter((window) => window.scope === "primary"),
|
|
172
174
|
secondaryLimitWindows: limitWindows.filter((window) => window.scope === "secondary"),
|
|
173
|
-
warnings
|
|
175
|
+
warnings,
|
|
176
|
+
analytics: quotaSnapshot?.userIdHash
|
|
177
|
+
? {
|
|
178
|
+
agentName: normalizeAnalyticsAgentName(this.label),
|
|
179
|
+
userIdHash: quotaSnapshot.userIdHash
|
|
180
|
+
}
|
|
181
|
+
: undefined
|
|
174
182
|
};
|
|
175
183
|
}
|
|
176
184
|
}
|
|
@@ -199,11 +207,29 @@ async function collectAntigravityQuotaFromLocalRpc() {
|
|
|
199
207
|
throw new Error("Antigravity local language server was not found.");
|
|
200
208
|
}
|
|
201
209
|
const fetchedAt = Date.now();
|
|
202
|
-
const
|
|
203
|
-
|
|
210
|
+
const [quotaResult, statusResult] = await Promise.allSettled([
|
|
211
|
+
readAntigravityQuotaSummary(server),
|
|
212
|
+
readAntigravityUserStatus(server)
|
|
213
|
+
]);
|
|
214
|
+
printAntigravityUserStatusResponse(statusResult);
|
|
215
|
+
if (quotaResult.status === "rejected") {
|
|
216
|
+
throw quotaResult.reason;
|
|
217
|
+
}
|
|
218
|
+
const entries = parseAntigravityQuotaEntries(quotaResult.value);
|
|
219
|
+
const planType = statusResult.status === "fulfilled"
|
|
220
|
+
? parseAntigravityPlanType(statusResult.value)
|
|
221
|
+
: "unknown";
|
|
222
|
+
const userIdHash = statusResult.status === "fulfilled"
|
|
223
|
+
? parseAntigravityUserIdHash(statusResult.value, normalizeAnalyticsAgentName("Antigravity"))
|
|
224
|
+
: null;
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
entry.planType = planType;
|
|
227
|
+
}
|
|
204
228
|
await writeAntigravityDebugEvent("quota-rpc-response", {
|
|
205
229
|
port: server.port,
|
|
206
|
-
|
|
230
|
+
quotaPath: ANTIGRAVITY_QUOTA_SUMMARY_PATH,
|
|
231
|
+
userStatusPath: ANTIGRAVITY_USER_STATUS_PATH,
|
|
232
|
+
planType,
|
|
207
233
|
entries: entries.map((entry) => ({
|
|
208
234
|
limitId: entry.limitId,
|
|
209
235
|
remainingFraction: entry.remainingFraction,
|
|
@@ -212,13 +238,39 @@ async function collectAntigravityQuotaFromLocalRpc() {
|
|
|
212
238
|
scope: entry.scope,
|
|
213
239
|
modelIds: entry.modelIds
|
|
214
240
|
})),
|
|
215
|
-
...(isAntigravityRawDebugEnabled()
|
|
241
|
+
...(isAntigravityRawDebugEnabled()
|
|
242
|
+
? {
|
|
243
|
+
quotaPayload: quotaResult.value,
|
|
244
|
+
userStatusPayload: statusResult.status === "fulfilled"
|
|
245
|
+
? statusResult.value
|
|
246
|
+
: {
|
|
247
|
+
error: statusResult.reason instanceof Error
|
|
248
|
+
? statusResult.reason.message
|
|
249
|
+
: String(statusResult.reason)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
: {})
|
|
216
253
|
});
|
|
217
254
|
return {
|
|
218
255
|
entries,
|
|
219
|
-
fetchedAt
|
|
256
|
+
fetchedAt,
|
|
257
|
+
userIdHash
|
|
220
258
|
};
|
|
221
259
|
}
|
|
260
|
+
function printAntigravityUserStatusResponse(statusResult) {
|
|
261
|
+
try {
|
|
262
|
+
if (statusResult.status === "fulfilled") {
|
|
263
|
+
console.error("Antigravity user status response:", JSON.stringify(statusResult.value, null, 2));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
console.error("Antigravity user status response error:", statusResult.reason instanceof Error
|
|
267
|
+
? statusResult.reason.message
|
|
268
|
+
: String(statusResult.reason));
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Debug printing must never disturb usage collection.
|
|
272
|
+
}
|
|
273
|
+
}
|
|
222
274
|
function recordsForQuotaWindow(quota, records) {
|
|
223
275
|
if (quota.modelIds.length === 0) {
|
|
224
276
|
return [];
|
|
@@ -458,13 +510,29 @@ async function probeAntigravityPorts(ports, csrfToken) {
|
|
|
458
510
|
async function readAntigravityQuotaSummary(server) {
|
|
459
511
|
return requestAntigravityQuotaSummary(server);
|
|
460
512
|
}
|
|
513
|
+
async function readAntigravityUserStatus(server) {
|
|
514
|
+
return requestAntigravityUserStatus(server);
|
|
515
|
+
}
|
|
461
516
|
function requestAntigravityQuotaSummary(server) {
|
|
462
|
-
|
|
517
|
+
return requestAntigravityRpc(server, ANTIGRAVITY_QUOTA_SUMMARY_PATH, {});
|
|
518
|
+
}
|
|
519
|
+
function requestAntigravityUserStatus(server) {
|
|
520
|
+
return requestAntigravityRpc(server, ANTIGRAVITY_USER_STATUS_PATH, {
|
|
521
|
+
metadata: {
|
|
522
|
+
ideName: "antigravity",
|
|
523
|
+
extensionName: "antigravity",
|
|
524
|
+
ideVersion: "unknown",
|
|
525
|
+
locale: "en"
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function requestAntigravityRpc(server, rpcPath, payload) {
|
|
530
|
+
const body = JSON.stringify(payload);
|
|
463
531
|
return new Promise((resolve, reject) => {
|
|
464
532
|
const request = https.request({
|
|
465
533
|
hostname: "127.0.0.1",
|
|
466
534
|
port: server.port,
|
|
467
|
-
path:
|
|
535
|
+
path: rpcPath,
|
|
468
536
|
method: "POST",
|
|
469
537
|
timeout: 5000,
|
|
470
538
|
agent: antigravityHttpsAgent,
|
|
@@ -481,7 +549,7 @@ function requestAntigravityQuotaSummary(server) {
|
|
|
481
549
|
response.on("end", () => {
|
|
482
550
|
const responseBody = Buffer.concat(chunks).toString("utf8");
|
|
483
551
|
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
|
|
484
|
-
reject(new Error(`Unexpected Antigravity
|
|
552
|
+
reject(new Error(`Unexpected Antigravity RPC response from ${rpcPath}: ${response.statusCode ?? "unknown"}`));
|
|
485
553
|
return;
|
|
486
554
|
}
|
|
487
555
|
try {
|
|
@@ -493,7 +561,7 @@ function requestAntigravityQuotaSummary(server) {
|
|
|
493
561
|
});
|
|
494
562
|
});
|
|
495
563
|
request.on("timeout", () => {
|
|
496
|
-
request.destroy(new Error(
|
|
564
|
+
request.destroy(new Error(`Timed out reading Antigravity RPC ${rpcPath}.`));
|
|
497
565
|
});
|
|
498
566
|
request.on("error", reject);
|
|
499
567
|
request.end(body);
|
|
@@ -550,6 +618,112 @@ export function parseAntigravityQuotaEntries(payload) {
|
|
|
550
618
|
}
|
|
551
619
|
return entries;
|
|
552
620
|
}
|
|
621
|
+
export function parseAntigravityPlanType(payload) {
|
|
622
|
+
const root = asRecord(payload);
|
|
623
|
+
const response = asRecord(root?.response);
|
|
624
|
+
const candidates = [
|
|
625
|
+
readNestedString(root, [
|
|
626
|
+
"userStatus",
|
|
627
|
+
"planStatus",
|
|
628
|
+
"planInfo",
|
|
629
|
+
"planName"
|
|
630
|
+
]),
|
|
631
|
+
readNestedString(root, [
|
|
632
|
+
"userStatus",
|
|
633
|
+
"planStatus",
|
|
634
|
+
"planInfo",
|
|
635
|
+
"planDisplayName"
|
|
636
|
+
]),
|
|
637
|
+
readNestedString(root, [
|
|
638
|
+
"userStatus",
|
|
639
|
+
"planStatus",
|
|
640
|
+
"planName"
|
|
641
|
+
]),
|
|
642
|
+
readNestedString(root, [
|
|
643
|
+
"userStatus",
|
|
644
|
+
"planName"
|
|
645
|
+
]),
|
|
646
|
+
readNestedString(response, [
|
|
647
|
+
"userStatus",
|
|
648
|
+
"planStatus",
|
|
649
|
+
"planInfo",
|
|
650
|
+
"planName"
|
|
651
|
+
]),
|
|
652
|
+
readNestedString(response, [
|
|
653
|
+
"userStatus",
|
|
654
|
+
"planStatus",
|
|
655
|
+
"planInfo",
|
|
656
|
+
"planDisplayName"
|
|
657
|
+
]),
|
|
658
|
+
readNestedString(response, [
|
|
659
|
+
"userStatus",
|
|
660
|
+
"planStatus",
|
|
661
|
+
"planName"
|
|
662
|
+
]),
|
|
663
|
+
readNestedString(response, [
|
|
664
|
+
"userStatus",
|
|
665
|
+
"planName"
|
|
666
|
+
]),
|
|
667
|
+
readNestedString(response, [
|
|
668
|
+
"planStatus",
|
|
669
|
+
"planInfo",
|
|
670
|
+
"planName"
|
|
671
|
+
]),
|
|
672
|
+
readNestedString(response, [
|
|
673
|
+
"planInfo",
|
|
674
|
+
"planName"
|
|
675
|
+
]),
|
|
676
|
+
readNestedString(response, ["planName"]),
|
|
677
|
+
readNestedString(root, ["planName"])
|
|
678
|
+
];
|
|
679
|
+
const rawPlan = candidates.find((value) => Boolean(value));
|
|
680
|
+
return normalizeAntigravityPlanType(rawPlan ?? null);
|
|
681
|
+
}
|
|
682
|
+
export function parseAntigravityUserIdHash(payload, agentName) {
|
|
683
|
+
const root = asRecord(payload);
|
|
684
|
+
const response = asRecord(root?.response);
|
|
685
|
+
const email = readNestedString(root, ["userStatus", "email"]) ??
|
|
686
|
+
readNestedString(response, ["userStatus", "email"]) ??
|
|
687
|
+
readNestedString(root, ["email"]) ??
|
|
688
|
+
readNestedString(response, ["email"]);
|
|
689
|
+
return buildUserIdHash([agentName, email ?? ""]);
|
|
690
|
+
}
|
|
691
|
+
function readNestedString(value, pathParts) {
|
|
692
|
+
let current = value;
|
|
693
|
+
for (const part of pathParts) {
|
|
694
|
+
const record = asRecord(current);
|
|
695
|
+
if (!record) {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
current = record[part];
|
|
699
|
+
}
|
|
700
|
+
return asString(current);
|
|
701
|
+
}
|
|
702
|
+
function normalizeAntigravityPlanType(value) {
|
|
703
|
+
if (!value) {
|
|
704
|
+
return "unknown";
|
|
705
|
+
}
|
|
706
|
+
const normalized = value.toLowerCase();
|
|
707
|
+
if (normalized.includes("ultra")) {
|
|
708
|
+
return "ultra";
|
|
709
|
+
}
|
|
710
|
+
if (normalized.includes("pro") || normalized.includes("premium")) {
|
|
711
|
+
return "pro";
|
|
712
|
+
}
|
|
713
|
+
if (normalized.includes("free") || normalized.includes("standard")) {
|
|
714
|
+
return "free";
|
|
715
|
+
}
|
|
716
|
+
return value;
|
|
717
|
+
}
|
|
718
|
+
function normalizeAnalyticsAgentName(label) {
|
|
719
|
+
return label.replace(/\s+/g, "");
|
|
720
|
+
}
|
|
721
|
+
function buildUserIdHash(parts) {
|
|
722
|
+
if (parts.some((part) => !part)) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
return createHash("md5").update(parts.join("-")).digest("hex");
|
|
726
|
+
}
|
|
553
727
|
function resolveQuotaWindow(window) {
|
|
554
728
|
switch (window) {
|
|
555
729
|
case "5h":
|
|
@@ -28,7 +28,6 @@ const VSCODE_CLAUDE_EXTENSION_PREFIX = "anthropic.claude-code-";
|
|
|
28
28
|
const CLAUDE_SESSION_WINDOW_MINUTES = 5 * 60;
|
|
29
29
|
const CLAUDE_WEEK_WINDOW_MINUTES = 7 * 24 * 60;
|
|
30
30
|
const ANSI_ESCAPE_SEQUENCE = /\u001B\[[0-9;]*[A-Za-z]/g;
|
|
31
|
-
const DEFAULT_CLAUDE_ENTRYPOINTS = ["sdk-cli", "claude"];
|
|
32
31
|
const MONTH_INDEX_BY_LABEL = {
|
|
33
32
|
jan: 0,
|
|
34
33
|
feb: 1,
|
|
@@ -51,18 +50,16 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
51
50
|
constructor(options = {}) {
|
|
52
51
|
super(options.id ?? "claude", options.label ?? "Claude");
|
|
53
52
|
this.root = path.resolve(options.root ?? os.homedir());
|
|
54
|
-
this.entrypoints = new Set(options.entrypoints ?? DEFAULT_CLAUDE_ENTRYPOINTS);
|
|
55
|
-
this.usageCommandKind = options.usageCommandKind ?? "cli";
|
|
56
53
|
this.readUsageCommandOutput = options.readUsageCommandOutput;
|
|
57
54
|
this.readAuthStatusOutput = options.readAuthStatusOutput;
|
|
58
55
|
this.now = options.now ?? (() => new Date());
|
|
59
56
|
}
|
|
60
57
|
async getStats(options = {}) {
|
|
61
|
-
traceClaude(options.traceLogger,
|
|
62
|
-
const resolvedSessionsRoot = await resolveClaudeSessionsRoot(this.root,
|
|
58
|
+
traceClaude(options.traceLogger, `Starting stats collection with root=${this.root} (aggregating all Claude entrypoints).`);
|
|
59
|
+
const resolvedSessionsRoot = await resolveClaudeSessionsRoot(this.root, options.traceLogger);
|
|
63
60
|
const sessionsRoot = resolvedSessionsRoot.rootPath;
|
|
64
61
|
const agentName = normalizeAnalyticsAgentName(this.label);
|
|
65
|
-
const userIdHash = await readClaudeUserIdHash(this.root, this.
|
|
62
|
+
const userIdHash = await readClaudeUserIdHash(this.root, this.readAuthStatusOutput, agentName, options.traceLogger);
|
|
66
63
|
const byModel = new Map();
|
|
67
64
|
const byDay = createDailyUsageAggregates();
|
|
68
65
|
const windows = createLimitWindowAggregates();
|
|
@@ -75,16 +72,17 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
75
72
|
tokenEvents: 0,
|
|
76
73
|
malformedLines: 0
|
|
77
74
|
};
|
|
78
|
-
const parsedSessionFiles = await loadParsedClaudeSessionFiles(sessionsRoot,
|
|
79
|
-
traceClaude(options.traceLogger,
|
|
75
|
+
const parsedSessionFiles = await loadParsedClaudeSessionFiles(sessionsRoot, options.traceLogger);
|
|
76
|
+
traceClaude(options.traceLogger, `Loaded ${parsedSessionFiles.length} parsed session file(s) from ${sessionsRoot}.`);
|
|
80
77
|
for (const file of parsedSessionFiles) {
|
|
81
|
-
const matchingEvents = file.events
|
|
82
|
-
traceClaude(options.traceLogger,
|
|
78
|
+
const matchingEvents = file.events;
|
|
79
|
+
traceClaude(options.traceLogger, [
|
|
83
80
|
`Session file ${describeSessionFilePath(sessionsRoot, file.filePath)}:`,
|
|
84
81
|
`lines=${file.linesRead}`,
|
|
85
82
|
`malformed=${file.malformedLines}`,
|
|
86
83
|
`assistantUsageEvents=${file.events.length}`,
|
|
87
84
|
`matchingEvents=${matchingEvents.length}`,
|
|
85
|
+
`source=${file.sourceKind}`,
|
|
88
86
|
`entrypoints=${summarizeEventCounts(file.events.map((event) => event.entrypoint || "<empty>"))}`,
|
|
89
87
|
`models=${summarizeDistinctValues(file.events.map((event) => event.modelId || "unknown"))}`
|
|
90
88
|
].join(" "));
|
|
@@ -102,7 +100,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
102
100
|
...parsedEvents.keyedEvents.values(),
|
|
103
101
|
...parsedEvents.unkeyedEvents.values()
|
|
104
102
|
];
|
|
105
|
-
traceClaude(options.traceLogger,
|
|
103
|
+
traceClaude(options.traceLogger, [
|
|
106
104
|
`Transcript selection summary: filesWithMatches=${parseTotals.filesScanned}/${parsedSessionFiles.length}`,
|
|
107
105
|
`selectedEvents=${selectedEvents.length}`,
|
|
108
106
|
`duplicateUsageKeys=${parsedEvents.duplicateUsageKeys}`,
|
|
@@ -110,7 +108,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
110
108
|
`duplicateUnkeyedEvents=${parsedEvents.duplicateUnkeyedEvents}`
|
|
111
109
|
].join(" "));
|
|
112
110
|
if (selectedEvents.length === 0 && parsedSessionFiles.length > 0) {
|
|
113
|
-
traceClaude(options.traceLogger,
|
|
111
|
+
traceClaude(options.traceLogger, "No assistant usage events were found in the parsed Claude session files.");
|
|
114
112
|
}
|
|
115
113
|
for (const event of selectedEvents) {
|
|
116
114
|
addModelUsage(byModel, event.modelId, event.totals);
|
|
@@ -149,7 +147,6 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
149
147
|
const [fallbackPrimaryLimitWindows, fallbackSecondaryLimitWindows] = buildWindowLists(windows);
|
|
150
148
|
const liveLimitWindows = await buildLiveLimitWindows({
|
|
151
149
|
root: this.root,
|
|
152
|
-
usageCommandKind: this.usageCommandKind,
|
|
153
150
|
readUsageCommandOutput: this.readUsageCommandOutput,
|
|
154
151
|
readAuthStatusOutput: this.readAuthStatusOutput,
|
|
155
152
|
traceLogger: options.traceLogger,
|
|
@@ -162,7 +159,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
|
|
|
162
159
|
const secondaryLimitWindows = liveLimitWindows.secondaryLimitWindows.length > 0
|
|
163
160
|
? liveLimitWindows.secondaryLimitWindows
|
|
164
161
|
: fallbackSecondaryLimitWindows;
|
|
165
|
-
traceClaude(options.traceLogger,
|
|
162
|
+
traceClaude(options.traceLogger, [
|
|
166
163
|
`Finished stats collection:`,
|
|
167
164
|
`filesScanned=${parseTotals.filesScanned}`,
|
|
168
165
|
`linesRead=${parseTotals.linesRead}`,
|
|
@@ -274,14 +271,14 @@ function resolveClaudeCacheWriteBreakdown(usage) {
|
|
|
274
271
|
function isSessionFile(filePath) {
|
|
275
272
|
return filePath.endsWith(".jsonl");
|
|
276
273
|
}
|
|
277
|
-
async function resolveClaudeSessionsRoot(root,
|
|
274
|
+
async function resolveClaudeSessionsRoot(root, traceLogger) {
|
|
278
275
|
const candidates = buildClaudeSessionsRootCandidates(root);
|
|
279
|
-
traceClaude(traceLogger,
|
|
276
|
+
traceClaude(traceLogger, `Checking ${candidates.length} Claude session root candidate(s).`);
|
|
280
277
|
for (const candidate of candidates) {
|
|
281
278
|
const exists = await isDirectory(candidate.rootPath);
|
|
282
|
-
traceClaude(traceLogger,
|
|
279
|
+
traceClaude(traceLogger, `Session root candidate ${candidate.rootLabel} -> ${candidate.rootPath} (${exists ? "exists" : "missing"}).`);
|
|
283
280
|
if (exists) {
|
|
284
|
-
traceClaude(traceLogger,
|
|
281
|
+
traceClaude(traceLogger, `Selected session root ${candidate.rootLabel} -> ${candidate.rootPath}.`);
|
|
285
282
|
return candidate;
|
|
286
283
|
}
|
|
287
284
|
}
|
|
@@ -289,7 +286,7 @@ async function resolveClaudeSessionsRoot(root, usageCommandKind, traceLogger) {
|
|
|
289
286
|
rootLabel: "~/.claude/projects",
|
|
290
287
|
rootPath: path.join(path.resolve(root), ".claude", "projects")
|
|
291
288
|
};
|
|
292
|
-
traceClaude(traceLogger,
|
|
289
|
+
traceClaude(traceLogger, `No session root candidate exists yet; defaulting to ${fallbackCandidate.rootLabel} -> ${fallbackCandidate.rootPath}.`);
|
|
293
290
|
return fallbackCandidate;
|
|
294
291
|
}
|
|
295
292
|
function buildClaudeSessionsRootCandidates(root) {
|
|
@@ -378,32 +375,38 @@ async function* walkSessionFiles(directory) {
|
|
|
378
375
|
}
|
|
379
376
|
}
|
|
380
377
|
}
|
|
381
|
-
async function loadParsedClaudeSessionFiles(sessionsRoot,
|
|
378
|
+
async function loadParsedClaudeSessionFiles(sessionsRoot, traceLogger) {
|
|
382
379
|
const cacheKey = path.resolve(sessionsRoot);
|
|
383
380
|
const cached = parsedClaudeSessionFilesCache.get(cacheKey);
|
|
384
381
|
if (cached) {
|
|
385
382
|
const files = await cached;
|
|
386
|
-
traceClaude(traceLogger,
|
|
383
|
+
traceClaude(traceLogger, `Session parse cache hit for ${sessionsRoot} (${files.length} file(s)).`);
|
|
387
384
|
return files;
|
|
388
385
|
}
|
|
389
386
|
const pending = (async () => {
|
|
390
387
|
const files = [];
|
|
391
|
-
traceClaude(traceLogger,
|
|
388
|
+
traceClaude(traceLogger, `Scanning session files under ${sessionsRoot}.`);
|
|
392
389
|
for await (const filePath of walkSessionFiles(sessionsRoot)) {
|
|
393
|
-
files.push(await parseSessionFile(filePath));
|
|
390
|
+
files.push(await parseSessionFile(filePath, sessionsRoot));
|
|
394
391
|
}
|
|
395
|
-
|
|
392
|
+
inferClaudeSessionFileSources(files);
|
|
393
|
+
traceClaude(traceLogger, `Completed session file scan under ${sessionsRoot}: ${files.length} file(s) parsed.`);
|
|
396
394
|
return files;
|
|
397
395
|
})();
|
|
398
396
|
parsedClaudeSessionFilesCache.set(cacheKey, pending);
|
|
399
397
|
return pending;
|
|
400
398
|
}
|
|
401
|
-
async function parseSessionFile(filePath) {
|
|
399
|
+
async function parseSessionFile(filePath, sessionsRoot) {
|
|
402
400
|
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
403
401
|
const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
404
402
|
let linesRead = 0;
|
|
405
403
|
let malformedLines = 0;
|
|
406
404
|
const events = [];
|
|
405
|
+
const assistantEntryPoints = new Set();
|
|
406
|
+
let hasIdeOpenedFileAttachment = false;
|
|
407
|
+
let hasIdeOpenedFileMarker = false;
|
|
408
|
+
let hasIdeTooling = false;
|
|
409
|
+
let hasQueueOperations = false;
|
|
407
410
|
for await (const line of lineReader) {
|
|
408
411
|
linesRead += 1;
|
|
409
412
|
if (!line.trim()) {
|
|
@@ -417,6 +420,19 @@ async function parseSessionFile(filePath) {
|
|
|
417
420
|
malformedLines += 1;
|
|
418
421
|
continue;
|
|
419
422
|
}
|
|
423
|
+
if (payloadObject.type === "queue-operation") {
|
|
424
|
+
hasQueueOperations = true;
|
|
425
|
+
}
|
|
426
|
+
if (messageContainsIdeOpenedFileMarker(asRecord(payloadObject.message))) {
|
|
427
|
+
hasIdeOpenedFileMarker = true;
|
|
428
|
+
}
|
|
429
|
+
const attachment = asRecord(payloadObject.attachment);
|
|
430
|
+
if (attachment?.type === "opened_file_in_ide") {
|
|
431
|
+
hasIdeOpenedFileAttachment = true;
|
|
432
|
+
}
|
|
433
|
+
if (attachmentHasIdeTooling(attachment)) {
|
|
434
|
+
hasIdeTooling = true;
|
|
435
|
+
}
|
|
420
436
|
if (payloadObject.type !== "assistant") {
|
|
421
437
|
continue;
|
|
422
438
|
}
|
|
@@ -427,12 +443,14 @@ async function parseSessionFile(filePath) {
|
|
|
427
443
|
}
|
|
428
444
|
const modelId = String(message?.model ?? "unknown");
|
|
429
445
|
const eventTimeMs = Date.parse(String(payloadObject.timestamp ?? ""));
|
|
446
|
+
const entrypoint = typeof payloadObject.entrypoint === "string" ? payloadObject.entrypoint : "";
|
|
430
447
|
const rateLimits = extractRateLimits(payloadObject, message);
|
|
431
448
|
const normalizedUsage = normalizeUsage(usage);
|
|
432
449
|
const usageKey = buildUsageEventKey(payloadObject, message);
|
|
433
450
|
const usageSignature = buildUsageSignature(payloadObject, modelId, normalizedUsage);
|
|
451
|
+
assistantEntryPoints.add(entrypoint);
|
|
434
452
|
events.push({
|
|
435
|
-
entrypoint
|
|
453
|
+
entrypoint,
|
|
436
454
|
usageKey,
|
|
437
455
|
usageSignature,
|
|
438
456
|
timestampMs: eventTimeMs,
|
|
@@ -441,7 +459,101 @@ async function parseSessionFile(filePath) {
|
|
|
441
459
|
rateLimits
|
|
442
460
|
});
|
|
443
461
|
}
|
|
444
|
-
return {
|
|
462
|
+
return {
|
|
463
|
+
filePath,
|
|
464
|
+
sessionGroupKey: buildClaudeSessionGroupKey(sessionsRoot, filePath),
|
|
465
|
+
linesRead,
|
|
466
|
+
malformedLines,
|
|
467
|
+
sourceKind: "unknown",
|
|
468
|
+
sourceReason: "unclassified",
|
|
469
|
+
signals: {
|
|
470
|
+
assistantEntryPoints: [...assistantEntryPoints].sort(),
|
|
471
|
+
hasIdeOpenedFileAttachment,
|
|
472
|
+
hasIdeOpenedFileMarker,
|
|
473
|
+
hasIdeTooling,
|
|
474
|
+
hasQueueOperations
|
|
475
|
+
},
|
|
476
|
+
events
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function buildClaudeSessionGroupKey(sessionsRoot, filePath) {
|
|
480
|
+
const relativePath = path.relative(sessionsRoot, filePath);
|
|
481
|
+
if (!relativePath || relativePath.startsWith("..")) {
|
|
482
|
+
return filePath;
|
|
483
|
+
}
|
|
484
|
+
const normalizedRelativePath = relativePath.split(path.sep).join("/");
|
|
485
|
+
const subagentMatch = normalizedRelativePath.match(/^(.*\/[^/]+)\/subagents\/[^/]+\.jsonl$/);
|
|
486
|
+
if (subagentMatch?.[1]) {
|
|
487
|
+
return subagentMatch[1];
|
|
488
|
+
}
|
|
489
|
+
return normalizedRelativePath.replace(/\.jsonl$/i, "");
|
|
490
|
+
}
|
|
491
|
+
function inferClaudeSessionFileSources(files) {
|
|
492
|
+
const groups = new Map();
|
|
493
|
+
for (const file of files) {
|
|
494
|
+
const group = groups.get(file.sessionGroupKey) ?? {
|
|
495
|
+
assistantEntryPoints: new Set(),
|
|
496
|
+
hasIdeHints: false
|
|
497
|
+
};
|
|
498
|
+
for (const entrypoint of file.signals.assistantEntryPoints) {
|
|
499
|
+
group.assistantEntryPoints.add(entrypoint);
|
|
500
|
+
}
|
|
501
|
+
group.hasIdeHints =
|
|
502
|
+
group.hasIdeHints ||
|
|
503
|
+
file.signals.hasIdeOpenedFileAttachment ||
|
|
504
|
+
file.signals.hasIdeOpenedFileMarker ||
|
|
505
|
+
file.signals.hasIdeTooling ||
|
|
506
|
+
file.signals.hasQueueOperations;
|
|
507
|
+
groups.set(file.sessionGroupKey, group);
|
|
508
|
+
}
|
|
509
|
+
for (const file of files) {
|
|
510
|
+
const group = groups.get(file.sessionGroupKey);
|
|
511
|
+
const { kind, reason } = classifyClaudeSessionGroup(group);
|
|
512
|
+
file.sourceKind = kind;
|
|
513
|
+
file.sourceReason = reason;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function classifyClaudeSessionGroup(group) {
|
|
517
|
+
if (!group) {
|
|
518
|
+
return { kind: "unknown", reason: "missing session group signals" };
|
|
519
|
+
}
|
|
520
|
+
if (group.assistantEntryPoints.has("claude-vscode")) {
|
|
521
|
+
return { kind: "vscode", reason: "explicit claude-vscode entrypoint" };
|
|
522
|
+
}
|
|
523
|
+
if (group.assistantEntryPoints.has("sdk-cli") || group.assistantEntryPoints.has("claude")) {
|
|
524
|
+
return { kind: "cli", reason: "explicit sdk-cli/claude entrypoint" };
|
|
525
|
+
}
|
|
526
|
+
if (group.assistantEntryPoints.has("cli")) {
|
|
527
|
+
return group.hasIdeHints
|
|
528
|
+
? { kind: "vscode", reason: "generic cli entrypoint with IDE session hints" }
|
|
529
|
+
: { kind: "cli", reason: "generic cli entrypoint without IDE session hints" };
|
|
530
|
+
}
|
|
531
|
+
return { kind: "unknown", reason: "no assistant entrypoints" };
|
|
532
|
+
}
|
|
533
|
+
function attachmentHasIdeTooling(attachment) {
|
|
534
|
+
if (attachment?.type !== "deferred_tools_delta") {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
return extractStringArray(attachment.addedNames).some((name) => name.startsWith("mcp__ide__"));
|
|
538
|
+
}
|
|
539
|
+
function messageContainsIdeOpenedFileMarker(message) {
|
|
540
|
+
const content = message?.content;
|
|
541
|
+
if (typeof content === "string") {
|
|
542
|
+
return content.includes("<ide_opened_file>");
|
|
543
|
+
}
|
|
544
|
+
if (!Array.isArray(content)) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
return content.some((item) => {
|
|
548
|
+
const contentItem = asRecord(item);
|
|
549
|
+
return typeof contentItem?.text === "string" && contentItem.text.includes("<ide_opened_file>");
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function extractStringArray(value) {
|
|
553
|
+
if (!Array.isArray(value)) {
|
|
554
|
+
return [];
|
|
555
|
+
}
|
|
556
|
+
return value.filter((item) => typeof item === "string");
|
|
445
557
|
}
|
|
446
558
|
function buildUsageEventKey(payloadObject, message) {
|
|
447
559
|
const sessionId = String(payloadObject.sessionId ?? "");
|
|
@@ -515,12 +627,11 @@ function normalizeTimestamp(value) {
|
|
|
515
627
|
function extractRateLimits(payloadObject, message) {
|
|
516
628
|
return asRecord(payloadObject.rate_limits) ?? asRecord(message?.rate_limits);
|
|
517
629
|
}
|
|
518
|
-
function traceClaude(traceLogger,
|
|
630
|
+
function traceClaude(traceLogger, message) {
|
|
519
631
|
if (!traceLogger) {
|
|
520
632
|
return;
|
|
521
633
|
}
|
|
522
|
-
|
|
523
|
-
traceLogger.log(`[${targetLabel}] ${message}`);
|
|
634
|
+
traceLogger.log(`[Claude] ${message}`);
|
|
524
635
|
}
|
|
525
636
|
function formatErrorMessage(error) {
|
|
526
637
|
if (!error) {
|
|
@@ -569,9 +680,6 @@ function summarizeDistinctValues(values, limit = 5) {
|
|
|
569
680
|
? `${visibleValues.join(", ")} (+${remainder} more)`
|
|
570
681
|
: visibleValues.join(", ");
|
|
571
682
|
}
|
|
572
|
-
function collectEntryPoints(files) {
|
|
573
|
-
return files.flatMap((file) => file.events.map((event) => event.entrypoint || "<empty>"));
|
|
574
|
-
}
|
|
575
683
|
function buildClaudeCommandEnvironment() {
|
|
576
684
|
return {
|
|
577
685
|
...process.env,
|
|
@@ -580,16 +688,16 @@ function buildClaudeCommandEnvironment() {
|
|
|
580
688
|
}
|
|
581
689
|
async function buildLiveLimitWindows(options) {
|
|
582
690
|
const [usageOutput, subscriptionType] = await Promise.all([
|
|
583
|
-
readClaudeUsageCommandOutput(options.root, options.
|
|
584
|
-
readClaudeSubscriptionType(options.root, options.
|
|
691
|
+
readClaudeUsageCommandOutput(options.root, options.readUsageCommandOutput, options.traceLogger),
|
|
692
|
+
readClaudeSubscriptionType(options.root, options.readAuthStatusOutput, options.traceLogger)
|
|
585
693
|
]);
|
|
586
694
|
const snapshots = parseLiveUsageWindowSnapshots(usageOutput, options.now);
|
|
587
|
-
traceClaude(options.traceLogger,
|
|
695
|
+
traceClaude(options.traceLogger, `Parsed ${snapshots.length} live usage snapshot(s) from /usage output.`);
|
|
588
696
|
if (snapshots.length === 0) {
|
|
589
|
-
traceClaude(options.traceLogger,
|
|
697
|
+
traceClaude(options.traceLogger, "No live usage snapshots matched the expected /usage format.");
|
|
590
698
|
}
|
|
591
699
|
const resolvedPlanType = subscriptionType || "live";
|
|
592
|
-
traceClaude(options.traceLogger,
|
|
700
|
+
traceClaude(options.traceLogger, `Resolved live plan type ${resolvedPlanType}.`);
|
|
593
701
|
const primaryLimitWindows = snapshots
|
|
594
702
|
.filter((snapshot) => snapshot.scope === "primary")
|
|
595
703
|
.map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now));
|
|
@@ -604,7 +712,7 @@ async function buildLiveLimitWindows(options) {
|
|
|
604
712
|
if (!row) {
|
|
605
713
|
continue;
|
|
606
714
|
}
|
|
607
|
-
traceClaude(options.traceLogger,
|
|
715
|
+
traceClaude(options.traceLogger, [
|
|
608
716
|
`Live window ${snapshot.scope}/${snapshot.label}:`,
|
|
609
717
|
`used=${snapshot.usedPercent}%`,
|
|
610
718
|
`range=${row.startTimeUtcIso}->${row.endTimeUtcIso}`,
|
|
@@ -620,41 +728,41 @@ async function buildLiveLimitWindows(options) {
|
|
|
620
728
|
secondaryLimitWindows
|
|
621
729
|
};
|
|
622
730
|
}
|
|
623
|
-
async function readClaudeSubscriptionType(root,
|
|
624
|
-
const output = await readClaudeAuthStatusOutput(root,
|
|
731
|
+
async function readClaudeSubscriptionType(root, override, traceLogger) {
|
|
732
|
+
const output = await readClaudeAuthStatusOutput(root, override, traceLogger);
|
|
625
733
|
const subscriptionType = parseClaudeSubscriptionType(output);
|
|
626
734
|
if (output && !subscriptionType) {
|
|
627
|
-
traceClaude(traceLogger,
|
|
735
|
+
traceClaude(traceLogger, "Could not parse subscription type from auth status output.");
|
|
628
736
|
}
|
|
629
|
-
traceClaude(traceLogger,
|
|
737
|
+
traceClaude(traceLogger, `Subscription type result: ${subscriptionType ?? "<none>"}.`);
|
|
630
738
|
return subscriptionType;
|
|
631
739
|
}
|
|
632
|
-
async function readClaudeAuthStatusOutput(root,
|
|
740
|
+
async function readClaudeAuthStatusOutput(root, override, traceLogger) {
|
|
633
741
|
if (override) {
|
|
634
742
|
try {
|
|
635
743
|
const output = await override();
|
|
636
|
-
traceClaude(traceLogger,
|
|
744
|
+
traceClaude(traceLogger, "Using injected auth status output override.");
|
|
637
745
|
return output;
|
|
638
746
|
}
|
|
639
747
|
catch {
|
|
640
|
-
traceClaude(traceLogger,
|
|
748
|
+
traceClaude(traceLogger, "Injected auth status output override failed.");
|
|
641
749
|
return null;
|
|
642
750
|
}
|
|
643
751
|
}
|
|
644
|
-
const cacheKey =
|
|
752
|
+
const cacheKey = path.resolve(root);
|
|
645
753
|
const cached = claudeAuthStatusOutputCache.get(cacheKey);
|
|
646
754
|
if (cached) {
|
|
647
|
-
traceClaude(traceLogger,
|
|
755
|
+
traceClaude(traceLogger, "Auth status output cache hit.");
|
|
648
756
|
return cached;
|
|
649
757
|
}
|
|
650
758
|
const pending = (async () => {
|
|
651
|
-
const binaryPath = await resolveClaudeBinaryPath(root,
|
|
759
|
+
const binaryPath = await resolveClaudeBinaryPath(root, traceLogger);
|
|
652
760
|
if (!binaryPath) {
|
|
653
|
-
traceClaude(traceLogger,
|
|
761
|
+
traceClaude(traceLogger, "Skipping auth status command because no Claude binary was found.");
|
|
654
762
|
return null;
|
|
655
763
|
}
|
|
656
764
|
try {
|
|
657
|
-
traceClaude(traceLogger,
|
|
765
|
+
traceClaude(traceLogger, `Running auth status command with ${binaryPath} (TZ=UTC).`);
|
|
658
766
|
const { stdout, stderr } = await execFileAsync(binaryPath, ["auth", "status"], {
|
|
659
767
|
encoding: "utf8",
|
|
660
768
|
env: buildClaudeCommandEnvironment(),
|
|
@@ -663,12 +771,12 @@ async function readClaudeAuthStatusOutput(root, usageCommandKind, override, trac
|
|
|
663
771
|
windowsHide: true
|
|
664
772
|
});
|
|
665
773
|
const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
666
|
-
traceClaude(traceLogger,
|
|
774
|
+
traceClaude(traceLogger, "Auth status command completed successfully.");
|
|
667
775
|
return combined || null;
|
|
668
776
|
}
|
|
669
777
|
catch (error) {
|
|
670
778
|
const combined = extractExecOutput(error);
|
|
671
|
-
traceClaude(traceLogger,
|
|
779
|
+
traceClaude(traceLogger, `Auth status command failed: ${formatErrorMessage(error)}.`);
|
|
672
780
|
return combined || null;
|
|
673
781
|
}
|
|
674
782
|
})();
|
|
@@ -705,43 +813,43 @@ function parseClaudeAuthStatusSnapshot(output) {
|
|
|
705
813
|
return null;
|
|
706
814
|
}
|
|
707
815
|
}
|
|
708
|
-
async function readClaudeUserIdHash(root,
|
|
709
|
-
const authStatusOutput = await readClaudeAuthStatusOutput(root,
|
|
816
|
+
async function readClaudeUserIdHash(root, override, agentName, traceLogger) {
|
|
817
|
+
const authStatusOutput = await readClaudeAuthStatusOutput(root, override, traceLogger);
|
|
710
818
|
const snapshot = parseClaudeAuthStatusSnapshot(authStatusOutput);
|
|
711
819
|
if (!snapshot) {
|
|
712
|
-
traceClaude(traceLogger,
|
|
820
|
+
traceClaude(traceLogger, "Auth status output did not yield an analytics identity snapshot.");
|
|
713
821
|
return null;
|
|
714
822
|
}
|
|
715
823
|
return buildUserIdHash([agentName, snapshot.email, snapshot.orgId, snapshot.orgName]);
|
|
716
824
|
}
|
|
717
|
-
async function readClaudeUsageCommandOutput(root,
|
|
825
|
+
async function readClaudeUsageCommandOutput(root, override, traceLogger) {
|
|
718
826
|
if (override) {
|
|
719
827
|
try {
|
|
720
828
|
const output = await override();
|
|
721
|
-
traceClaude(traceLogger,
|
|
722
|
-
traceClaude(traceLogger,
|
|
829
|
+
traceClaude(traceLogger, "Using injected /usage output override.");
|
|
830
|
+
traceClaude(traceLogger, `Usage returned:\n${describeUsageOutput(output)}`);
|
|
723
831
|
return output;
|
|
724
832
|
}
|
|
725
833
|
catch {
|
|
726
|
-
traceClaude(traceLogger,
|
|
834
|
+
traceClaude(traceLogger, "Injected /usage output override failed.");
|
|
727
835
|
return null;
|
|
728
836
|
}
|
|
729
837
|
}
|
|
730
|
-
const cacheKey =
|
|
838
|
+
const cacheKey = path.resolve(root);
|
|
731
839
|
const cached = claudeUsageOutputCache.get(cacheKey);
|
|
732
840
|
if (cached) {
|
|
733
|
-
traceClaude(traceLogger,
|
|
841
|
+
traceClaude(traceLogger, "Usage output cache hit.");
|
|
734
842
|
return cached;
|
|
735
843
|
}
|
|
736
844
|
const pending = (async () => {
|
|
737
|
-
const binaryPath = await resolveClaudeBinaryPath(root,
|
|
845
|
+
const binaryPath = await resolveClaudeBinaryPath(root, traceLogger);
|
|
738
846
|
if (!binaryPath) {
|
|
739
|
-
traceClaude(traceLogger,
|
|
740
|
-
traceClaude(traceLogger,
|
|
847
|
+
traceClaude(traceLogger, "Skipping /usage command because no Claude binary was found.");
|
|
848
|
+
traceClaude(traceLogger, "Usage returned:\n<not available>");
|
|
741
849
|
return null;
|
|
742
850
|
}
|
|
743
851
|
try {
|
|
744
|
-
traceClaude(traceLogger,
|
|
852
|
+
traceClaude(traceLogger, `Running /usage command with ${binaryPath} (TZ=UTC).`);
|
|
745
853
|
const { stdout, stderr } = await execFileAsync(binaryPath, ["-p", "/usage"], {
|
|
746
854
|
encoding: "utf8",
|
|
747
855
|
env: buildClaudeCommandEnvironment(),
|
|
@@ -750,14 +858,14 @@ async function readClaudeUsageCommandOutput(root, usageCommandKind, override, tr
|
|
|
750
858
|
windowsHide: true
|
|
751
859
|
});
|
|
752
860
|
const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
753
|
-
traceClaude(traceLogger,
|
|
754
|
-
traceClaude(traceLogger,
|
|
861
|
+
traceClaude(traceLogger, "Usage command completed successfully.");
|
|
862
|
+
traceClaude(traceLogger, `Usage returned:\n${describeUsageOutput(combined || null)}`);
|
|
755
863
|
return combined || null;
|
|
756
864
|
}
|
|
757
865
|
catch (error) {
|
|
758
866
|
const combined = extractExecOutput(error);
|
|
759
|
-
traceClaude(traceLogger,
|
|
760
|
-
traceClaude(traceLogger,
|
|
867
|
+
traceClaude(traceLogger, `Usage command failed: ${formatErrorMessage(error)}.`);
|
|
868
|
+
traceClaude(traceLogger, `Usage returned:\n${describeUsageOutput(combined || null)}`);
|
|
761
869
|
return combined || null;
|
|
762
870
|
}
|
|
763
871
|
})();
|
|
@@ -772,68 +880,68 @@ function extractExecOutput(error) {
|
|
|
772
880
|
const stderr = typeof error.stderr === "string" ? error.stderr : "";
|
|
773
881
|
return [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
774
882
|
}
|
|
775
|
-
async function resolveClaudeBinaryPath(root,
|
|
776
|
-
const cacheKey =
|
|
883
|
+
async function resolveClaudeBinaryPath(root, traceLogger) {
|
|
884
|
+
const cacheKey = path.resolve(root);
|
|
777
885
|
const cached = claudeBinaryPathCache.get(cacheKey);
|
|
778
886
|
if (cached) {
|
|
779
887
|
const binaryPath = await cached;
|
|
780
|
-
traceClaude(traceLogger,
|
|
888
|
+
traceClaude(traceLogger, `Binary detection cache hit: ${binaryPath ? `found ${binaryPath}` : "not found"}.`);
|
|
781
889
|
return binaryPath;
|
|
782
890
|
}
|
|
783
891
|
const pending = (async () => {
|
|
784
|
-
traceClaude(traceLogger,
|
|
785
|
-
const binaryPath =
|
|
786
|
-
|
|
787
|
-
: await resolveCliClaudeBinaryPath(root, traceLogger);
|
|
788
|
-
traceClaude(traceLogger, usageCommandKind, `Binary detection result: ${binaryPath ? `found ${binaryPath}` : "not found"}.`);
|
|
892
|
+
traceClaude(traceLogger, `Starting binary detection under ${root}.`);
|
|
893
|
+
const binaryPath = await resolveMergedClaudeBinaryPath(root, traceLogger);
|
|
894
|
+
traceClaude(traceLogger, `Binary detection result: ${binaryPath ? `found ${binaryPath}` : "not found"}.`);
|
|
789
895
|
return binaryPath;
|
|
790
896
|
})();
|
|
791
897
|
claudeBinaryPathCache.set(cacheKey, pending);
|
|
792
898
|
return pending;
|
|
793
899
|
}
|
|
794
|
-
async function
|
|
900
|
+
async function resolveMergedClaudeBinaryPath(root, traceLogger) {
|
|
901
|
+
const candidates = [
|
|
902
|
+
...(await resolveVsCodeClaudeBinaryCandidates(root, traceLogger)),
|
|
903
|
+
...resolveDirectClaudeBinaryCandidates(root)
|
|
904
|
+
];
|
|
905
|
+
for (const candidate of candidates) {
|
|
906
|
+
const accessCheck = await checkReadableExecutableFile(candidate);
|
|
907
|
+
traceClaude(traceLogger, `Checked ${candidate} -> ${accessCheck.ok ? "success" : `failure (${accessCheck.errorMessage ?? "unknown"})`}.`);
|
|
908
|
+
if (accessCheck.ok) {
|
|
909
|
+
return candidate;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
async function resolveVsCodeClaudeBinaryCandidates(root, traceLogger) {
|
|
795
915
|
const boosterDirectories = [
|
|
796
916
|
path.join(root, ".vscode", "extensions"),
|
|
797
917
|
path.join(root, ".vscode-server", "extensions"),
|
|
798
918
|
path.join(root, ".vscode-server-insiders", "extensions")
|
|
799
919
|
];
|
|
800
|
-
|
|
920
|
+
const candidates = [];
|
|
801
921
|
for (const directory of boosterDirectories) {
|
|
802
|
-
|
|
803
|
-
if (!firstFoundPath && binaryPath) {
|
|
804
|
-
firstFoundPath = binaryPath;
|
|
805
|
-
}
|
|
922
|
+
candidates.push(...(await resolveClaudeBinaryCandidatesFromExtensionDirectory(directory, traceLogger)));
|
|
806
923
|
}
|
|
807
|
-
return
|
|
924
|
+
return candidates;
|
|
808
925
|
}
|
|
809
|
-
async function
|
|
926
|
+
async function resolveClaudeBinaryCandidatesFromExtensionDirectory(directory, traceLogger) {
|
|
810
927
|
let entries;
|
|
811
|
-
traceClaude(traceLogger,
|
|
928
|
+
traceClaude(traceLogger, `Scanning extension directory ${directory}.`);
|
|
812
929
|
try {
|
|
813
930
|
entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
814
931
|
}
|
|
815
932
|
catch (error) {
|
|
816
|
-
traceClaude(traceLogger,
|
|
817
|
-
return
|
|
933
|
+
traceClaude(traceLogger, `Could not read ${directory}: ${formatErrorMessage(error)}.`);
|
|
934
|
+
return [];
|
|
818
935
|
}
|
|
819
936
|
const candidates = entries
|
|
820
937
|
.filter((entry) => entry.isDirectory() && entry.name.startsWith(VSCODE_CLAUDE_EXTENSION_PREFIX))
|
|
821
938
|
.map((entry) => entry.name)
|
|
822
939
|
.sort(compareClaudeExtensionDirectoryNames);
|
|
823
940
|
if (candidates.length === 0) {
|
|
824
|
-
traceClaude(traceLogger,
|
|
825
|
-
return
|
|
826
|
-
}
|
|
827
|
-
let firstFoundPath = null;
|
|
828
|
-
for (const candidate of candidates) {
|
|
829
|
-
const binaryPath = path.join(directory, candidate, "resources", "native-binary", "claude");
|
|
830
|
-
const accessCheck = await checkReadableExecutableFile(binaryPath);
|
|
831
|
-
traceClaude(traceLogger, "vscode", `Checked ${binaryPath} -> ${accessCheck.ok ? "success" : `failure (${accessCheck.errorMessage ?? "unknown"})`}.`);
|
|
832
|
-
if (!firstFoundPath && accessCheck.ok) {
|
|
833
|
-
firstFoundPath = binaryPath;
|
|
834
|
-
}
|
|
941
|
+
traceClaude(traceLogger, `No Claude VSCode extension candidates found in ${directory}.`);
|
|
942
|
+
return [];
|
|
835
943
|
}
|
|
836
|
-
return
|
|
944
|
+
return candidates.map((candidate) => path.join(directory, candidate, "resources", "native-binary", "claude"));
|
|
837
945
|
}
|
|
838
946
|
function compareClaudeExtensionDirectoryNames(left, right) {
|
|
839
947
|
const leftVersion = extractClaudeExtensionVersion(left);
|
|
@@ -857,20 +965,11 @@ function extractClaudeExtensionVersion(directoryName) {
|
|
|
857
965
|
.map((part) => Number(part))
|
|
858
966
|
.filter((part) => Number.isFinite(part));
|
|
859
967
|
}
|
|
860
|
-
|
|
861
|
-
|
|
968
|
+
function resolveDirectClaudeBinaryCandidates(root) {
|
|
969
|
+
return [
|
|
862
970
|
path.join(root, ".local", "bin", "claude"),
|
|
863
971
|
path.join(root, "bin", "claude")
|
|
864
972
|
];
|
|
865
|
-
let firstFoundPath = null;
|
|
866
|
-
for (const candidate of directCandidates) {
|
|
867
|
-
const accessCheck = await checkReadableExecutableFile(candidate);
|
|
868
|
-
traceClaude(traceLogger, "cli", `Checked ${candidate} -> ${accessCheck.ok ? "success" : `failure (${accessCheck.errorMessage ?? "unknown"})`}.`);
|
|
869
|
-
if (!firstFoundPath && accessCheck.ok) {
|
|
870
|
-
firstFoundPath = candidate;
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
return firstFoundPath;
|
|
874
973
|
}
|
|
875
974
|
async function checkReadableExecutableFile(filePath) {
|
|
876
975
|
try {
|
|
@@ -6,12 +6,6 @@ export function createProviders() {
|
|
|
6
6
|
return [
|
|
7
7
|
new CodexUsageProvider(),
|
|
8
8
|
new ClaudeUsageProvider(),
|
|
9
|
-
new ClaudeUsageProvider({
|
|
10
|
-
id: "claude-vscode",
|
|
11
|
-
label: "Claude VSCode",
|
|
12
|
-
entrypoints: ["claude-vscode"],
|
|
13
|
-
usageCommandKind: "vscode"
|
|
14
|
-
}),
|
|
15
9
|
new CopilotUsageProvider(),
|
|
16
10
|
new AntigravityUsageProvider()
|
|
17
11
|
];
|
|
@@ -29,6 +29,7 @@ export async function buildAnonymousUsagePayload(statsList) {
|
|
|
29
29
|
function buildAnonymousUsageReport(stats, window, letmecodeVersion) {
|
|
30
30
|
return {
|
|
31
31
|
agent: stats.analytics?.agentName ?? stats.providerLabel.replace(/\s+/g, ""),
|
|
32
|
+
model_type: resolveReportModelType(stats, window),
|
|
32
33
|
userid_hash: stats.analytics?.userIdHash ?? "",
|
|
33
34
|
plan_id: window.planType,
|
|
34
35
|
window_duration_seconds: window.windowMinutes * 60,
|
|
@@ -41,6 +42,31 @@ function buildAnonymousUsageReport(stats, window, letmecodeVersion) {
|
|
|
41
42
|
letmecode_version: letmecodeVersion
|
|
42
43
|
};
|
|
43
44
|
}
|
|
45
|
+
function resolveReportModelType(stats, window) {
|
|
46
|
+
if (stats.providerId === "antigravity") {
|
|
47
|
+
return resolveAntigravityReportModelType(stats, window);
|
|
48
|
+
}
|
|
49
|
+
if (window.limitId && window.limitId !== "unknown") {
|
|
50
|
+
return truncateSchemaString(window.limitId, 128);
|
|
51
|
+
}
|
|
52
|
+
if (window.modelUsage.length === 1) {
|
|
53
|
+
return truncateSchemaString(window.modelUsage[0]?.modelId ?? stats.providerId, 128);
|
|
54
|
+
}
|
|
55
|
+
return truncateSchemaString(stats.providerId, 128);
|
|
56
|
+
}
|
|
57
|
+
function resolveAntigravityReportModelType(stats, window) {
|
|
58
|
+
const limitId = window.limitId.toLowerCase();
|
|
59
|
+
if (limitId.includes("gemini")) {
|
|
60
|
+
return "gemini";
|
|
61
|
+
}
|
|
62
|
+
if (limitId.startsWith("3p") || limitId.includes("third-party")) {
|
|
63
|
+
return "third-party";
|
|
64
|
+
}
|
|
65
|
+
return truncateSchemaString(window.limitId || stats.providerId, 128);
|
|
66
|
+
}
|
|
67
|
+
function truncateSchemaString(value, maxLength) {
|
|
68
|
+
return value.length > maxLength ? value.slice(0, maxLength) : value;
|
|
69
|
+
}
|
|
44
70
|
function buildUsageRaw(modelUsage) {
|
|
45
71
|
const usageRaw = {};
|
|
46
72
|
for (const row of modelUsage) {
|
|
@@ -61,7 +87,10 @@ function resolveReportedUsedPercents(window) {
|
|
|
61
87
|
return clampPercent(window.maxUsedPercent - window.minUsedPercent);
|
|
62
88
|
}
|
|
63
89
|
function clampPercent(value) {
|
|
64
|
-
|
|
90
|
+
if (!Number.isFinite(value)) {
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
return Math.max(0, Math.min(100, value));
|
|
65
94
|
}
|
|
66
95
|
function roundDollars(value) {
|
|
67
96
|
return Number(value.toFixed(6));
|