@vibescore/tracker 0.1.0 → 0.1.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibescore/tracker",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -469,49 +469,253 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
469
469
  }
470
470
  if (touchedGroups.size === 0) return 0;
471
471
 
472
- const grouped = new Map();
472
+ const groupQueued = hourlyState.groupQueued && typeof hourlyState.groupQueued === 'object' ? hourlyState.groupQueued : {};
473
+ let codexTouched = false;
474
+ const legacyGroups = new Set();
475
+ for (const groupKey of touchedGroups) {
476
+ if (Object.prototype.hasOwnProperty.call(groupQueued, groupKey)) {
477
+ legacyGroups.add(groupKey);
478
+ }
479
+ if (!codexTouched && groupKey.startsWith(`${DEFAULT_SOURCE}${BUCKET_SEPARATOR}`)) {
480
+ codexTouched = true;
481
+ }
482
+ }
483
+
484
+ const groupedBuckets = new Map();
473
485
  for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
474
486
  if (!bucket || !bucket.totals) continue;
475
487
  const parsed = parseBucketKey(key);
476
488
  const hourStart = parsed.hourStart;
477
489
  if (!hourStart) continue;
478
490
  const groupKey = groupBucketKey(parsed.source, hourStart);
479
- if (!touchedGroups.has(groupKey)) continue;
491
+ if (!touchedGroups.has(groupKey) || legacyGroups.has(groupKey)) continue;
480
492
 
481
- let group = grouped.get(groupKey);
493
+ const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
494
+ const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
495
+ let group = groupedBuckets.get(groupKey);
482
496
  if (!group) {
483
- group = {
484
- source: normalizeSourceInput(parsed.source) || DEFAULT_SOURCE,
485
- hourStart,
486
- models: new Set(),
487
- totals: initTotals()
488
- };
489
- grouped.set(groupKey, group);
497
+ group = { source, hourStart, buckets: new Map() };
498
+ groupedBuckets.set(groupKey, group);
499
+ }
500
+
501
+ if (bucket.queuedKey != null && typeof bucket.queuedKey !== 'string') {
502
+ bucket.queuedKey = null;
503
+ }
504
+ group.buckets.set(model, bucket);
505
+ }
506
+
507
+ if (codexTouched) {
508
+ const recomputeGroups = new Set();
509
+ for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
510
+ if (!bucket || !bucket.totals) continue;
511
+ const parsed = parseBucketKey(key);
512
+ const hourStart = parsed.hourStart;
513
+ if (!hourStart) continue;
514
+ const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
515
+ if (source !== 'every-code') continue;
516
+ const groupKey = groupBucketKey(source, hourStart);
517
+ if (legacyGroups.has(groupKey) || groupedBuckets.has(groupKey)) continue;
518
+ const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
519
+ if (model !== DEFAULT_MODEL) continue;
520
+ recomputeGroups.add(groupKey);
521
+ }
522
+
523
+ if (recomputeGroups.size > 0) {
524
+ for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
525
+ if (!bucket || !bucket.totals) continue;
526
+ const parsed = parseBucketKey(key);
527
+ const hourStart = parsed.hourStart;
528
+ if (!hourStart) continue;
529
+ const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
530
+ const groupKey = groupBucketKey(source, hourStart);
531
+ if (!recomputeGroups.has(groupKey)) continue;
532
+ let group = groupedBuckets.get(groupKey);
533
+ if (!group) {
534
+ group = { source, hourStart, buckets: new Map() };
535
+ groupedBuckets.set(groupKey, group);
536
+ }
537
+ if (bucket.queuedKey != null && typeof bucket.queuedKey !== 'string') {
538
+ bucket.queuedKey = null;
539
+ }
540
+ const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
541
+ group.buckets.set(model, bucket);
542
+ }
490
543
  }
491
- group.models.add(parsed.model || DEFAULT_MODEL);
492
- addTotals(group.totals, bucket.totals);
493
544
  }
494
545
 
546
+ const codexDominants = collectCodexDominantModels(hourlyState);
547
+
495
548
  const toAppend = [];
