letmecode 0.1.11 → 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,11 +72,11 @@ 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) => matchesClaudeProviderEvent(event, file, this.entrypoints, this.usageCommandKind));
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}`,
@@ -103,7 +100,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
103
100
  ...parsedEvents.keyedEvents.values(),
104
101
  ...parsedEvents.unkeyedEvents.values()
105
102
  ];
106
- traceClaude(options.traceLogger, this.usageCommandKind, [
103
+ traceClaude(options.traceLogger, [
107
104
  `Transcript selection summary: filesWithMatches=${parseTotals.filesScanned}/${parsedSessionFiles.length}`,
108
105
  `selectedEvents=${selectedEvents.length}`,
109
106
  `duplicateUsageKeys=${parsedEvents.duplicateUsageKeys}`,
@@ -111,7 +108,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
111
108
  `duplicateUnkeyedEvents=${parsedEvents.duplicateUnkeyedEvents}`
112
109
  ].join(" "));
113
110
  if (selectedEvents.length === 0 && parsedSessionFiles.length > 0) {
114
- traceClaude(options.traceLogger, this.usageCommandKind, `No transcript usage matched entrypoints [${[...this.entrypoints].join(", ")}]. Observed entrypoints=${summarizeEventCounts(collectEntryPoints(parsedSessionFiles))}.`);
111
+ traceClaude(options.traceLogger, "No assistant usage events were found in the parsed Claude session files.");
115
112
  }
116
113
  for (const event of selectedEvents) {
117
114
  addModelUsage(byModel, event.modelId, event.totals);
@@ -150,7 +147,6 @@ export class ClaudeUsageProvider extends UsageProviderBase {
150
147
  const [fallbackPrimaryLimitWindows, fallbackSecondaryLimitWindows] = buildWindowLists(windows);
151
148
  const liveLimitWindows = await buildLiveLimitWindows({
152
149
  root: this.root,
153
- usageCommandKind: this.usageCommandKind,
154
150
  readUsageCommandOutput: this.readUsageCommandOutput,
155
151
  readAuthStatusOutput: this.readAuthStatusOutput,
156
152
  traceLogger: options.traceLogger,
@@ -163,7 +159,7 @@ export class ClaudeUsageProvider extends UsageProviderBase {
163
159
  const secondaryLimitWindows = liveLimitWindows.secondaryLimitWindows.length > 0
164
160
  ? liveLimitWindows.secondaryLimitWindows
165
161
  : fallbackSecondaryLimitWindows;
166
- traceClaude(options.traceLogger, this.usageCommandKind, [
162
+ traceClaude(options.traceLogger, [
167
163
  `Finished stats collection:`,
168
164
  `filesScanned=${parseTotals.filesScanned}`,
169
165
  `linesRead=${parseTotals.linesRead}`,
@@ -275,14 +271,14 @@ function resolveClaudeCacheWriteBreakdown(usage) {
275
271
  function isSessionFile(filePath) {
276
272
  return filePath.endsWith(".jsonl");
277
273
  }
278
- async function resolveClaudeSessionsRoot(root, usageCommandKind, traceLogger) {
274
+ async function resolveClaudeSessionsRoot(root, traceLogger) {
279
275
  const candidates = buildClaudeSessionsRootCandidates(root);
280
- traceClaude(traceLogger, usageCommandKind, `Checking ${candidates.length} Claude session root candidate(s).`);
276
+ traceClaude(traceLogger, `Checking ${candidates.length} Claude session root candidate(s).`);
281
277
  for (const candidate of candidates) {
282
278
  const exists = await isDirectory(candidate.rootPath);
283
- 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"}).`);
284
280
  if (exists) {
285
- traceClaude(traceLogger, usageCommandKind, `Selected session root ${candidate.rootLabel} -> ${candidate.rootPath}.`);
281
+ traceClaude(traceLogger, `Selected session root ${candidate.rootLabel} -> ${candidate.rootPath}.`);
286
282
  return candidate;
287
283
  }
288
284
  }
@@ -290,7 +286,7 @@ async function resolveClaudeSessionsRoot(root, usageCommandKind, traceLogger) {
290
286
  rootLabel: "~/.claude/projects",
291
287
  rootPath: path.join(path.resolve(root), ".claude", "projects")
292
288
  };
