letmecode 0.1.15 → 0.1.17

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.
@@ -97,8 +97,8 @@ export class ClaudeUsageProvider extends UsageProviderBase {
97
97
  }
98
98
  }
99
99
  const selectedEvents = [
100
- ...parsedEvents.keyedEvents.values(),
101
- ...parsedEvents.unkeyedEvents.values()
100
+ ...new Set(parsedEvents.keyedEvents.values()),
101
+ ...parsedEvents.unkeyedEvents
102
102
  ];
103
103
  traceClaude(options.traceLogger, [
104
104
  `Transcript selection summary: filesWithMatches=${parseTotals.filesScanned}/${parsedSessionFiles.length}`,
@@ -125,10 +125,10 @@ export class ClaudeUsageProvider extends UsageProviderBase {
125
125
  warnings.push(`Collapsed ${parsedEvents.duplicateUsageKeys} duplicate Claude usage event(s) by request/message key.`);
126
126
  }
127
127
  if (options.verbose && parsedEvents.duplicateUsageKeyCollisions > 0) {
128
- warnings.push(`Detected ${parsedEvents.duplicateUsageKeyCollisions} Claude usage key collision(s) with different token usage; keeping the highest-cost/latest event per key.`);
128
+ warnings.push(`Detected ${parsedEvents.duplicateUsageKeyCollisions} Claude usage key collision(s) with different token usage; merged same-key rows by per-field maxima to avoid double-counting cumulative snapshots.`);
129
129
  }
130
130
  if (options.verbose && parsedEvents.duplicateUnkeyedEvents > 0) {
131
- warnings.push(`Collapsed ${parsedEvents.duplicateUnkeyedEvents} duplicate unkeyed Claude usage event(s) by usage signature.`);
131
+ warnings.push(`Collapsed ${parsedEvents.duplicateUnkeyedEvents} adjacent duplicate unkeyed Claude usage event(s) by usage signature.`);
132
132
  }
133
133
  const modelUsage = [...byModel.entries()]
134
134
  .map(([modelId, totals]) => ({ modelId, totals }))
@@ -446,15 +446,18 @@ async function parseSessionFile(filePath, sessionsRoot) {
446
446
  const entrypoint = typeof payloadObject.entrypoint === "string" ? payloadObject.entrypoint : "";
447
447
  const rateLimits = extractRateLimits(payloadObject, message);
448
448
  const normalizedUsage = normalizeUsage(usage);
449
- const usageKey = buildUsageEventKey(payloadObject, message);
449
+ const usageKeys = buildUsageEventKeys(payloadObject, message);
450
450
  const usageSignature = buildUsageSignature(payloadObject, modelId, normalizedUsage);
451
451
  assistantEntryPoints.add(entrypoint);
452
452
  events.push({
453
453
  entrypoint,
454
- usageKey,
454
+ filePath,
455
+ lineNumber: linesRead,
456
+ usageKeys,
455
457
  usageSignature,
456
458
  timestampMs: eventTimeMs,
457
459
  modelId,
460
+ usage: normalizedUsage,
458
461
  totals: usageToTotals(modelId, normalizedUsage),
459
462
  rateLimits
460
463
  });
@@ -555,18 +558,21 @@ function extractStringArray(value) {
555
558
  }
556
559
  return value.filter((item) => typeof item === "string");
557
560
  }
558
- function buildUsageEventKey(payloadObject, message) {
561
+ function buildUsageEventKeys(payloadObject, message) {
559
562
  const sessionId = String(payloadObject.sessionId ?? "");
560
563
  const requestId = typeof payloadObject.requestId === "string" ? payloadObject.requestId : "";
561
564
  const messageId = typeof message?.id === "string" ? message.id : "";
562
- if (!requestId && !messageId) {
563
- return null;
564
- }
565
- return `${sessionId}|${requestId || messageId}`;
565
+ return [...new Set([
566
+ requestId ? `${sessionId}|request:${requestId}` : "",
567
+ messageId ? `${sessionId}|message:${messageId}` : ""
568
+ ].filter(Boolean))];
566
569
  }
567
570
  function buildUsageSignature(payloadObject, modelId, usage) {
571
+ return buildUsageSignatureFromParts(String(payloadObject.sessionId ?? ""), modelId, usage);
572
+ }
573
+ function buildUsageSignatureFromParts(sessionId, modelId, usage) {
568
574
  return [
569
- String(payloadObject.sessionId ?? ""),
575
+ sessionId,
570
576
  modelId,
571
577
  usage.inputTokens,
572
578
  usage.cacheCreationInputTokens,
@@ -580,46 +586,109 @@ function buildUsageSignature(payloadObject, modelId, usage) {
580
586
  function createParsedUsageEventAccumulator() {
581
587
  return {
582
588
  keyedEvents: new Map(),
583
- unkeyedEvents: new Map(),
589
+ unkeyedEvents: [],
590
+ lastUnkeyedEventsBySignature: new Map(),
584
591
  duplicateUsageKeys: 0,
585
592
  duplicateUsageKeyCollisions: 0,
586
593
  duplicateUnkeyedEvents: 0
587
594
  };
588
595
  }
589
596
  function recordParsedUsageEvent(parsedEvents, event) {
590
- if (event.usageKey) {
591
- const previous = parsedEvents.keyedEvents.get(event.usageKey);
592
- if (!previous) {
593
- parsedEvents.keyedEvents.set(event.usageKey, event);
597
+ if (event.usageKeys.length > 0) {
598
+ const previousMatches = [...new Set(event.usageKeys
599
+ .map((usageKey) => parsedEvents.keyedEvents.get(usageKey))
600
+ .filter((candidate) => Boolean(candidate)))];
601
+ if (previousMatches.length === 0) {
602
+ for (const usageKey of event.usageKeys) {
603
+ parsedEvents.keyedEvents.set(usageKey, event);
604
+ }
594
605
  return;
595
606
  }
596
607
  parsedEvents.duplicateUsageKeys += 1;
597
- if (previous.usageSignature !== event.usageSignature) {
608
+ const distinctUsageSignatures = new Set([
609
+ event.usageSignature,
610
+ ...previousMatches.map((candidate) => candidate.usageSignature)
611
+ ]);
612
+ if (distinctUsageSignatures.size > 1) {
598
613
  parsedEvents.duplicateUsageKeyCollisions += 1;
599
614
  }
600
- if (shouldReplaceUsageEvent(previous, event)) {
601
- parsedEvents.keyedEvents.set(event.usageKey, event);
615
+ const mergedEvent = previousMatches.reduce(mergeParsedUsageEvents, event);
616
+ for (const usageKey of mergedEvent.usageKeys) {
617
+ parsedEvents.keyedEvents.set(usageKey, mergedEvent);
602
618
  }
603
619
  return;
604
620
  }
605
- const previous = parsedEvents.unkeyedEvents.get(event.usageSignature);
606
- if (!previous) {
607
- parsedEvents.unkeyedEvents.set(event.usageSignature, event);
621
+ const previousRecord = parsedEvents.lastUnkeyedEventsBySignature.get(event.usageSignature);
622
+ if (!previousRecord || !canCollapseAdjacentUnkeyedUsageEvents(previousRecord.event, event)) {
623
+ parsedEvents.unkeyedEvents.push(event);
624
+ parsedEvents.lastUnkeyedEventsBySignature.set(event.usageSignature, {
625
+ event,
626
+ index: parsedEvents.unkeyedEvents.length - 1
627
+ });
608
628
  return;
609
629
  }
610
630
  parsedEvents.duplicateUnkeyedEvents += 1;
611
- if (shouldReplaceUsageEvent(previous, event)) {
612
- parsedEvents.unkeyedEvents.set(event.usageSignature, event);
631
+ if (normalizeTimestamp(event.timestampMs) > normalizeTimestamp(previousRecord.event.timestampMs)) {
632
+ parsedEvents.unkeyedEvents[previousRecord.index] = event;
633
+ parsedEvents.lastUnkeyedEventsBySignature.set(event.usageSignature, {
634
+ event,
635
+ index: previousRecord.index
636
+ });
613
637
  }
614
638
  }
615
- function shouldReplaceUsageEvent(previous, next) {
616
- if (next.totals.estimatedCredits > previous.totals.estimatedCredits) {
617
- return true;
639
+ function mergeParsedUsageEvents(previous, next) {
640
+ const mergedUsage = mergeClaudeUsage(previous.usage, next.usage);
641
+ const modelId = selectMergedEventModelId(previous, next);
642
+ const latestEvent = normalizeTimestamp(next.timestampMs) >= normalizeTimestamp(previous.timestampMs) ? next : previous;
643
+ const sessionId = extractUsageKeySessionId(previous.usageKeys) || extractUsageKeySessionId(next.usageKeys);
644
+ return {
645
+ entrypoint: latestEvent.entrypoint || previous.entrypoint || next.entrypoint,
646
+ filePath: latestEvent.filePath,
647
+ lineNumber: latestEvent.lineNumber,
648
+ usageKeys: [...new Set([...previous.usageKeys, ...next.usageKeys])],
649
+ usageSignature: buildUsageSignatureFromParts(sessionId, modelId, mergedUsage),
650
+ timestampMs: Math.max(normalizeTimestamp(previous.timestampMs), normalizeTimestamp(next.timestampMs)),
651
+ modelId,
652
+ usage: mergedUsage,
653
+ totals: usageToTotals(modelId, mergedUsage),
654
+ rateLimits: latestEvent.rateLimits ?? previous.rateLimits ?? next.rateLimits
655
+ };
656
+ }
657
+ function mergeClaudeUsage(previous, next) {
658
+ return {
659
+ inputTokens: Math.max(previous.inputTokens, next.inputTokens),
660
+ cacheReadInputTokens: Math.max(previous.cacheReadInputTokens, next.cacheReadInputTokens),
661
+ cacheCreationInputTokens: Math.max(previous.cacheCreationInputTokens, next.cacheCreationInputTokens),
662
+ cacheCreation5mInputTokens: Math.max(previous.cacheCreation5mInputTokens, next.cacheCreation5mInputTokens),
663
+ cacheCreation1hInputTokens: Math.max(previous.cacheCreation1hInputTokens, next.cacheCreation1hInputTokens),
664
+ outputTokens: Math.max(previous.outputTokens, next.outputTokens),
665
+ inferenceGeo: next.inferenceGeo || previous.inferenceGeo
666
+ };
667
+ }
668
+ function selectMergedEventModelId(previous, next) {
669
+ if (previous.modelId === next.modelId) {
670
+ return previous.modelId;
671
+ }
672
+ if (isInternalClaudeModel(previous.modelId) && !isInternalClaudeModel(next.modelId)) {
673
+ return next.modelId;
618
674
  }
619
- if (next.totals.estimatedCredits === previous.totals.estimatedCredits) {
620
- return normalizeTimestamp(next.timestampMs) > normalizeTimestamp(previous.timestampMs);
675
+ if (isInternalClaudeModel(next.modelId) && !isInternalClaudeModel(previous.modelId)) {
676
+ return previous.modelId;
677
+ }
678
+ return next.totals.estimatedCredits >= previous.totals.estimatedCredits
679
+ ? next.modelId
680
+ : previous.modelId;
681
+ }
682
+ function canCollapseAdjacentUnkeyedUsageEvents(previous, next) {
683
+ return previous.filePath === next.filePath && next.lineNumber === previous.lineNumber + 1;
684
+ }
685
+ function extractUsageKeySessionId(usageKeys) {
686
+ const usageKey = usageKeys[0];
687
+ if (!usageKey) {
688
+ return "";
621
689
  }
622
- return false;
690
+ const separatorIndex = usageKey.indexOf("|");
691
+ return separatorIndex >= 0 ? usageKey.slice(0, separatorIndex) : usageKey;
623
692
  }
624
693
  function normalizeTimestamp(value) {
625
694
  return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Provider-based terminal usage dashboard for LetMeCode.",
5
5
  "author": "devforth.io",
6
6
  "license": "MIT",