496
- const groupQueued = hourlyState.groupQueued && typeof hourlyState.groupQueued === 'object' ? hourlyState.groupQueued : {};
497
- for (const group of grouped.values()) {
498
- const model = group.models.size === 1 ? [...group.models][0] : DEFAULT_MODEL;
499
- const key = totalsKey(group.totals);
500
- const groupKey = groupBucketKey(group.source, group.hourStart);
501
- if (groupQueued[groupKey] === key) continue;
549
+ for (const group of groupedBuckets.values()) {
550
+ const unknownBucket = group.buckets.get(DEFAULT_MODEL) || null;
551
+ const dominantModel = pickDominantModel(group.buckets);
552
+ let alignedModel = null;
553
+ if (unknownBucket?.alignedModel) {
554
+ const normalized = normalizeModelInput(unknownBucket.alignedModel);
555
+ alignedModel = normalized && normalized !== DEFAULT_MODEL ? normalized : null;
556
+ }
557
+ const zeroTotals = initTotals();
558
+ const zeroKey = totalsKey(zeroTotals);
559
+
560
+ if (dominantModel) {
561
+ if (alignedModel && !group.buckets.has(alignedModel)) {
562
+ toAppend.push(
563
+ JSON.stringify({
564
+ source: group.source,
565
+ model: alignedModel,
566
+ hour_start: group.hourStart,
567
+ input_tokens: zeroTotals.input_tokens,
568
+ cached_input_tokens: zeroTotals.cached_input_tokens,
569
+ output_tokens: zeroTotals.output_tokens,
570
+ reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
571
+ total_tokens: zeroTotals.total_tokens
572
+ })
573
+ );
574
+ }
575
+ if (unknownBucket && !alignedModel && unknownBucket.queuedKey && unknownBucket.queuedKey !== zeroKey) {
576
+ if (unknownBucket.retractedUnknownKey !== zeroKey) {
577
+ toAppend.push(
578
+ JSON.stringify({
579
+ source: group.source,
580
+ model: DEFAULT_MODEL,
581
+ hour_start: group.hourStart,
582
+ input_tokens: zeroTotals.input_tokens,
583
+ cached_input_tokens: zeroTotals.cached_input_tokens,
584
+ output_tokens: zeroTotals.output_tokens,
585
+ reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
586
+ total_tokens: zeroTotals.total_tokens
587
+ })
588
+ );
589
+ unknownBucket.retractedUnknownKey = zeroKey;
590
+ }
591
+ }
592
+ if (unknownBucket) unknownBucket.alignedModel = null;
593
+ for (const [model, bucket] of group.buckets.entries()) {
594
+ if (model === DEFAULT_MODEL) continue;
595
+ let totals = bucket.totals;
596
+ if (model === dominantModel && unknownBucket?.totals) {
597
+ totals = cloneTotals(bucket.totals);
598
+ addTotals(totals, unknownBucket.totals);
599
+ }
600
+ const key = totalsKey(totals);
601
+ if (bucket.queuedKey === key) continue;
602
+ toAppend.push(
603
+ JSON.stringify({
604
+ source: group.source,
605
+ model,
606
+ hour_start: group.hourStart,
607
+ input_tokens: totals.input_tokens,
608
+ cached_input_tokens: totals.cached_input_tokens,
609
+ output_tokens: totals.output_tokens,
610
+ reasoning_output_tokens: totals.reasoning_output_tokens,
611
+ total_tokens: totals.total_tokens
612
+ })
613
+ );
614
+ bucket.queuedKey = key;
615
+ }
616
+ continue;
617
+ }
618
+
619
+ if (!unknownBucket?.totals) continue;
620
+ let outputModel = DEFAULT_MODEL;
621
+ if (group.source === 'every-code') {
622
+ const aligned = findNearestCodexModel(group.hourStart, codexDominants);
623
+ if (aligned) outputModel = aligned;
624
+ }
625
+ const nextAligned = outputModel !== DEFAULT_MODEL ? outputModel : null;
626
+ if (alignedModel && alignedModel !== nextAligned) {
627
+ toAppend.push(
628
+ JSON.stringify({
629
+ source: group.source,
630
+ model: alignedModel,
631
+ hour_start: group.hourStart,
632
+ input_tokens: zeroTotals.input_tokens,
633
+ cached_input_tokens: zeroTotals.cached_input_tokens,
634
+ output_tokens: zeroTotals.output_tokens,
635
+ reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
636
+ total_tokens: zeroTotals.total_tokens
637
+ })
638
+ );
639
+ }
640
+ if (!alignedModel && nextAligned && unknownBucket.queuedKey && unknownBucket.queuedKey !== zeroKey) {
641
+ if (unknownBucket.retractedUnknownKey !== zeroKey) {
642
+ toAppend.push(
643
+ JSON.stringify({
644
+ source: group.source,
645
+ model: DEFAULT_MODEL,
646
+ hour_start: group.hourStart,
647
+ input_tokens: zeroTotals.input_tokens,
648
+ cached_input_tokens: zeroTotals.cached_input_tokens,
649
+ output_tokens: zeroTotals.output_tokens,
650
+ reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
651
+ total_tokens: zeroTotals.total_tokens
652
+ })
653
+ );
654
+ unknownBucket.retractedUnknownKey = zeroKey;
655
+ }
656
+ }
657
+ if (unknownBucket) unknownBucket.alignedModel = nextAligned;
658
+ const key = totalsKey(unknownBucket.totals);
659
+ const outputKey = outputModel === DEFAULT_MODEL ? key : `${key}|${outputModel}`;
660
+ if (unknownBucket.queuedKey === outputKey) continue;
502
661
  toAppend.push(
503
662
  JSON.stringify({
504
663
  source: group.source,
505
- model,
664
+ model: outputModel,
506
665
  hour_start: group.hourStart,
507
- input_tokens: group.totals.input_tokens,
508
- cached_input_tokens: group.totals.cached_input_tokens,
509
- output_tokens: group.totals.output_tokens,
510
- reasoning_output_tokens: group.totals.reasoning_output_tokens,
511
- total_tokens: group.totals.total_tokens
666
+ input_tokens: unknownBucket.totals.input_tokens,
667
+ cached_input_tokens: unknownBucket.totals.cached_input_tokens,
668
+ output_tokens: unknownBucket.totals.output_tokens,
669
+ reasoning_output_tokens: unknownBucket.totals.reasoning_output_tokens,
670
+ total_tokens: unknownBucket.totals.total_tokens
512
671
  })
513
672
  );
514
- groupQueued[groupKey] = key;
673
+ unknownBucket.queuedKey = outputKey;
674
+ }
675
+
676
+ if (legacyGroups.size > 0) {
677
+ const grouped = new Map();
678
+ for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
679
+ if (!bucket || !bucket.totals) continue;
680
+ const parsed = parseBucketKey(key);
681
+ const hourStart = parsed.hourStart;
682
+ if (!hourStart) continue;
683
+ const groupKey = groupBucketKey(parsed.source, hourStart);
684
+ if (!legacyGroups.has(groupKey)) continue;
685
+
686
+ let group = grouped.get(groupKey);
687
+ if (!group) {
688
+ group = {
689
+ source: normalizeSourceInput(parsed.source) || DEFAULT_SOURCE,
690
+ hourStart,
691
+ models: new Set(),
692
+ totals: initTotals()
693
+ };
694
+ grouped.set(groupKey, group);
695
+ }
696
+ group.models.add(parsed.model || DEFAULT_MODEL);
697
+ addTotals(group.totals, bucket.totals);
698
+ }
699
+
700
+ for (const group of grouped.values()) {
701
+ const model = group.models.size === 1 ? [...group.models][0] : DEFAULT_MODEL;
702
+ const key = totalsKey(group.totals);
703
+ const groupKey = groupBucketKey(group.source, group.hourStart);
704
+ if (groupQueued[groupKey] === key) continue;
705
+ toAppend.push(
706
+ JSON.stringify({
707
+ source: group.source,
708
+ model,
709
+ hour_start: group.hourStart,
710
+ input_tokens: group.totals.input_tokens,
711
+ cached_input_tokens: group.totals.cached_input_tokens,
712
+ output_tokens: group.totals.output_tokens,
713
+ reasoning_output_tokens: group.totals.reasoning_output_tokens,
714
+ total_tokens: group.totals.total_tokens
715
+ })
716
+ );
717
+ groupQueued[groupKey] = key;
718
+ }
515
719
  }
