letmecode 0.1.11 → 0.1.13

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.
@@ -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,17 +6,11 @@ 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
  ];
18
12
  }
19
- export { AntigravityUsageProvider, collectAntigravityQuota, collectAntigravityUsage } from "./antigravity.js";
13
+ export { AntigravityUsageProvider } from "./antigravity.js";
20
14
  export { ClaudeUsageProvider } from "./claude.js";
21
15
  export { CodexUsageProvider } from "./codex.js";
22
16
  export { CopilotUsageProvider, configureCopilotVsCodeLogging } from "./copilot.js";
@@ -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,10 +1,9 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",
7
- "packageManager": "pnpm@10.28.2",
8
7
  "type": "commonjs",
9
8
  "bin": {
10
9
  "letmecode": "./bin/letmecode.js"
@@ -21,16 +20,6 @@
21
20
  "publishConfig": {
22
21
  "access": "public"
23
22
  },
24
- "scripts": {
25
- "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
26
- "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
27
- "prepack": "npm run build",
28
- "prestart": "npm run build",
29
- "start": "node ./bin/letmecode.js",
30
- "pretest": "npm run build",
31
- "smoke": "node ./bin/letmecode.js",
32
- "test": "node --test ink-app/test/*.test.mjs"
33
- },
34
23
  "keywords": [
35
24
  "cli",
36
25
  "ink",
@@ -38,7 +27,6 @@
38
27
  "typescript"
39
28
  ],
40
29
  "dependencies": {
41
- "@tokscale/cli": "4.0.2",
42
30
  "ink": "4.4.1",
43
31
  "jsonc-parser": "^3.3.1",
44
32
  "react": "18.3.1"
@@ -47,5 +35,14 @@
47
35
  "@types/node": "^24.0.7",
48
36
  "@types/react": "^18.3.24",
49
37
  "typescript": "^5.8.3"
38
+ },
39
+ "scripts": {
40
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('ink-app/dist', { recursive: true, force: true });\"",
41
+ "build": "npm run clean && tsc -p tsconfig.json && tsc -p ink-app/tsconfig.json",
42
+ "prestart": "npm run build",
43
+ "start": "node ./bin/letmecode.js",
44
+ "pretest": "npm run build",
45
+ "smoke": "node ./bin/letmecode.js",
46
+ "test": "node --test ink-app/test/*.test.mjs"
50
47
  }
51
- }
48
+ }