letmecode 0.1.3 → 0.1.5

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.
@@ -1,31 +1,66 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
1
3
  import fs from "node:fs";
2
4
  import os from "node:os";
3
5
  import path from "node:path";
4
6
  import readline from "node:readline";
7
+ import { promisify } from "node:util";
5
8
  import { UsageProviderBase, addUsageTotals, createEmptyUsageTotals, sumUsageTotals } from "./contract.js";
6
9
  import { applyRateLimits, asRecord, buildWindowLists, createLimitWindowAggregates, numberOrZero } from "./limits.js";
7
10
  import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "./daily.js";
11
+ import { resolveUsageRate } from "./pricing.js";
8
12
  const RATE_CARD = {
9
- "claude-opus-4-8": { input: 5, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5, output: 25 },
10
- "claude-opus-4-7": { input: 5, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5, output: 25 },
11
- "claude-opus-4-6": { input: 5, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5, output: 25 },
12
- "claude-opus-4-5": { input: 5, cacheWrite5m: 6.25, cacheWrite1h: 10, cacheRead: 0.5, output: 25 },
13
- "claude-opus-4-1": { input: 15, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5, output: 75 },
14
- "claude-opus-4": { input: 15, cacheWrite5m: 18.75, cacheWrite1h: 30, cacheRead: 1.5, output: 75 },
15
- "claude-sonnet-4-6": { input: 3, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3, output: 15 },
16
- "claude-sonnet-4-5": { input: 3, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3, output: 15 },
17
- "claude-sonnet-4": { input: 3, cacheWrite5m: 3.75, cacheWrite1h: 6, cacheRead: 0.3, output: 15 },
18
- "claude-haiku-4-5": { input: 1, cacheWrite5m: 1.25, cacheWrite1h: 2, cacheRead: 0.1, output: 5 },
19
- "claude-haiku-3-5": { input: 0.8, cacheWrite5m: 1, cacheWrite1h: 1.6, cacheRead: 0.08, output: 4 }
13
+ "claude-opus-4-8": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
14
+ "claude-opus-4-7": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
15
+ "claude-opus-4-6": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
16
+ "claude-opus-4-5": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, cacheWrite5m: 6.25, cacheWrite1h: 10, output: 25 },
17
+ "claude-opus-4-1": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
18
+ "claude-opus-4": { input: 15, cacheRead: 1.5, cacheWrite: 18.75, cacheWrite5m: 18.75, cacheWrite1h: 30, output: 75 },
19
+ "claude-sonnet-4-6": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
20
+ "claude-sonnet-4-5": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
21
+ "claude-sonnet-4": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, cacheWrite5m: 3.75, cacheWrite1h: 6, output: 15 },
22
+ "claude-haiku-4-5": { input: 1, cacheRead: 0.1, cacheWrite: 1.25, cacheWrite5m: 1.25, cacheWrite1h: 2, output: 5 },
23
+ "claude-haiku-3-5": { input: 0.8, cacheRead: 0.08, cacheWrite: 1, cacheWrite5m: 1, cacheWrite1h: 1.6, output: 4 }
20
24
  };
25
+ const execFileAsync = promisify(execFile);
21
26
  const USD_TO_CREDITS = 100;