293
- 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}.`);
294
290
  return fallbackCandidate;
295
291
  }
296
292
  function buildClaudeSessionsRootCandidates(root) {
@@ -379,22 +375,22 @@ async function* walkSessionFiles(directory) {
379
375
  }
380
376
  }
381
377
  }
382
- async function loadParsedClaudeSessionFiles(sessionsRoot, usageCommandKind, traceLogger) {
378
+ async function loadParsedClaudeSessionFiles(sessionsRoot, traceLogger) {
383
379
  const cacheKey = path.resolve(sessionsRoot);
384
380
  const cached = parsedClaudeSessionFilesCache.get(cacheKey);
385
381
  if (cached) {
386
382
  const files = await cached;
387
- 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)).`);
388
384
  return files;
389
385
  }
390
386
  const pending = (async () => {
391
387
  const files = [];
392
- traceClaude(traceLogger, usageCommandKind, `Scanning session files under ${sessionsRoot}.`);
388
+ traceClaude(traceLogger, `Scanning session files under ${sessionsRoot}.`);
393
389
  for await (const filePath of walkSessionFiles(sessionsRoot)) {
394
390
  files.push(await parseSessionFile(filePath, sessionsRoot));
395
391
  }
396
392
  inferClaudeSessionFileSources(files);
397
- traceClaude(traceLogger, usageCommandKind, `Completed session file scan under ${sessionsRoot}: ${files.length} file(s) parsed.`);
393
+ traceClaude(traceLogger, `Completed session file scan under ${sessionsRoot}: ${files.length} file(s) parsed.`);
398
394
  return files;
399
395
  })();
400
396
  parsedClaudeSessionFilesCache.set(cacheKey, pending);
@@ -625,30 +621,17 @@ function shouldReplaceUsageEvent(previous, next) {
625
621
  }
626
622
  return false;
627
623
  }
628
- function matchesClaudeProviderEvent(event, file, entrypoints, usageCommandKind) {
629
- if (entrypoints.has(event.entrypoint)) {
630
- return true;
631
- }
632
- if (event.entrypoint !== "cli") {
633
- return false;
634
- }
635
- if (usageCommandKind === "vscode") {
636
- return file.sourceKind === "vscode";
637
- }
638
- return file.sourceKind === "cli";
639
- }
640
624
  function normalizeTimestamp(value) {
641
625
  return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
642
626
  }
643
627
  function extractRateLimits(payloadObject, message) {
644
628
  return asRecord(payloadObject.rate_limits) ?? asRecord(message?.rate_limits);
645
629
  }
