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 CLI SDK and Claude VSCode detection details,",
65
- " session root selection, parsed session file summaries, entrypoint matching,",
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 payload = await readAntigravityQuotaSummary(server);
203
- const entries = parseAntigravityQuotaEntries(payload);
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
- path: ANTIGRAVITY_QUOTA_SUMMARY_PATH,
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() ? { payload } : {})
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
- const body = "{}";
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: ANTIGRAVITY_QUOTA_SUMMARY_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 quota summary response: ${response.statusCode ?? "unknown"}`));
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("Timed out reading Antigravity quota summary."));
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, this.usageCommandKind, `Starting stats collection with root=${this.root} entrypoints=[${[...this.entrypoints].join(", ")}].`);
62
- const resolvedSessionsRoot = await resolveClaudeSessionsRoot(this.root, this.usageCommandKind, options.traceLogger);
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.usageCommandKind, this.readAuthStatusOutput, agentName, options.traceLogger);
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, this.usageCommandKind, options.traceLogger);
79
- traceClaude(options.traceLogger, this.usageCommandKind, `Loaded ${parsedSessionFiles.length} parsed session file(s) from ${sessionsRoot}.`);
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.filter((event) => this.entrypoints.has(event.entrypoint));
82
- traceClaude(options.traceLogger, this.usageCommandKind, [
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, this.usageCommandKind, [
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, this.usageCommandKind, `No transcript usage matched entrypoints [${[...this.entrypoints].join(", ")}]. Observed entrypoints=${summarizeEventCounts(collectEntryPoints(parsedSessionFiles))}.`);
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, this.usageCommandKind, [
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, usageCommandKind, traceLogger) {
274
+ async function resolveClaudeSessionsRoot(root, traceLogger) {
278
275
  const candidates = buildClaudeSessionsRootCandidates(root);
279
- traceClaude(traceLogger, usageCommandKind, `Checking ${candidates.length} Claude session root candidate(s).`);
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, usageCommandKind, `Session root candidate ${candidate.rootLabel} -> ${candidate.rootPath} (${exists ? "exists" : "missing"}).`);
279
+ traceClaude(traceLogger, `Session root candidate ${candidate.rootLabel} -> ${candidate.rootPath} (${exists ? "exists" : "missing"}).`);
283
280
  if (exists) {
284
- traceClaude(traceLogger, usageCommandKind, `Selected session root ${candidate.rootLabel} -> ${candidate.rootPath}.`);
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, usageCommandKind, `No session root candidate exists yet; defaulting to ${fallbackCandidate.rootLabel} -> ${fallbackCandidate.rootPath}.`);
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, usageCommandKind, traceLogger) {
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, usageCommandKind, `Session parse cache hit for ${sessionsRoot} (${files.length} file(s)).`);
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, usageCommandKind, `Scanning session files under ${sessionsRoot}.`);
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
- traceClaude(traceLogger, usageCommandKind, `Completed session file scan under ${sessionsRoot}: ${files.length} file(s) parsed.`);
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: typeof payloadObject.entrypoint === "string" ? payloadObject.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 { filePath, linesRead, malformedLines, events };
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, usageCommandKind, message) {
630
+ function traceClaude(traceLogger, message) {
519
631
  if (!traceLogger) {
520
632
  return;
521
633
  }
522
- const targetLabel = usageCommandKind === "vscode" ? "Claude VSCode" : "Claude";
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.usageCommandKind, options.readUsageCommandOutput, options.traceLogger),
584
- readClaudeSubscriptionType(options.root, options.usageCommandKind, options.readAuthStatusOutput, options.traceLogger)
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, options.usageCommandKind, `Parsed ${snapshots.length} live usage snapshot(s) from /usage output.`);
695
+ traceClaude(options.traceLogger, `Parsed ${snapshots.length} live usage snapshot(s) from /usage output.`);
588
696
  if (snapshots.length === 0) {
589
- traceClaude(options.traceLogger, options.usageCommandKind, "No live usage snapshots matched the expected /usage format.");
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, options.usageCommandKind, `Resolved live plan type ${resolvedPlanType}.`);
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, options.usageCommandKind, [
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, usageCommandKind, override, traceLogger) {
624
- const output = await readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger);
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, usageCommandKind, "Could not parse subscription type from auth status output.");
735
+ traceClaude(traceLogger, "Could not parse subscription type from auth status output.");
628
736
  }
629
- traceClaude(traceLogger, usageCommandKind, `Subscription type result: ${subscriptionType ?? "<none>"}.`);
737
+ traceClaude(traceLogger, `Subscription type result: ${subscriptionType ?? "<none>"}.`);
630
738
  return subscriptionType;
631
739
  }
632
- async function readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger) {
740
+ async function readClaudeAuthStatusOutput(root, override, traceLogger) {
633
741
  if (override) {
634
742
  try {
635
743
  const output = await override();
636
- traceClaude(traceLogger, usageCommandKind, "Using injected auth status output override.");
744
+ traceClaude(traceLogger, "Using injected auth status output override.");
637
745
  return output;
638
746
  }
639
747
  catch {
640
- traceClaude(traceLogger, usageCommandKind, "Injected auth status output override failed.");
748
+ traceClaude(traceLogger, "Injected auth status output override failed.");
641
749
  return null;
642
750
  }
643
751
  }
644
- const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
752
+ const cacheKey = path.resolve(root);
645
753
  const cached = claudeAuthStatusOutputCache.get(cacheKey);
646
754
  if (cached) {
647
- traceClaude(traceLogger, usageCommandKind, "Auth status output cache hit.");
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, usageCommandKind, traceLogger);
759
+ const binaryPath = await resolveClaudeBinaryPath(root, traceLogger);
652
760
  if (!binaryPath) {
653
- traceClaude(traceLogger, usageCommandKind, "Skipping auth status command because no Claude binary was found.");
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, usageCommandKind, `Running auth status command with ${binaryPath} (TZ=UTC).`);
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, usageCommandKind, "Auth status command completed successfully.");
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, usageCommandKind, `Auth status command failed: ${formatErrorMessage(error)}.`);
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, usageCommandKind, override, agentName, traceLogger) {
709
- const authStatusOutput = await readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger);
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, usageCommandKind, "Auth status output did not yield an analytics identity snapshot.");
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, usageCommandKind, override, traceLogger) {
825
+ async function readClaudeUsageCommandOutput(root, override, traceLogger) {
718
826
  if (override) {
719
827
  try {
720
828
  const output = await override();
721
- traceClaude(traceLogger, usageCommandKind, "Using injected /usage output override.");
722
- traceClaude(traceLogger, usageCommandKind, `Usage returned:\n${describeUsageOutput(output)}`);
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, usageCommandKind, "Injected /usage output override failed.");
834
+ traceClaude(traceLogger, "Injected /usage output override failed.");
727
835
  return null;
728
836
  }
729
837
  }
730
- const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
838
+ const cacheKey = path.resolve(root);
731
839
  const cached = claudeUsageOutputCache.get(cacheKey);
732
840
  if (cached) {
733
- traceClaude(traceLogger, usageCommandKind, "Usage output cache hit.");
841
+ traceClaude(traceLogger, "Usage output cache hit.");
734
842
  return cached;
735
843
  }
736
844
  const pending = (async () => {
737
- const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind, traceLogger);
845
+ const binaryPath = await resolveClaudeBinaryPath(root, traceLogger);
738
846
  if (!binaryPath) {
739
- traceClaude(traceLogger, usageCommandKind, "Skipping /usage command because no Claude binary was found.");
740
- traceClaude(traceLogger, usageCommandKind, "Usage returned:\n<not available>");
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, usageCommandKind, `Running /usage command with ${binaryPath} (TZ=UTC).`);
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, usageCommandKind, "Usage command completed successfully.");
754
- traceClaude(traceLogger, usageCommandKind, `Usage returned:\n${describeUsageOutput(combined || null)}`);
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, usageCommandKind, `Usage command failed: ${formatErrorMessage(error)}.`);
760
- traceClaude(traceLogger, usageCommandKind, `Usage returned:\n${describeUsageOutput(combined || null)}`);
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, usageCommandKind, traceLogger) {
776
- const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
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, usageCommandKind, `Binary detection cache hit: ${binaryPath ? `found ${binaryPath}` : "not found"}.`);
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, usageCommandKind, `Starting binary detection under ${root}.`);
785
- const binaryPath = usageCommandKind === "vscode"
786
- ? await resolveVsCodeClaudeBinaryPath(root, traceLogger)
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 resolveVsCodeClaudeBinaryPath(root, traceLogger) {
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
- let firstFoundPath = null;
920
+ const candidates = [];
801
921
  for (const directory of boosterDirectories) {
802
- const binaryPath = await resolveClaudeBinaryFromExtensionDirectory(directory, traceLogger);
803
- if (!firstFoundPath && binaryPath) {
804
- firstFoundPath = binaryPath;
805
- }
922
+ candidates.push(...(await resolveClaudeBinaryCandidatesFromExtensionDirectory(directory, traceLogger)));
806
923
  }
807
- return firstFoundPath;
924
+ return candidates;
808
925
  }
809
- async function resolveClaudeBinaryFromExtensionDirectory(directory, traceLogger) {
926
+ async function resolveClaudeBinaryCandidatesFromExtensionDirectory(directory, traceLogger) {
810
927
  let entries;
811
- traceClaude(traceLogger, "vscode", `Scanning extension directory ${directory}.`);
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, "vscode", `Could not read ${directory}: ${formatErrorMessage(error)}.`);
817
- return null;
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, "vscode", `No Claude VSCode extension candidates found in ${directory}.`);
825
- return null;
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 firstFoundPath;
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
- async function resolveCliClaudeBinaryPath(root, traceLogger) {
861
- const directCandidates = [
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
- return Math.max(0, Math.min(100, Math.round(value)));
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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",