516
720
 
517
721
  hourlyState.groupQueued = groupQueued;
@@ -523,6 +727,91 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
523
727
  return toAppend.length;
524
728
  }
525
729
 
730
+ function pickDominantModel(buckets) {
731
+ let dominantModel = null;
732
+ let dominantTotal = -1;
733
+ for (const [model, bucket] of buckets.entries()) {
734
+ if (model === DEFAULT_MODEL) continue;
735
+ const total = Number(bucket?.totals?.total_tokens || 0);
736
+ if (
737
+ dominantModel == null ||
738
+ total > dominantTotal ||
739
+ (total === dominantTotal && model < dominantModel)
740
+ ) {
741
+ dominantModel = model;
742
+ dominantTotal = total;
743
+ }
744
+ }
745
+ return dominantModel;
746
+ }
747
+
748
+ function cloneTotals(totals) {
749
+ const cloned = initTotals();
750
+ addTotals(cloned, totals || {});
751
+ return cloned;
752
+ }
753
+
754
+ function collectCodexDominantModels(hourlyState) {
755
+ const grouped = new Map();
756
+ for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
757
+ if (!bucket || !bucket.totals) continue;
758
+ const parsed = parseBucketKey(key);
759
+ const hourStart = parsed.hourStart;
760
+ if (!hourStart) continue;
761
+ const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
762
+ if (source !== DEFAULT_SOURCE) continue;
763
+ const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
764
+ if (model === DEFAULT_MODEL) continue;
765
+
766
+ let models = grouped.get(hourStart);
767
+ if (!models) {
768
+ models = new Map();
769
+ grouped.set(hourStart, models);
770
+ }
771
+ const total = Number(bucket.totals.total_tokens || 0);
772
+ models.set(model, (models.get(model) || 0) + total);
773
+ }
774
+
775
+ const dominants = [];
776
+ for (const [hourStart, models] of grouped.entries()) {
777
+ let dominantModel = null;
778
+ let dominantTotal = -1;
779
+ for (const [model, total] of models.entries()) {
780
+ if (
781
+ dominantModel == null ||
782
+ total > dominantTotal ||
783
+ (total === dominantTotal && model < dominantModel)
784
+ ) {
785
+ dominantModel = model;
786
+ dominantTotal = total;
787
+ }
788
+ }
789
+ if (dominantModel) {
790
+ dominants.push({ hourStart, model: dominantModel });
791
+ }
792
+ }
793
+
794
+ return dominants;
795
+ }
796
+
797
+ function findNearestCodexModel(hourStart, dominants) {
798
+ if (!hourStart || !dominants || dominants.length === 0) return null;
799
+ const target = Date.parse(hourStart);
800
+ if (!Number.isFinite(target)) return null;
801
+
802
+ let best = null;
803
+ for (const entry of dominants) {
804
+ const candidate = Date.parse(entry.hourStart);
805
+ if (!Number.isFinite(candidate)) continue;
806
+ const diff = Math.abs(candidate - target);
807
+ if (!best || diff < best.diff || (diff === best.diff && candidate < best.time)) {
808
+ best = { diff, time: candidate, model: entry.model };
809
+ }
810
+ }
811
+
812
+ return best ? best.model : null;
813
+ }
814
+
526
815
  function normalizeHourlyState(raw) {
527
816
  const state = raw && typeof raw === 'object' ? raw : {};
528
817
  const version = Number(state.version || 1);
@@ -78,7 +78,7 @@ async function readBatch(queuePath, startOffset, maxBuckets) {
78
78
  const model = normalizeModel(bucket?.model) || DEFAULT_MODEL;
79
79
  bucket.source = source;
80
80
  bucket.model = model;
81
- bucketMap.set(bucketKey(source, hourStart), bucket);
81
+ bucketMap.set(bucketKey(source, model, hourStart), bucket);
82
82
  linesRead += 1;
83
83
  if (linesRead >= maxBuckets) break;
84
84
  }
@@ -97,8 +97,8 @@ async function safeFileSize(p) {
97
97
  }
98
98
  }
99
99
 
100
- function bucketKey(source, hourStart) {
101
- return `${source}${BUCKET_SEPARATOR}${hourStart}`;
100
+ function bucketKey(source, model, hourStart) {
101
+ return `${source}${BUCKET_SEPARATOR}${model}${BUCKET_SEPARATOR}${hourStart}`;
102
102
  }
103
103
 
104
104
  function normalizeSource(value) {