646
- function traceClaude(traceLogger, usageCommandKind, message) {
630
+ function traceClaude(traceLogger, message) {
647
631
  if (!traceLogger) {
648
632
  return;
649
633
  }
650
- const targetLabel = usageCommandKind === "vscode" ? "Claude VSCode" : "Claude";
651
- traceLogger.log(`[${targetLabel}] ${message}`);
634
+ traceLogger.log(`[Claude] ${message}`);
652
635
  }
653
636
  function formatErrorMessage(error) {
654
637
  if (!error) {
@@ -697,9 +680,6 @@ function summarizeDistinctValues(values, limit = 5) {
697
680
  ? `${visibleValues.join(", ")} (+${remainder} more)`
698
681
  : visibleValues.join(", ");
699
682
  }
700
- function collectEntryPoints(files) {
701
- return files.flatMap((file) => file.events.map((event) => event.entrypoint || "<empty>"));
702
- }
703
683
  function buildClaudeCommandEnvironment() {
704
684
  return {
705
685
  ...process.env,
@@ -708,16 +688,16 @@ function buildClaudeCommandEnvironment() {
708
688
  }
709
689
  async function buildLiveLimitWindows(options) {
710
690
  const [usageOutput, subscriptionType] = await Promise.all([
711
- readClaudeUsageCommandOutput(options.root, options.usageCommandKind, options.readUsageCommandOutput, options.traceLogger),
712
- 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)
713
693
  ]);
714
694
  const snapshots = parseLiveUsageWindowSnapshots(usageOutput, options.now);
715
- 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.`);
716
696
  if (snapshots.length === 0) {
717
- 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.");
718
698
  }
719
699
  const resolvedPlanType = subscriptionType || "live";
720
- traceClaude(options.traceLogger, options.usageCommandKind, `Resolved live plan type ${resolvedPlanType}.`);
700
+ traceClaude(options.traceLogger, `Resolved live plan type ${resolvedPlanType}.`);
721
701
  const primaryLimitWindows = snapshots
722
702
  .filter((snapshot) => snapshot.scope === "primary")
723
703
  .map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now));
@@ -732,7 +712,7 @@ async function buildLiveLimitWindows(options) {
732
712
  if (!row) {
733
713
  continue;
734
714
  }
735
- traceClaude(options.traceLogger, options.usageCommandKind, [
715
+ traceClaude(options.traceLogger, [
736
716
  `Live window ${snapshot.scope}/${snapshot.label}:`,
737
717
  `used=${snapshot.usedPercent}%`,
738
718
  `range=${row.startTimeUtcIso}->${row.endTimeUtcIso}`,
@@ -748,41 +728,41 @@ async function buildLiveLimitWindows(options) {
748
728
  secondaryLimitWindows
749
729
  };
750
730
  }
751
- async function readClaudeSubscriptionType(root, usageCommandKind, override, traceLogger) {
752
- const output = await readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger);
731
+ async function readClaudeSubscriptionType(root, override, traceLogger) {
732
+ const output = await readClaudeAuthStatusOutput(root, override, traceLogger);
753
733
  const subscriptionType = parseClaudeSubscriptionType(output);
754
734
  if (output && !subscriptionType) {
755
- traceClaude(traceLogger, usageCommandKind, "Could not parse subscription type from auth status output.");
735
+ traceClaude(traceLogger, "Could not parse subscription type from auth status output.");
756
736
  }
757
- traceClaude(traceLogger, usageCommandKind, `Subscription type result: ${subscriptionType ?? "<none>"}.`);
737
+ traceClaude(traceLogger, `Subscription type result: ${subscriptionType ?? "<none>"}.`);
758
738
  return subscriptionType;
759
739
  }
760
- async function readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger) {
740
+ async function readClaudeAuthStatusOutput(root, override, traceLogger) {
761
741
  if (override) {
762
742
  try {
763
743
  const output = await override();
764
- traceClaude(traceLogger, usageCommandKind, "Using injected auth status output override.");
744
+ traceClaude(traceLogger, "Using injected auth status output override.");
765
745
  return output;
766
746
  }
767
747
  catch {
768
- traceClaude(traceLogger, usageCommandKind, "Injected auth status output override failed.");
748
+ traceClaude(traceLogger, "Injected auth status output override failed.");
769
749
  return null;
770
750
  }
771
751
  }
772
- const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
752
+ const cacheKey = path.resolve(root);
773
753
  const cached = claudeAuthStatusOutputCache.get(cacheKey);
774
754
  if (cached) {
775
- traceClaude(traceLogger, usageCommandKind, "Auth status output cache hit.");
755
+ traceClaude(traceLogger, "Auth status output cache hit.");
776
756
  return cached;
777
757
  }
778
758
  const pending = (async () => {
779
- const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind, traceLogger);
759
+ const binaryPath = await resolveClaudeBinaryPath(root, traceLogger);
780
760
  if (!binaryPath) {
781
- 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.");
782
762
  return null;
783
763
  }
784
764
  try {
785
- traceClaude(traceLogger, usageCommandKind, `Running auth status command with ${binaryPath} (TZ=UTC).`);
765
+ traceClaude(traceLogger, `Running auth status command with ${binaryPath} (TZ=UTC).`);
786
766
  const { stdout, stderr } = await execFileAsync(binaryPath, ["auth", "status"], {
787
767
  encoding: "utf8",
788
768
  env: buildClaudeCommandEnvironment(),
@@ -791,12 +771,12 @@ async function readClaudeAuthStatusOutput(root, usageCommandKind, override, trac
791
771
  windowsHide: true
792
772
  });
793
773
  const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
794
- traceClaude(traceLogger, usageCommandKind, "Auth status command completed successfully.");
774
+ traceClaude(traceLogger, "Auth status command completed successfully.");
795
775
  return combined || null;
796
776
  }
797
777
  catch (error) {
798
778
  const combined = extractExecOutput(error);
799
- traceClaude(traceLogger, usageCommandKind, `Auth status command failed: ${formatErrorMessage(error)}.`);
779
+ traceClaude(traceLogger, `Auth status command failed: ${formatErrorMessage(error)}.`);
800
780
  return combined || null;
801
781
  }
802
782
  })();
@@ -833,43 +813,43 @@ function parseClaudeAuthStatusSnapshot(output) {
833
813
  return null;
834
814
  }
835
815
  }
836
- async function readClaudeUserIdHash(root, usageCommandKind, override, agentName, traceLogger) {
837
- const authStatusOutput = await readClaudeAuthStatusOutput(root, usageCommandKind, override, traceLogger);
816
+ async function readClaudeUserIdHash(root, override, agentName, traceLogger) {
817
+ const authStatusOutput = await readClaudeAuthStatusOutput(root, override, traceLogger);
838
818
  const snapshot = parseClaudeAuthStatusSnapshot(authStatusOutput);
839
819
  if (!snapshot) {
840
- 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.");
841
821
  return null;
842
822
  }
843
823
  return buildUserIdHash([agentName, snapshot.email, snapshot.orgId, snapshot.orgName]);
844
824
  }
845
- async function readClaudeUsageCommandOutput(root, usageCommandKind, override, traceLogger) {
825
+ async function readClaudeUsageCommandOutput(root, override, traceLogger) {
846
826
  if (override) {
847
827
  try {
848
828
  const output = await override();
849
- traceClaude(traceLogger, usageCommandKind, "Using injected /usage output override.");
850
- traceClaude(traceLogger, usageCommandKind, `Usage returned:\n${describeUsageOutput(output)}`);
829
+ traceClaude(traceLogger, "Using injected /usage output override.");
830
+ traceClaude(traceLogger, `Usage returned:\n${describeUsageOutput(output)}`);
851
831
  return output;
852
832
  }
853
833
  catch {
854
- traceClaude(traceLogger, usageCommandKind, "Injected /usage output override failed.");
834
+ traceClaude(traceLogger, "Injected /usage output override failed.");
855
835
  return null;
856
836
  }
857
837
  }
858
- const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
838
+ const cacheKey = path.resolve(root);
859
839
  const cached = claudeUsageOutputCache.get(cacheKey);
860
840
  if (cached) {
861
- traceClaude(traceLogger, usageCommandKind, "Usage output cache hit.");
841
+ traceClaude(traceLogger, "Usage output cache hit.");
862
842
  return cached;
863
843
  }
864
844
  const pending = (async () => {
865
- const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind, traceLogger);
845
+ const binaryPath = await resolveClaudeBinaryPath(root, traceLogger);
866
846
  if (!binaryPath) {
867
- traceClaude(traceLogger, usageCommandKind, "Skipping /usage command because no Claude binary was found.");
868
- 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>");
869
849
  return null;
870
850
  }
871
851
  try {
872
- traceClaude(traceLogger, usageCommandKind, `Running /usage command with ${binaryPath} (TZ=UTC).`);
852
+ traceClaude(traceLogger, `Running /usage command with ${binaryPath} (TZ=UTC).`);
873
853
  const { stdout, stderr } = await execFileAsync(binaryPath, ["-p", "/usage"], {
874
854
  encoding: "utf8",
875
855
  env: buildClaudeCommandEnvironment(),
@@ -878,14 +858,14 @@ async function readClaudeUsageCommandOutput(root, usageCommandKind, override, tr
878
858
  windowsHide: true
879
859
  });
880
860
  const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
881
- traceClaude(traceLogger, usageCommandKind, "Usage command completed successfully.");
882
- 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)}`);
883
863
  return combined || null;
884
864
  }
885
865
  catch (error) {
886
866
  const combined = extractExecOutput(error);
887
- traceClaude(traceLogger, usageCommandKind, `Usage command failed: ${formatErrorMessage(error)}.`);
888
- 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)}`);
889
869
  return combined || null;
890
870
  }
891
871
  })();
@@ -900,68 +880,68 @@ function extractExecOutput(error) {
900
880
  const stderr = typeof error.stderr === "string" ? error.stderr : "";
901
881
  return [stdout, stderr].filter(Boolean).join("\n").trim();
902
882
  }
903
- async function resolveClaudeBinaryPath(root, usageCommandKind, traceLogger) {
904
- const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
883
+ async function resolveClaudeBinaryPath(root, traceLogger) {
884
+ const cacheKey = path.resolve(root);
905
885
  const cached = claudeBinaryPathCache.get(cacheKey);
906
886
  if (cached) {
907
887
  const binaryPath = await cached;
908
- traceClaude(traceLogger, usageCommandKind, `Binary detection cache hit: ${binaryPath ? `found ${binaryPath}` : "not found"}.`);
888
+ traceClaude(traceLogger, `Binary detection cache hit: ${binaryPath ? `found ${binaryPath}` : "not found"}.`);
909
889
  return binaryPath;
910
890
  }
911
891
  const pending = (async () => {
912
- traceClaude(traceLogger, usageCommandKind, `Starting binary detection under ${root}.`);
913
- const binaryPath = usageCommandKind === "vscode"
914
- ? await resolveVsCodeClaudeBinaryPath(root, traceLogger)
915
- : await resolveCliClaudeBinaryPath(root, traceLogger);
916
- 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"}.`);
917
895
  return binaryPath;
918
896
  })();