27
+ const VSCODE_CLAUDE_EXTENSION_PREFIX = "anthropic.claude-code-";
28
+ const CLAUDE_SESSION_WINDOW_MINUTES = 5 * 60;
29
+ const CLAUDE_WEEK_WINDOW_MINUTES = 7 * 24 * 60;
30
+ const ANSI_ESCAPE_SEQUENCE = /\u001B\[[0-9;]*[A-Za-z]/g;
31
+ const DEFAULT_CLAUDE_ENTRYPOINTS = ["sdk-cli", "claude"];
32
+ const MONTH_INDEX_BY_LABEL = {
33
+ jan: 0,
34
+ feb: 1,
35
+ mar: 2,
36
+ apr: 3,
37
+ may: 4,
38
+ jun: 5,
39
+ jul: 6,
40
+ aug: 7,
41
+ sep: 8,
42
+ oct: 9,
43
+ nov: 10,
44
+ dec: 11
45
+ };
46
+ const parsedClaudeSessionFilesCache = new Map();
47
+ const claudeBinaryPathCache = new Map();
48
+ const claudeUsageOutputCache = new Map();
49
+ const claudeAuthStatusOutputCache = new Map();
22
50
  export class ClaudeUsageProvider extends UsageProviderBase {
23
51
  constructor(options = {}) {
24
- super("claude", "Claude");
52
+ super(options.id ?? "claude", options.label ?? "Claude");
25
53
  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
+ this.readUsageCommandOutput = options.readUsageCommandOutput;
57
+ this.readAuthStatusOutput = options.readAuthStatusOutput;
58
+ this.now = options.now ?? (() => new Date());
26
59
  }
27
60
  async getStats(options = {}) {
28
61
  const sessionsRoot = path.join(this.root, ".claude", "projects");
62
+ const agentName = normalizeAnalyticsAgentName(this.label);
63
+ const userIdHash = await readClaudeUserIdHash(this.root, this.usageCommandKind, this.readAuthStatusOutput, agentName);
29
64
  const byModel = new Map();
30
65
  const byDay = createDailyUsageAggregates();
31
66
  const windows = createLimitWindowAggregates();
@@ -38,11 +73,18 @@ export class ClaudeUsageProvider extends UsageProviderBase {
38
73
  tokenEvents: 0,
39
74
  malformedLines: 0
40
75
  };
41
- for await (const file of walkSessionFiles(sessionsRoot)) {
76
+ const parsedSessionFiles = await loadParsedClaudeSessionFiles(sessionsRoot);
77
+ for (const file of parsedSessionFiles) {
78
+ const matchingEvents = file.events.filter((event) => this.entrypoints.has(event.entrypoint));
79
+ if (matchingEvents.length === 0) {
80
+ continue;
81
+ }
42
82
  parseTotals.filesScanned += 1;
43
- const fileStats = await parseSessionFile(file, parsedEvents);
44
- parseTotals.linesRead += fileStats.linesRead;
45
- parseTotals.malformedLines += fileStats.malformedLines;
83
+ parseTotals.linesRead += file.linesRead;
84
+ parseTotals.malformedLines += file.malformedLines;
85
+ for (const event of matchingEvents) {
86
+ recordParsedUsageEvent(parsedEvents, event);
87
+ }
46
88
  }
47
89
  const selectedEvents = [
48
90
  ...parsedEvents.keyedEvents.values(),
@@ -73,22 +115,30 @@ export class ClaudeUsageProvider extends UsageProviderBase {
73
115
  .sort((left, right) => right.totals.estimatedCredits - left.totals.estimatedCredits);
74
116
  const unknownPricedModels = modelUsage
75
117
  .map((row) => row.modelId)
76
- .filter((modelId) => !resolveRate(modelId));
118
+ .filter((modelId) => !resolveRate(modelId) && !isInternalClaudeModel(modelId));
77
119
  if (unknownPricedModels.length > 0) {
78
120
  warnings.push(`No credit rate configured for: ${unknownPricedModels.join(", ")}.`);
79
121
  }
80
- if (parseTotals.filesScanned === 0) {
122
+ if (parsedSessionFiles.length === 0) {
81
123
  warnings.push(`No Claude session files found under ${sessionsRoot}.`);
82
124
  }
83
125
  const summaryTotals = sumUsageTotals(modelUsage.map((row) => row.totals));
84
126
  const dayUsage = buildDailyUsageRows(byDay);
85
- const [primaryLimitWindows, secondaryLimitWindows] = buildWindowLists(windows);
86
- if (parseTotals.filesScanned > 0 &&
87
- parseTotals.tokenEvents > 0 &&
88
- primaryLimitWindows.length === 0 &&
89
- secondaryLimitWindows.length === 0) {
90
- warnings.push("Claude transcripts did not expose rate-limit windows in the local logs.");
91
- }
127
+ const [fallbackPrimaryLimitWindows, fallbackSecondaryLimitWindows] = buildWindowLists(windows);
128
+ const liveLimitWindows = await buildLiveLimitWindows({
129
+ root: this.root,
130
+ usageCommandKind: this.usageCommandKind,
131
+ readUsageCommandOutput: this.readUsageCommandOutput,
132
+ readAuthStatusOutput: this.readAuthStatusOutput,
133
+ now: this.now(),
134
+ selectedEvents
135
+ });
136
+ const primaryLimitWindows = liveLimitWindows.primaryLimitWindows.length > 0
137
+ ? liveLimitWindows.primaryLimitWindows
138
+ : fallbackPrimaryLimitWindows;
139
+ const secondaryLimitWindows = liveLimitWindows.secondaryLimitWindows.length > 0
140
+ ? liveLimitWindows.secondaryLimitWindows
141
+ : fallbackSecondaryLimitWindows;
92
142
  return {
93
143
  providerId: this.id,
94
144
  providerLabel: this.label,
@@ -106,7 +156,11 @@ export class ClaudeUsageProvider extends UsageProviderBase {
106
156
  dayUsage,
107
157
  primaryLimitWindows,
108
158
  secondaryLimitWindows,
109
- warnings
159
+ warnings,
160
+ analytics: {
161
+ agentName,
162
+ userIdHash
163
+ }
110
164
  };
111
165
  }
112
166
  }
@@ -127,13 +181,10 @@ function normalizeUsage(value) {
127
181
  };
128
182
  }
129
183
  function resolveRate(modelId) {
130
- const candidates = Object.keys(RATE_CARD).sort((left, right) => right.length - left.length);
131
- for (const candidate of candidates) {
132
- if (modelId === candidate || modelId.startsWith(`${candidate}-`)) {
133
- return RATE_CARD[candidate];
134
- }
135
- }
136
- return undefined;
184
+ return resolveUsageRate(RATE_CARD, modelId, 0, { prefixMatch: true });
185
+ }
186
+ function isInternalClaudeModel(modelId) {
187
+ return modelId === "<synthetic>";
137
188
  }
138
189
  function creditsFor(modelId, usage) {
139
190
  const rate = resolveRate(modelId);
@@ -152,30 +203,27 @@ function creditsFor(modelId, usage) {
152
203
  }
153
204
  function usageToTotals(modelId, usage) {
154
205
  const cacheWriteBreakdown = resolveClaudeCacheWriteBreakdown(usage);
155
- const inputTotalTokens = usage.inputTokens +
156
- cacheWriteBreakdown.cacheWrite5mInputTokens +
157
- cacheWriteBreakdown.cacheWrite1hInputTokens +
158
- usage.cacheReadInputTokens;
206
+ const cacheWriteInputTokens = cacheWriteBreakdown.cacheWrite5mInputTokens +
207
+ cacheWriteBreakdown.cacheWrite1hInputTokens;
159
208
  return {
160
- inputTotalTokens,
209
+ inputTokens: usage.inputTokens,
161
210
  outputTokens: usage.outputTokens,
211
+ cacheReadInputTokens: usage.cacheReadInputTokens,
212
+ cacheWriteInputTokens,
213
+ cacheWrite5mInputTokens: cacheWriteBreakdown.cacheWrite5mInputTokens,
214
+ cacheWrite1hInputTokens: cacheWriteBreakdown.cacheWrite1hInputTokens,
162
215
  reasoningOutputTokens: 0,
163
- totalTokens: inputTotalTokens + usage.outputTokens,
216
+ totalTokens: usage.inputTokens +
217
+ usage.cacheReadInputTokens +
218
+ cacheWriteInputTokens +
219
+ usage.outputTokens,
164
220
  estimatedCredits: creditsFor(modelId, usage),
165
- eventCount: 1,
166
- tokenBreakdown: {
167
- schema: "anthropic",
168
- inputTokens: usage.inputTokens,
169
- cacheWrite5mInputTokens: cacheWriteBreakdown.cacheWrite5mInputTokens,
170
- cacheWrite1hInputTokens: cacheWriteBreakdown.cacheWrite1hInputTokens,
171
- cacheReadInputTokens: usage.cacheReadInputTokens,
172
- outputTokens: usage.outputTokens
173
- }
221
+ eventCount: 1
174
222
  };
175
223
  }
176
224
  function addModelUsage(byModel, modelId, deltaTotals) {
177
225
  const resolvedModelId = modelId || "unknown";
178
- const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals("anthropic");
226
+ const totals = byModel.get(resolvedModelId) ?? createEmptyUsageTotals();
179
227
  addUsageTotals(totals, deltaTotals);
180
228
  byModel.set(resolvedModelId, totals);
181
229
  }
@@ -208,11 +256,28 @@ async function* walkSessionFiles(directory) {
208
256
  }
209
257
  }
210
258
  }
211
- async function parseSessionFile(filePath, parsedEvents) {
259
+ async function loadParsedClaudeSessionFiles(sessionsRoot) {
260
+ const cacheKey = path.resolve(sessionsRoot);
261
+ const cached = parsedClaudeSessionFilesCache.get(cacheKey);
262
+ if (cached) {
263
+ return cached;
264
+ }
265
+ const pending = (async () => {
266
+ const files = [];
267
+ for await (const filePath of walkSessionFiles(sessionsRoot)) {
268
+ files.push(await parseSessionFile(filePath));
269
+ }
270
+ return files;
271
+ })();
272
+ parsedClaudeSessionFilesCache.set(cacheKey, pending);
273
+ return pending;
274
+ }
275
+ async function parseSessionFile(filePath) {
212
276
  const stream = fs.createReadStream(filePath, { encoding: "utf8" });
213
277
  const lineReader = readline.createInterface({ input: stream, crlfDelay: Infinity });
214
278
  let linesRead = 0;
215
279
  let malformedLines = 0;
280
+ const events = [];
216
281
  for await (const line of lineReader) {
217
282
  linesRead += 1;
218
283
  if (!line.trim()) {
@@ -240,17 +305,17 @@ async function parseSessionFile(filePath, parsedEvents) {
240
305
  const normalizedUsage = normalizeUsage(usage);
241
306
  const usageKey = buildUsageEventKey(payloadObject, message);
242
307
  const usageSignature = buildUsageSignature(payloadObject, modelId, normalizedUsage);
243
- const parsedEvent = {
308
+ events.push({
309
+ entrypoint: typeof payloadObject.entrypoint === "string" ? payloadObject.entrypoint : "",
244
310
  usageKey,
245
311
  usageSignature,
246
312
  timestampMs: eventTimeMs,
247
313
  modelId,
248
314
  totals: usageToTotals(modelId, normalizedUsage),
249
315
  rateLimits
250
- };
251
- recordParsedUsageEvent(parsedEvents, parsedEvent);
316
+ });
252
317
  }
253
- return { linesRead, malformedLines };
318
+ return { linesRead, malformedLines, events };
254
319
  }
255
320
  function buildUsageEventKey(payloadObject, message) {
256
321
  const sessionId = String(payloadObject.sessionId ?? "");
@@ -324,3 +389,342 @@ function normalizeTimestamp(value) {
324
389
  function extractRateLimits(payloadObject, message) {
325
390
  return asRecord(payloadObject.rate_limits) ?? asRecord(message?.rate_limits);
326
391
  }
392
+ async function buildLiveLimitWindows(options) {
393
+ const [usageOutput, subscriptionType] = await Promise.all([
394
+ readClaudeUsageCommandOutput(options.root, options.usageCommandKind, options.readUsageCommandOutput),
395
+ readClaudeSubscriptionType(options.root, options.usageCommandKind, options.readAuthStatusOutput)
396
+ ]);
397
+ const snapshots = parseLiveUsageWindowSnapshots(usageOutput, options.now);
398
+ const resolvedPlanType = subscriptionType || "live";
399
+ return {
400
+ primaryLimitWindows: snapshots
401
+ .filter((snapshot) => snapshot.scope === "primary")
402
+ .map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now)),
403
+ secondaryLimitWindows: snapshots
404
+ .filter((snapshot) => snapshot.scope === "secondary")
405
+ .map((snapshot) => buildLiveLimitWindowRow(snapshot, resolvedPlanType, options.selectedEvents, options.now))
406
+ };
407
+ }
408
+ async function readClaudeSubscriptionType(root, usageCommandKind, override) {
409
+ const output = await readClaudeAuthStatusOutput(root, usageCommandKind, override);
410
+ return parseClaudeSubscriptionType(output);
411
+ }
412
+ async function readClaudeAuthStatusOutput(root, usageCommandKind, override) {
413
+ if (override) {
414
+ try {
415
+ return await override();
416
+ }
417
+ catch {
418
+ return null;
419
+ }
420
+ }
421
+ const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
422
+ const cached = claudeAuthStatusOutputCache.get(cacheKey);
423
+ if (cached) {
424
+ return cached;
425
+ }
426
+ const pending = (async () => {
427
+ const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind);
428
+ if (!binaryPath) {
429
+ return null;
430
+ }
431
+ try {
432
+ const { stdout, stderr } = await execFileAsync(binaryPath, ["auth", "status"], {
433
+ encoding: "utf8",
434
+ maxBuffer: 1024 * 1024,
435
+ timeout: 15000,
436
+ windowsHide: true
437
+ });
438
+ const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
439
+ return combined || null;
440
+ }
441
+ catch (error) {
442
+ const combined = extractExecOutput(error);
443
+ return combined || null;
444
+ }
445
+ })();
446
+ claudeAuthStatusOutputCache.set(cacheKey, pending);
447
+ return pending;
448
+ }
449
+ function parseClaudeSubscriptionType(output) {
450
+ const snapshot = parseClaudeAuthStatusSnapshot(output);
451
+ return snapshot?.subscriptionType ?? null;
452
+ }
453
+ function parseClaudeAuthStatusSnapshot(output) {
454
+ if (!output) {
455
+ return null;
456
+ }
457
+ const normalizedOutput = output.replace(ANSI_ESCAPE_SEQUENCE, "").trim();
458
+ const firstBraceIndex = normalizedOutput.indexOf("{");
459
+ const lastBraceIndex = normalizedOutput.lastIndexOf("}");
460
+ if (firstBraceIndex < 0 || lastBraceIndex <= firstBraceIndex) {
461
+ return null;
462
+ }
463
+ try {
464
+ const payload = JSON.parse(normalizedOutput.slice(firstBraceIndex, lastBraceIndex + 1));
465
+ const record = asRecord(payload);
466
+ const email = typeof record?.email === "string" ? record.email.trim() : "";
467
+ const orgId = typeof record?.orgId === "string" ? record.orgId.trim() : "";
468
+ const orgName = typeof record?.orgName === "string" ? record.orgName.trim() : "";
469
+ const subscriptionType = typeof record?.subscriptionType === "string" ? record.subscriptionType.trim() : "";
470
+ if (!email || !orgId || !orgName || !subscriptionType) {
471
+ return null;
472
+ }
473
+ return { email, orgId, orgName, subscriptionType };
474
+ }
475
+ catch {
476
+ return null;
477
+ }
478
+ }
479
+ async function readClaudeUserIdHash(root, usageCommandKind, override, agentName) {
480
+ const authStatusOutput = await readClaudeAuthStatusOutput(root, usageCommandKind, override);
481
+ const snapshot = parseClaudeAuthStatusSnapshot(authStatusOutput);
482
+ if (!snapshot) {
483
+ return null;
484
+ }
485
+ return buildUserIdHash([agentName, snapshot.email, snapshot.orgId, snapshot.orgName]);
486
+ }
487
+ async function readClaudeUsageCommandOutput(root, usageCommandKind, override) {
488
+ if (override) {
489
+ try {
490
+ return await override();
491
+ }
492
+ catch {
493
+ return null;
494
+ }
495
+ }
496
+ const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
497
+ const cached = claudeUsageOutputCache.get(cacheKey);
498
+ if (cached) {
499
+ return cached;
500
+ }
501
+ const pending = (async () => {
502
+ const binaryPath = await resolveClaudeBinaryPath(root, usageCommandKind);
503
+ if (!binaryPath) {
504
+ return null;
505
+ }
506
+ try {
507
+ const { stdout, stderr } = await execFileAsync(binaryPath, ["-p", "/usage"], {
508
+ encoding: "utf8",
509
+ maxBuffer: 1024 * 1024,
510
+ timeout: 15000,
511
+ windowsHide: true
512
+ });
513
+ const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
514
+ return combined || null;
515
+ }
516
+ catch (error) {
517
+ const combined = extractExecOutput(error);
518
+ return combined || null;
519
+ }
520
+ })();
521
+ claudeUsageOutputCache.set(cacheKey, pending);
522
+ return pending;
523
+ }
524
+ function extractExecOutput(error) {
525
+ if (!error || typeof error !== "object") {
526
+ return "";
527
+ }
528
+ const stdout = typeof error.stdout === "string" ? error.stdout : "";
529
+ const stderr = typeof error.stderr === "string" ? error.stderr : "";
530
+ return [stdout, stderr].filter(Boolean).join("\n").trim();
531
+ }
532
+ async function resolveClaudeBinaryPath(root, usageCommandKind) {
533
+ const cacheKey = `${usageCommandKind}:${path.resolve(root)}`;
534
+ const cached = claudeBinaryPathCache.get(cacheKey);
535
+ if (cached) {
536
+ return cached;
537
+ }
538
+ const pending = usageCommandKind === "vscode"
539
+ ? resolveVsCodeClaudeBinaryPath(root)
540
+ : resolveCliClaudeBinaryPath(root);
541
+ claudeBinaryPathCache.set(cacheKey, pending);
542
+ return pending;
543
+ }
544
+ async function resolveVsCodeClaudeBinaryPath(root) {
545
+ const boosterDirectories = [
546
+ path.join(root, ".vscode", "extensions"),
547
+ path.join(root, ".vscode-server", "extensions")
548
+ ];
549
+ for (const directory of boosterDirectories) {
550
+ const binaryPath = await resolveClaudeBinaryFromExtensionDirectory(directory);
551
+ if (binaryPath) {
552
+ return binaryPath;
553
+ }
554
+ }
555
+ return null;
556
+ }
557
+ async function resolveClaudeBinaryFromExtensionDirectory(directory) {
558
+ let entries;
559
+ try {
560
+ entries = await fs.promises.readdir(directory, { withFileTypes: true });
561
+ }
562
+ catch {
563
+ return null;
564
+ }
565
+ const candidates = entries
566
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith(VSCODE_CLAUDE_EXTENSION_PREFIX))
567
+ .map((entry) => entry.name)
568
+ .sort(compareClaudeExtensionDirectoryNames);
569
+ for (const candidate of candidates) {
570
+ const binaryPath = path.join(directory, candidate, "resources", "native-binary", "claude");
571
+ if (await isReadableExecutableFile(binaryPath)) {
572
+ return binaryPath;
573
+ }
574
+ }
575
+ return null;
576
+ }
577
+ function compareClaudeExtensionDirectoryNames(left, right) {
578
+ const leftVersion = extractClaudeExtensionVersion(left);
579
+ const rightVersion = extractClaudeExtensionVersion(right);
580
+ const length = Math.max(leftVersion.length, rightVersion.length);
581
+ for (let index = 0; index < length; index += 1) {
582
+ const difference = (rightVersion[index] ?? 0) - (leftVersion[index] ?? 0);
583
+ if (difference !== 0) {
584
+ return difference;
585
+ }
586
+ }
587
+ return right.localeCompare(left);
588
+ }
589
+ function extractClaudeExtensionVersion(directoryName) {
590
+ if (!directoryName.startsWith(VSCODE_CLAUDE_EXTENSION_PREFIX)) {
591
+ return [];
592
+ }
593
+ const versionLabel = directoryName.slice(VSCODE_CLAUDE_EXTENSION_PREFIX.length).split("-")[0] ?? "";
594
+ return versionLabel
595
+ .split(".")
596
+ .map((part) => Number(part))
597
+ .filter((part) => Number.isFinite(part));
598
+ }
599
+ async function resolveCliClaudeBinaryPath(root) {
600
+ const directCandidates = [
601
+ path.join(root, ".local", "bin", "claude"),
602
+ path.join(root, "bin", "claude")
603
+ ];
604
+ for (const candidate of directCandidates) {
605
+ if (await isReadableExecutableFile(candidate)) {
606
+ return candidate;
607
+ }
608
+ }
609
+ return null;
610
+ }
611
+ async function isReadableExecutableFile(filePath) {
612
+ try {
613
+ await fs.promises.access(filePath, fs.constants.R_OK | fs.constants.X_OK);
614
+ return true;
615
+ }
616
+ catch {
617
+ return false;
618
+ }
619
+ }
620
+ function parseLiveUsageWindowSnapshots(usageOutput, now) {
621
+ if (!usageOutput) {
622
+ return [];
623
+ }
624
+ const snapshots = new Map();
625
+ const normalizedOutput = usageOutput.replace(ANSI_ESCAPE_SEQUENCE, "");
626
+ for (const line of normalizedOutput.split(/\r?\n/)) {
627
+ const match = line
628
+ .trim()
629
+ .match(/^Current\s+(session|week)(?:\s+\([^)]+\))?:\s+(\d+)%\s+used\b.*?\bresets\s+(.+)$/i);
630
+ if (!match) {
631
+ continue;
632
+ }
633
+ const label = match[1].toLowerCase() === "session" ? "session" : "week";
634
+ const usedPercent = Number(match[2]);
635
+ const windowMinutes = label === "session" ? CLAUDE_SESSION_WINDOW_MINUTES : CLAUDE_WEEK_WINDOW_MINUTES;
636
+ const resetsAtMs = parseResetTimestampUtc(match[3], now.getTime(), windowMinutes);
637
+ if (!Number.isFinite(usedPercent) || !resetsAtMs) {
638
+ continue;
639
+ }
640
+ snapshots.set(label, {
641
+ scope: label === "session" ? "primary" : "secondary",
642
+ label,
643
+ usedPercent,
644
+ resetsAtMs,
645
+ windowMinutes
646
+ });
647
+ }
648
+ return [...snapshots.values()].sort((left, right) => left.windowMinutes - right.windowMinutes);
649
+ }
650
+ function parseResetTimestampUtc(value, nowMs, windowMinutes) {
651
+ const match = value
652
+ .trim()
653
+ .match(/^([A-Za-z]{3})\s+(\d{1,2}),\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s+\(UTC\)$/i);
654
+ if (!match) {
655
+ return null;
656
+ }
657
+ const monthIndex = MONTH_INDEX_BY_LABEL[match[1].slice(0, 3).toLowerCase()];
658
+ const day = Number(match[2]);
659
+ const hour12 = Number(match[3]);
660
+ const minute = Number(match[4] ?? "0");
661
+ const meridiem = match[5].toLowerCase();
662
+ if (monthIndex === undefined ||
663
+ !Number.isFinite(day) ||
664
+ !Number.isFinite(hour12) ||
665
+ !Number.isFinite(minute) ||
666
+ day < 1 ||
667
+ day > 31 ||
668
+ hour12 < 1 ||
669
+ hour12 > 12 ||
670
+ minute < 0 ||
671
+ minute > 59) {
672
+ return null;
673
+ }
674
+ const hour24 = resolveHour24(hour12, meridiem);
675
+ const currentYear = new Date(nowMs).getUTCFullYear();
676
+ const candidates = [currentYear - 1, currentYear, currentYear + 1]
677
+ .map((year) => Date.UTC(year, monthIndex, day, hour24, minute))
678
+ .filter((candidate) => Number.isFinite(candidate));
679
+ const maxFutureMs = nowMs + windowMinutes * 60000 + 24 * 60 * 60 * 1000;
680
+ const plausibleFutureCandidate = candidates
681
+ .filter((candidate) => candidate >= nowMs - 60000 && candidate <= maxFutureMs)
682
+ .sort((left, right) => left - right)[0];
683
+ if (plausibleFutureCandidate !== undefined) {
684
+ return plausibleFutureCandidate;
685
+ }
686
+ const nearestCandidate = candidates.sort((left, right) => Math.abs(left - nowMs) - Math.abs(right - nowMs))[0];
687
+ return nearestCandidate ?? null;
688
+ }
689
+ function resolveHour24(hour12, meridiem) {
690
+ if (hour12 === 12) {
691
+ return meridiem === "am" ? 0 : 12;
692
+ }
693
+ return meridiem === "pm" ? hour12 + 12 : hour12;
694
+ }
695
+ function buildLiveLimitWindowRow(snapshot, planType, selectedEvents, now) {
696
+ const startTimeMs = snapshot.resetsAtMs - snapshot.windowMinutes * 60000;
697
+ const inWindowEvents = selectedEvents.filter((event) => Number.isFinite(event.timestampMs) &&
698
+ event.timestampMs >= startTimeMs &&
699
+ event.timestampMs < snapshot.resetsAtMs);
700
+ const totals = sumUsageTotals(inWindowEvents.map((event) => event.totals));
701
+ const fallbackLastSeenMs = Math.min(now.getTime(), snapshot.resetsAtMs);
702
+ const firstSeenMs = inWindowEvents.reduce((minimum, event) => Math.min(minimum, event.timestampMs), Number.POSITIVE_INFINITY);
703
+ const lastSeenMs = inWindowEvents.reduce((maximum, event) => Math.max(maximum, event.timestampMs), Number.NEGATIVE_INFINITY);
704
+ return {
705
+ scope: snapshot.scope,
706
+ planType,
707
+ limitId: `current-${snapshot.label}`,
708
+ windowMinutes: snapshot.windowMinutes,
709
+ startTimeUtcIso: toUtcIso(startTimeMs),
710
+ endTimeUtcIso: toUtcIso(snapshot.resetsAtMs),
711
+ firstSeenUtcIso: toUtcIso(Number.isFinite(firstSeenMs) ? firstSeenMs : startTimeMs),
712
+ lastSeenUtcIso: toUtcIso(Number.isFinite(lastSeenMs) ? lastSeenMs : fallbackLastSeenMs),
713
+ minUsedPercent: snapshot.usedPercent,
714
+ maxUsedPercent: snapshot.usedPercent,
715
+ totals,
716
+ eventCount: totals.eventCount
717
+ };
718
+ }
719
+ function toUtcIso(value) {
720
+ return new Date(value).toISOString().replace(".000Z", "Z");
721
+ }
722
+ function buildUserIdHash(parts) {
723
+ if (parts.some((part) => !part)) {
724
+ return null;
725
+ }
726
+ return createHash("md5").update(parts.join("-")).digest("hex");
727
+ }
728
+ function normalizeAnalyticsAgentName(label) {
729
+ return label.replace(/\s+/g, "");
730
+ }