919
897
  claudeBinaryPathCache.set(cacheKey, pending);
920
898
  return pending;
921
899
  }
922
- 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) {
923
915
  const boosterDirectories = [
924
916
  path.join(root, ".vscode", "extensions"),
925
917
  path.join(root, ".vscode-server", "extensions"),
926
918
  path.join(root, ".vscode-server-insiders", "extensions")
927
919
  ];
928
- let firstFoundPath = null;
920
+ const candidates = [];
929
921
  for (const directory of boosterDirectories) {
930
- const binaryPath = await resolveClaudeBinaryFromExtensionDirectory(directory, traceLogger);
931
- if (!firstFoundPath && binaryPath) {
932
- firstFoundPath = binaryPath;
933
- }
922
+ candidates.push(...(await resolveClaudeBinaryCandidatesFromExtensionDirectory(directory, traceLogger)));
934
923
  }
935
- return firstFoundPath;
924
+ return candidates;
936
925
  }
937
- async function resolveClaudeBinaryFromExtensionDirectory(directory, traceLogger) {
926
+ async function resolveClaudeBinaryCandidatesFromExtensionDirectory(directory, traceLogger) {
938
927
  let entries;
939
- traceClaude(traceLogger, "vscode", `Scanning extension directory ${directory}.`);
928
+ traceClaude(traceLogger, `Scanning extension directory ${directory}.`);
940
929
  try {
941
930
  entries = await fs.promises.readdir(directory, { withFileTypes: true });
942
931
  }
943
932
  catch (error) {
944
- traceClaude(traceLogger, "vscode", `Could not read ${directory}: ${formatErrorMessage(error)}.`);
945
- return null;
933
+ traceClaude(traceLogger, `Could not read ${directory}: ${formatErrorMessage(error)}.`);
934
+ return [];
946
935
  }
947
936
  const candidates = entries
948
937
  .filter((entry) => entry.isDirectory() && entry.name.startsWith(VSCODE_CLAUDE_EXTENSION_PREFIX))
949
938
  .map((entry) => entry.name)
950
939
  .sort(compareClaudeExtensionDirectoryNames);
951
940
  if (candidates.length === 0) {
952
- traceClaude(traceLogger, "vscode", `No Claude VSCode extension candidates found in ${directory}.`);
953
- return null;
954
- }
955
- let firstFoundPath = null;
956
- for (const candidate of candidates) {
957
- const binaryPath = path.join(directory, candidate, "resources", "native-binary", "claude");
958
- const accessCheck = await checkReadableExecutableFile(binaryPath);
959
- traceClaude(traceLogger, "vscode", `Checked ${binaryPath} -> ${accessCheck.ok ? "success" : `failure (${accessCheck.errorMessage ?? "unknown"})`}.`);
960
- if (!firstFoundPath && accessCheck.ok) {
961
- firstFoundPath = binaryPath;
962
- }
941
+ traceClaude(traceLogger, `No Claude VSCode extension candidates found in ${directory}.`);
942
+ return [];
963
943
  }
964
- return firstFoundPath;
944
+ return candidates.map((candidate) => path.join(directory, candidate, "resources", "native-binary", "claude"));
965
945
  }
966
946
  function compareClaudeExtensionDirectoryNames(left, right) {
967
947
  const leftVersion = extractClaudeExtensionVersion(left);
@@ -985,20 +965,11 @@ function extractClaudeExtensionVersion(directoryName) {
985
965
  .map((part) => Number(part))
986
966
  .filter((part) => Number.isFinite(part));
987
967
  }
988
- async function resolveCliClaudeBinaryPath(root, traceLogger) {
989
- const directCandidates = [
968
+ function resolveDirectClaudeBinaryCandidates(root) {
969
+ return [
990
970
  path.join(root, ".local", "bin", "claude"),
991
971
  path.join(root, "bin", "claude")
992
972
  ];
993
- let firstFoundPath = null;
994
- for (const candidate of directCandidates) {
995
- const accessCheck = await checkReadableExecutableFile(candidate);
996
- traceClaude(traceLogger, "cli", `Checked ${candidate} -> ${accessCheck.ok ? "success" : `failure (${accessCheck.errorMessage ?? "unknown"})`}.`);
997
- if (!firstFoundPath && accessCheck.ok) {
998
- firstFoundPath = candidate;
999
- }
1000
- }
1001
- return firstFoundPath;
1002
973
  }
1003
974
  async function checkReadableExecutableFile(filePath) {
1004
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.11",
3
+ "version": "0.1.12",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",