vibestats 1.3.9 → 1.3.10

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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +319 -191
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,6 +18,8 @@ vibestats # Daily usage table
18
18
  vibestats --monthly # Monthly aggregation
19
19
  vibestats --model # Aggregate by model
20
20
  vibestats --total # Show only totals
21
+ vibestats --kimi # Kimi family stats only
22
+ vibestats --minimax # MiniMax family stats only
21
23
 
22
24
  # Wrapped summary
23
25
  vibestats --wrapped # Annual wrapped summary
@@ -37,6 +39,8 @@ vibestats --claude-limits
37
39
  | `--model` | Aggregate by model |
38
40
  | `--sessions` | Aggregate by session |
39
41
  | `--total` | Show only totals |
42
+ | `--kimi` | Show only Kimi family stats |
43
+ | `--minimax` | Show only MiniMax family stats |
40
44
  | `--since YYYY-MM-DD` | Filter from date |
41
45
  | `--until YYYY-MM-DD` | Filter to date |
42
46
  | `--last N` | Last N days (shorthand: `--last7`, `--last30`, etc.) |
package/dist/index.js CHANGED
@@ -280,6 +280,195 @@ function calculateCodexCost(modelName, inputTokens, outputTokens, cachedInputTok
280
280
  return inputCost + cachedCost + outputCost;
281
281
  }
282
282
 
283
+ // src/pricing.ts
284
+ var MODEL_PRICING = {
285
+ // Fireworks router pricing as of 2026-03-30:
286
+ // https://fireworks.ai/pricing
287
+ // Fireworks exposes a single "cached input" rate, so cache creation
288
+ // tokens are treated as regular input and cache reads use the discounted rate.
289
+ "accounts/fireworks/routers/kimi-k2p5-turbo": {
290
+ input: 0.99,
291
+ output: 4.94,
292
+ cacheWrite: 0.99,
293
+ cacheRead: 0.16
294
+ },
295
+ // Fireworks prices the MiniMax M2 family at one rate. We apply that
296
+ // official family pricing to MiniMax-M2.7 because Fireworks does not
297
+ // publish a distinct M2.7 per-token row.
298
+ "MiniMax-M2.7": {
299
+ input: 0.3,
300
+ output: 1.2,
301
+ cacheWrite: 0.3,
302
+ cacheRead: 0.03
303
+ },
304
+ // Opus 4.6 (same pricing as Opus 4.5)
305
+ "claude-opus-4-6-20260101": {
306
+ input: 5,
307
+ output: 25,
308
+ cacheWrite: 6.25,
309
+ cacheRead: 0.5
310
+ },
311
+ // Opus 4.5 (cheaper than Opus 4.1)
312
+ "claude-opus-4-5-20251101": {
313
+ input: 5,
314
+ output: 25,
315
+ cacheWrite: 6.25,
316
+ cacheRead: 0.5
317
+ },
318
+ // Sonnet 4.6 (same pricing as Sonnet 4.5)
319
+ "claude-sonnet-4-6-20260101": {
320
+ input: 3,
321
+ output: 15,
322
+ cacheWrite: 3.75,
323
+ cacheRead: 0.3
324
+ },
325
+ // Sonnet 4.5
326
+ "claude-sonnet-4-5-20250929": {
327
+ input: 3,
328
+ output: 15,
329
+ cacheWrite: 3.75,
330
+ cacheRead: 0.3
331
+ },
332
+ // Opus 4.1 (MORE expensive than Opus 4.5)
333
+ "claude-opus-4-1-20250805": {
334
+ input: 15,
335
+ output: 75,
336
+ cacheWrite: 18.75,
337
+ cacheRead: 1.5
338
+ },
339
+ // Haiku 4.5
340
+ "claude-haiku-4-5-20251001": {
341
+ input: 1,
342
+ output: 5,
343
+ cacheWrite: 1.25,
344
+ cacheRead: 0.1
345
+ },
346
+ // Sonnet 3.5 (legacy - same as Sonnet 4.5)
347
+ "claude-3-5-sonnet-20241022": {
348
+ input: 3,
349
+ output: 15,
350
+ cacheWrite: 3.75,
351
+ cacheRead: 0.3
352
+ },
353
+ "claude-3-5-sonnet-20240620": {
354
+ input: 3,
355
+ output: 15,
356
+ cacheWrite: 3.75,
357
+ cacheRead: 0.3
358
+ },
359
+ // Haiku 3.5 (legacy - same as Haiku 4.5)
360
+ "claude-3-5-haiku-20241022": {
361
+ input: 1,
362
+ output: 5,
363
+ cacheWrite: 1.25,
364
+ cacheRead: 0.1
365
+ }
366
+ };
367
+ function normalizeModelName(modelName) {
368
+ return modelName.trim().toLowerCase();
369
+ }
370
+ function hasModelVersion(normalized, family, version) {
371
+ const versionPattern = version.replace(/\./g, "[\\s._-]*");
372
+ const familyPattern = family.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
373
+ const boundary = "(?:^|[^a-z0-9])";
374
+ const tailBoundary = "(?:[^a-z0-9]|$)";
375
+ return new RegExp(`${boundary}${familyPattern}(?:[^a-z0-9]+)?${versionPattern}${tailBoundary}`).test(normalized) || new RegExp(`${boundary}${versionPattern}(?:[^a-z0-9]+)?${familyPattern}${tailBoundary}`).test(normalized);
376
+ }
377
+ function isKimiK25Turbo(normalized) {
378
+ return normalized.includes("kimi-code") || normalized.includes("accounts/fireworks/routers/kimi-k2p5-turbo") || normalized.includes("accounts/fireworks/models/kimi-k2p5-turbo") || normalized.includes("custom:kimi-k2.5-turbo") || normalized.includes("custom:kimi-k2p5-turbo") || normalized.includes("kimi") && (normalized.includes("k2.5") || normalized.includes("k2p5")) && normalized.includes("turbo");
379
+ }
380
+ function isMiniMaxM27(normalized) {
381
+ return normalized.includes("minimax-code") || normalized.includes("custom:minimax-m2.7") || normalized.includes("custom:minimax-m2-7") || normalized.includes("minimax-m2.7") || normalized.includes("minimax-m2-7");
382
+ }
383
+ function isKimiFamilyModel(modelName) {
384
+ const normalized = normalizeModelName(modelName);
385
+ return isKimiK25Turbo(normalized) || normalized.includes("kimi");
386
+ }
387
+ function isMiniMaxFamilyModel(modelName) {
388
+ const normalized = normalizeModelName(modelName);
389
+ return isMiniMaxM27(normalized) || normalized.includes("minimax");
390
+ }
391
+ function matchesModelFamilies(modelName, families) {
392
+ if (!families || families.length === 0) return true;
393
+ return families.some((family) => {
394
+ if (family === "kimi") return isKimiFamilyModel(modelName);
395
+ if (family === "minimax") return isMiniMaxFamilyModel(modelName);
396
+ return false;
397
+ });
398
+ }
399
+ function formatModelFamilyLabel(families) {
400
+ if (!families || families.length === 0) return void 0;
401
+ const unique = Array.from(new Set(families));
402
+ const labels = unique.map((family) => family === "kimi" ? "Kimi" : "MiniMax");
403
+ if (labels.length === 0) return void 0;
404
+ if (labels.length === 1) return labels[0];
405
+ return labels.join(" + ");
406
+ }
407
+ function tryGetModelPricing(modelName) {
408
+ if (MODEL_PRICING[modelName]) {
409
+ return MODEL_PRICING[modelName];
410
+ }
411
+ const normalized = normalizeModelName(modelName);
412
+ if (isKimiK25Turbo(normalized)) {
413
+ return MODEL_PRICING["accounts/fireworks/routers/kimi-k2p5-turbo"];
414
+ }
415
+ if (isMiniMaxM27(normalized)) {
416
+ return MODEL_PRICING["MiniMax-M2.7"];
417
+ }
418
+ if (hasModelVersion(normalized, "opus", "4.6")) {
419
+ return MODEL_PRICING["claude-opus-4-6-20260101"];
420
+ }
421
+ if (hasModelVersion(normalized, "opus", "4.5")) {
422
+ return MODEL_PRICING["claude-opus-4-5-20251101"];
423
+ }
424
+ if (hasModelVersion(normalized, "opus", "4.1") || normalized.includes("opus-4")) {
425
+ return MODEL_PRICING["claude-opus-4-1-20250805"];
426
+ }
427
+ if (hasModelVersion(normalized, "sonnet", "4.6")) {
428
+ return MODEL_PRICING["claude-sonnet-4-6-20260101"];
429
+ }
430
+ if (hasModelVersion(normalized, "sonnet", "4.5")) {
431
+ return MODEL_PRICING["claude-sonnet-4-5-20250929"];
432
+ }
433
+ if (hasModelVersion(normalized, "sonnet", "3.5") || normalized.includes("sonnet")) {
434
+ return MODEL_PRICING["claude-3-5-sonnet-20241022"];
435
+ }
436
+ if (hasModelVersion(normalized, "haiku", "4.5")) {
437
+ return MODEL_PRICING["claude-haiku-4-5-20251001"];
438
+ }
439
+ if (hasModelVersion(normalized, "haiku", "3.5") || normalized.includes("haiku")) {
440
+ return MODEL_PRICING["claude-3-5-haiku-20241022"];
441
+ }
442
+ return null;
443
+ }
444
+ function getModelDisplayName(modelName) {
445
+ const normalized = normalizeModelName(modelName);
446
+ if (normalized.includes("kimi-code")) {
447
+ return "Kimi K2.5 Turbo";
448
+ }
449
+ if (normalized.includes("minimax-code")) {
450
+ return "MiniMax-M2.7";
451
+ }
452
+ if (isKimiK25Turbo(normalized)) {
453
+ return "Kimi K2.5 Turbo";
454
+ }
455
+ if (isMiniMaxM27(normalized)) {
456
+ return "MiniMax-M2.7";
457
+ }
458
+ if (hasModelVersion(normalized, "opus", "4.6")) return "Opus 4.6";
459
+ if (hasModelVersion(normalized, "opus", "4.5")) return "Opus 4.5";
460
+ if (hasModelVersion(normalized, "opus", "4.1")) return "Opus 4.1";
461
+ if (normalized.includes("opus")) return "Opus";
462
+ if (hasModelVersion(normalized, "sonnet", "4.6")) return "Sonnet 4.6";
463
+ if (hasModelVersion(normalized, "sonnet", "4.5")) return "Sonnet 4.5";
464
+ if (hasModelVersion(normalized, "sonnet", "3.5")) return "Sonnet 3.5";
465
+ if (normalized.includes("sonnet")) return "Sonnet";
466
+ if (hasModelVersion(normalized, "haiku", "4.5")) return "Haiku 4.5";
467
+ if (hasModelVersion(normalized, "haiku", "3.5")) return "Haiku 3.5";
468
+ if (normalized.includes("haiku")) return "Haiku";
469
+ return modelName;
470
+ }
471
+
283
472
  // src/url-encoder.ts
284
473
  function formatCompactNumber(num) {
285
474
  if (num >= 1e9) {
@@ -634,14 +823,14 @@ function buildActivityGraph(stats, metric, requestedDays = 365) {
634
823
  }
635
824
  };
636
825
  }
637
- function buildActivityTitle(source, metric) {
638
- const sourceLabel = source === "codex" ? "Codex" : source === "combined" ? "AI Coding" : "Local AI";
826
+ function buildActivityTitle(stats, metric) {
827
+ const sourceLabel = stats.scopeLabel ? stats.scopeLabel : stats.source === "codex" ? "Codex" : stats.source === "combined" ? "AI Coding" : "Local AI";
639
828
  const metricLabel = metric === "tokens" ? "Activity" : metric === "sessions" ? "Session Activity" : "Message Activity";
640
829
  return `${sourceLabel} ${metricLabel}`;
641
830
  }
642
831
  function buildActivityArtifactPayload(stats, metric, requestedDays = 365) {
643
832
  return {
644
- title: buildActivityTitle(stats.source, metric),
833
+ title: buildActivityTitle(stats, metric),
645
834
  source: stats.source,
646
835
  activity: buildActivityGraph(stats, metric, requestedDays)
647
836
  };
@@ -671,151 +860,6 @@ import { promises as fs } from "fs";
671
860
  import { homedir } from "os";
672
861
  import { basename, join } from "path";
673
862
  import { promisify } from "util";
674
-
675
- // src/pricing.ts
676
- var MODEL_PRICING = {
677
- // Fireworks router pricing as of 2026-03-30:
678
- // https://fireworks.ai/pricing
679
- // Fireworks exposes a single "cached input" rate, so cache creation
680
- // tokens are treated as regular input and cache reads use the discounted rate.
681
- "accounts/fireworks/routers/kimi-k2p5-turbo": {
682
- input: 0.99,
683
- output: 4.94,
684
- cacheWrite: 0.99,
685
- cacheRead: 0.16
686
- },
687
- // Fireworks prices the MiniMax M2 family at one rate. We apply that
688
- // official family pricing to MiniMax-M2.7 because Fireworks does not
689
- // publish a distinct M2.7 per-token row.
690
- "MiniMax-M2.7": {
691
- input: 0.3,
692
- output: 1.2,
693
- cacheWrite: 0.3,
694
- cacheRead: 0.03
695
- },
696
- // Opus 4.6 (same pricing as Opus 4.5)
697
- "claude-opus-4-6-20260101": {
698
- input: 5,
699
- output: 25,
700
- cacheWrite: 6.25,
701
- cacheRead: 0.5
702
- },
703
- // Opus 4.5 (cheaper than Opus 4.1)
704
- "claude-opus-4-5-20251101": {
705
- input: 5,
706
- output: 25,
707
- cacheWrite: 6.25,
708
- cacheRead: 0.5
709
- },
710
- // Sonnet 4.6 (same pricing as Sonnet 4.5)
711
- "claude-sonnet-4-6-20260101": {
712
- input: 3,
713
- output: 15,
714
- cacheWrite: 3.75,
715
- cacheRead: 0.3
716
- },
717
- // Sonnet 4.5
718
- "claude-sonnet-4-5-20250929": {
719
- input: 3,
720
- output: 15,
721
- cacheWrite: 3.75,
722
- cacheRead: 0.3
723
- },
724
- // Opus 4.1 (MORE expensive than Opus 4.5)
725
- "claude-opus-4-1-20250805": {
726
- input: 15,
727
- output: 75,
728
- cacheWrite: 18.75,
729
- cacheRead: 1.5
730
- },
731
- // Haiku 4.5
732
- "claude-haiku-4-5-20251001": {
733
- input: 1,
734
- output: 5,
735
- cacheWrite: 1.25,
736
- cacheRead: 0.1
737
- },
738
- // Sonnet 3.5 (legacy - same as Sonnet 4.5)
739
- "claude-3-5-sonnet-20241022": {
740
- input: 3,
741
- output: 15,
742
- cacheWrite: 3.75,
743
- cacheRead: 0.3
744
- },
745
- "claude-3-5-sonnet-20240620": {
746
- input: 3,
747
- output: 15,
748
- cacheWrite: 3.75,
749
- cacheRead: 0.3
750
- },
751
- // Haiku 3.5 (legacy - same as Haiku 4.5)
752
- "claude-3-5-haiku-20241022": {
753
- input: 1,
754
- output: 5,
755
- cacheWrite: 1.25,
756
- cacheRead: 0.1
757
- }
758
- };
759
- function tryGetModelPricing(modelName) {
760
- if (MODEL_PRICING[modelName]) {
761
- return MODEL_PRICING[modelName];
762
- }
763
- const normalized = modelName.toLowerCase();
764
- if (normalized.includes("kimi-k2p5-turbo")) {
765
- return MODEL_PRICING["accounts/fireworks/routers/kimi-k2p5-turbo"];
766
- }
767
- if (normalized.includes("minimax-m2.7")) {
768
- return MODEL_PRICING["MiniMax-M2.7"];
769
- }
770
- if (modelName.includes("opus-4-6") || modelName.includes("opus-4.6")) {
771
- return MODEL_PRICING["claude-opus-4-6-20260101"];
772
- }
773
- if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) {
774
- return MODEL_PRICING["claude-opus-4-5-20251101"];
775
- }
776
- if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1") || modelName.includes("opus-4")) {
777
- return MODEL_PRICING["claude-opus-4-1-20250805"];
778
- }
779
- if (modelName.includes("sonnet-4-6") || modelName.includes("sonnet-4.6")) {
780
- return MODEL_PRICING["claude-sonnet-4-6-20260101"];
781
- }
782
- if (modelName.includes("sonnet-4-5") || modelName.includes("sonnet-4.5")) {
783
- return MODEL_PRICING["claude-sonnet-4-5-20250929"];
784
- }
785
- if (modelName.includes("sonnet")) {
786
- return MODEL_PRICING["claude-3-5-sonnet-20241022"];
787
- }
788
- if (modelName.includes("haiku-4-5") || modelName.includes("haiku-4.5")) {
789
- return MODEL_PRICING["claude-haiku-4-5-20251001"];
790
- }
791
- if (modelName.includes("haiku")) {
792
- return MODEL_PRICING["claude-3-5-haiku-20241022"];
793
- }
794
- return null;
795
- }
796
- function getModelDisplayName(modelName) {
797
- const normalized = modelName.toLowerCase();
798
- if (normalized.includes("accounts/fireworks/routers/kimi-k2p5-turbo") || normalized.includes("custom:kimi-k2.5-turbo")) {
799
- return "Kimi K2.5 Turbo";
800
- }
801
- if (normalized.includes("minimax-m2.7") || normalized.includes("custom:minimax-m2.7")) {
802
- return "MiniMax-M2.7";
803
- }
804
- if (modelName.includes("opus-4-6") || modelName.includes("opus-4.6")) return "Opus 4.6";
805
- if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) return "Opus 4.5";
806
- if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1")) return "Opus 4.1";
807
- if (modelName.includes("opus")) return "Opus";
808
- if (modelName.includes("sonnet-4-6") || modelName.includes("sonnet-4.6")) return "Sonnet 4.6";
809
- if (modelName.includes("sonnet-4-5") || modelName.includes("sonnet-4.5")) return "Sonnet 4.5";
810
- if (modelName.includes("sonnet-3-5") || modelName.includes("sonnet-3.5")) return "Sonnet 3.5";
811
- if (modelName.includes("sonnet")) return "Sonnet";
812
- if (modelName.includes("haiku-4-5") || modelName.includes("haiku-4.5")) return "Haiku 4.5";
813
- if (modelName.includes("haiku-3-5") || modelName.includes("haiku-3.5")) return "Haiku 3.5";
814
- if (modelName.includes("haiku")) return "Haiku";
815
- return modelName;
816
- }
817
-
818
- // src/anthropic-sources.ts
819
863
  var execFileAsync = promisify(execFile);
820
864
  var SQLITE_SEPARATOR = "";
821
865
  var SQLITE_MAX_BUFFER = 64 * 1024 * 1024;
@@ -871,10 +915,16 @@ async function findFiles(dir, matcher, visited = /* @__PURE__ */ new Set(), resu
871
915
  function normalizeAnthropicModelName(modelName) {
872
916
  const trimmed = modelName.trim();
873
917
  const normalized = trimmed.toLowerCase();
874
- if (/^custom:minimax-m2\.7(?:-\d+)?$/.test(normalized)) {
918
+ if (normalized.includes("kimi-code")) {
919
+ return "accounts/fireworks/routers/kimi-k2p5-turbo";
920
+ }
921
+ if (normalized.includes("minimax-code")) {
922
+ return "MiniMax-M2.7";
923
+ }
924
+ if (/^custom:minimax-m2(?:\.|-)?7(?:-\d+)?$/.test(normalized)) {
875
925
  return "MiniMax-M2.7";
876
926
  }
877
- if (/^custom:kimi-k2\.5-turbo(?:-\d+)?$/.test(normalized)) {
927
+ if (/^custom:kimi-k2(?:\.|p)?5-turbo(?:-\d+)?$/.test(normalized) || /^accounts\/fireworks\/models\/kimi-k2p5-turbo$/.test(normalized) || /^accounts\/fireworks\/routers\/kimi-k2p5-turbo$/.test(normalized)) {
878
928
  return "accounts/fireworks/routers/kimi-k2p5-turbo";
879
929
  }
880
930
  return trimmed;
@@ -1152,10 +1202,14 @@ async function collectAnthropicUsageEntries(options = {}) {
1152
1202
  parseOpenCodeEntries(),
1153
1203
  parseFactoryEntries()
1154
1204
  ]);
1155
- return [...claudeEntries, ...opencodeEntries, ...factoryEntries];
1205
+ const entries = [...claudeEntries, ...opencodeEntries, ...factoryEntries];
1206
+ if (!options.families || options.families.length === 0) {
1207
+ return entries;
1208
+ }
1209
+ return entries.filter((entry) => matchesModelFamilies(entry.rawModel, options.families));
1156
1210
  }
1157
- async function loadClaudeStatsFromJsonl() {
1158
- const entries = await collectAnthropicUsageEntries();
1211
+ async function loadClaudeStatsFromJsonl(options = {}) {
1212
+ const entries = await collectAnthropicUsageEntries(options);
1159
1213
  if (entries.length === 0) {
1160
1214
  return null;
1161
1215
  }
@@ -1233,6 +1287,15 @@ async function loadClaudeStatsFromJsonl() {
1233
1287
 
1234
1288
  // src/usage/loader.ts
1235
1289
  var MAX_RECURSION_DEPTH = 10;
1290
+ function normalizePresentableModelName(modelName) {
1291
+ return getModelDisplayName(modelName).trim();
1292
+ }
1293
+ function normalizeEntryModels(entries) {
1294
+ return entries.map((entry) => ({
1295
+ ...entry,
1296
+ model: normalizePresentableModelName(entry.model)
1297
+ }));
1298
+ }
1236
1299
  function toLocalDateString2(isoTimestamp) {
1237
1300
  const date = new Date(isoTimestamp);
1238
1301
  const year = date.getFullYear();
@@ -1286,8 +1349,8 @@ async function findJsonlFiles(dir, visited = /* @__PURE__ */ new Set(), depth =
1286
1349
  }
1287
1350
  return result;
1288
1351
  }
1289
- async function parseClaudeJsonl(projectFilter) {
1290
- const entries = await collectAnthropicUsageEntries({ projectFilter });
1352
+ async function parseClaudeJsonl(projectFilter, families) {
1353
+ const entries = await collectAnthropicUsageEntries({ projectFilter, families });
1291
1354
  return entries.map((entry) => ({
1292
1355
  date: entry.date,
1293
1356
  model: entry.model,
@@ -1357,6 +1420,7 @@ async function parseCodexJsonl() {
1357
1420
  entries.push({
1358
1421
  date,
1359
1422
  model: getCodexModelDisplayName(currentModel),
1423
+ rawModel: currentModel,
1360
1424
  inputTokens: inputTokens - cachedInputTokens,
1361
1425
  outputTokens,
1362
1426
  cacheWriteTokens: 0,
@@ -1490,6 +1554,7 @@ function aggregateByModel(entries) {
1490
1554
  const modelMap = /* @__PURE__ */ new Map();
1491
1555
  for (const e of entries) {
1492
1556
  const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1557
+ if (entryTotal <= 0) continue;
1493
1558
  const existing = modelMap.get(e.model);
1494
1559
  if (existing) {
1495
1560
  existing.inputTokens += e.inputTokens;
@@ -1576,6 +1641,7 @@ function computeModelBreakdown(entries) {
1576
1641
  const modelMap = /* @__PURE__ */ new Map();
1577
1642
  for (const e of entries) {
1578
1643
  const total = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1644
+ if (total <= 0) continue;
1579
1645
  const existing = modelMap.get(e.model);
1580
1646
  if (existing) {
1581
1647
  existing.tokens += total;
@@ -1661,10 +1727,10 @@ function computeActivityStats(entries, source) {
1661
1727
  };
1662
1728
  }
1663
1729
  async function loadUsageStats(options) {
1664
- const { aggregation, since, until, codexOnly, combined, projectFilter } = options;
1730
+ const { aggregation, since, until, codexOnly, combined, projectFilter, families } = options;
1665
1731
  let entries = [];
1666
1732
  if (!codexOnly) {
1667
- entries = entries.concat(await parseClaudeJsonl(projectFilter));
1733
+ entries = entries.concat(await parseClaudeJsonl(projectFilter, families));
1668
1734
  }
1669
1735
  if (codexOnly || combined) {
1670
1736
  entries = entries.concat(await parseCodexJsonl());
@@ -1672,6 +1738,10 @@ async function loadUsageStats(options) {
1672
1738
  if (entries.length === 0) {
1673
1739
  return null;
1674
1740
  }
1741
+ entries = normalizeEntryModels(entries);
1742
+ if (families && families.length > 0) {
1743
+ entries = entries.filter((entry) => matchesModelFamilies(entry.rawModel || entry.model, families));
1744
+ }
1675
1745
  entries = filterByDateRange(entries, since, until);
1676
1746
  entries = entries.filter((e) => !e.model.toLowerCase().includes("synthetic"));
1677
1747
  if (entries.length === 0) {
@@ -1711,6 +1781,7 @@ async function loadUsageStats(options) {
1711
1781
  rows,
1712
1782
  totals,
1713
1783
  source,
1784
+ scopeLabel: formatModelFamilyLabel(families),
1714
1785
  aggregation,
1715
1786
  dateRange,
1716
1787
  modelBreakdown,
@@ -1718,10 +1789,10 @@ async function loadUsageStats(options) {
1718
1789
  };
1719
1790
  }
1720
1791
  async function loadActivityStats(options) {
1721
- const { since, until, codexOnly, combined, projectFilter } = options;
1792
+ const { since, until, codexOnly, combined, projectFilter, families } = options;
1722
1793
  let entries = [];
1723
1794
  if (!codexOnly) {
1724
- entries = entries.concat(await parseClaudeJsonl(projectFilter));
1795
+ entries = entries.concat(await parseClaudeJsonl(projectFilter, families));
1725
1796
  }
1726
1797
  if (codexOnly || combined) {
1727
1798
  entries = entries.concat(await parseCodexJsonl());
@@ -1729,6 +1800,10 @@ async function loadActivityStats(options) {
1729
1800
  if (entries.length === 0) {
1730
1801
  return null;
1731
1802
  }
1803
+ entries = normalizeEntryModels(entries);
1804
+ if (families && families.length > 0) {
1805
+ entries = entries.filter((entry) => matchesModelFamilies(entry.rawModel || entry.model, families));
1806
+ }
1732
1807
  entries = filterByDateRange(entries, since, until);
1733
1808
  entries = entries.filter((entry) => !entry.model.toLowerCase().includes("synthetic"));
1734
1809
  if (entries.length === 0) {
@@ -1737,7 +1812,9 @@ async function loadActivityStats(options) {
1737
1812
  let source = "claude";
1738
1813
  if (codexOnly) source = "codex";
1739
1814
  else if (combined) source = "combined";
1740
- return computeActivityStats(entries, source);
1815
+ const stats = computeActivityStats(entries, source);
1816
+ stats.scopeLabel = formatModelFamilyLabel(families);
1817
+ return stats;
1741
1818
  }
1742
1819
 
1743
1820
  // src/usage/table.ts
@@ -1785,9 +1862,11 @@ function displayUsageTable(stats, options = {}) {
1785
1862
  const termWidth = process.stdout.columns || 140;
1786
1863
  const useCompact = compact || termWidth < 120;
1787
1864
  console.log();
1788
- let title = "Claude Code Usage";
1789
- if (stats.source === "codex") title = "Codex CLI Usage";
1790
- else if (stats.source === "combined") title = "AI Coding Usage (Claude + Codex)";
1865
+ let title = stats.scopeLabel ? `${stats.scopeLabel} Usage` : "Claude Code Usage";
1866
+ if (!stats.scopeLabel) {
1867
+ if (stats.source === "codex") title = "Codex CLI Usage";
1868
+ else if (stats.source === "combined") title = "AI Coding Usage (Claude + Codex)";
1869
+ }
1791
1870
  const aggLabel = stats.aggregation === "daily" ? "Daily" : stats.aggregation === "monthly" ? "Monthly" : stats.aggregation === "model" ? "By Model" : stats.aggregation === "session" ? "By Session" : "Total";
1792
1871
  console.log(`${c.orange}${c.bold}${title} - ${aggLabel} Report${c.reset}`);
1793
1872
  console.log(`${c.gray}Date range: ${stats.dateRange.start} to ${stats.dateRange.end}${c.reset}`);
@@ -1868,9 +1947,11 @@ function displayTotalOnly(stats, options = {}) {
1868
1947
  const c = getColors(options.showColors);
1869
1948
  const { hideCost } = options;
1870
1949
  console.log();
1871
- let title = "Claude Code Usage";
1872
- if (stats.source === "codex") title = "Codex CLI Usage";
1873
- else if (stats.source === "combined") title = "AI Coding Usage (Claude + Codex)";
1950
+ let title = stats.scopeLabel ? `${stats.scopeLabel} Usage` : "Claude Code Usage";
1951
+ if (!stats.scopeLabel) {
1952
+ if (stats.source === "codex") title = "Codex CLI Usage";
1953
+ else if (stats.source === "combined") title = "AI Coding Usage (Claude + Codex)";
1954
+ }
1874
1955
  console.log(`${c.orange}${c.bold}${title} - Summary${c.reset}`);
1875
1956
  console.log(`${c.gray}Date range: ${stats.dateRange.start} to ${stats.dateRange.end}${c.reset}`);
1876
1957
  if (stats.sessionCounts) {
@@ -1959,7 +2040,7 @@ async function findJsonlFiles2(dir) {
1959
2040
  }
1960
2041
  return files;
1961
2042
  }
1962
- async function parseSessionFile(filePath) {
2043
+ async function parseSessionFile(filePath, families) {
1963
2044
  try {
1964
2045
  const content = await fs3.readFile(filePath, "utf-8");
1965
2046
  const lines = content.trim().split("\n");
@@ -2014,7 +2095,12 @@ async function parseSessionFile(filePath) {
2014
2095
  };
2015
2096
  let primaryModel = "gpt-5";
2016
2097
  let maxTokens = 0;
2098
+ let matchedModel = false;
2017
2099
  for (const [model, usage] of Object.entries(perModelUsage)) {
2100
+ if (!matchesModelFamilies(model, families)) {
2101
+ continue;
2102
+ }
2103
+ matchedModel = true;
2018
2104
  summedUsage.input_tokens += usage.input_tokens;
2019
2105
  summedUsage.cached_input_tokens += usage.cached_input_tokens;
2020
2106
  summedUsage.output_tokens += usage.output_tokens;
@@ -2025,6 +2111,9 @@ async function parseSessionFile(filePath) {
2025
2111
  primaryModel = model;
2026
2112
  }
2027
2113
  }
2114
+ if (families && families.length > 0 && !matchedModel) {
2115
+ return null;
2116
+ }
2028
2117
  return {
2029
2118
  id: sessionMeta.id,
2030
2119
  canonicalId: canonicalId || sessionMeta.id,
@@ -2039,7 +2128,7 @@ async function parseSessionFile(filePath) {
2039
2128
  return null;
2040
2129
  }
2041
2130
  }
2042
- async function loadCodexStats() {
2131
+ async function loadCodexStats(options = {}) {
2043
2132
  const codexDir = getCodexDir2();
2044
2133
  if (!await codexDataExists()) {
2045
2134
  return null;
@@ -2056,7 +2145,7 @@ async function loadCodexStats() {
2056
2145
  }
2057
2146
  const sessions = [];
2058
2147
  for (const file of jsonlFiles) {
2059
- const session = await parseSessionFile(file);
2148
+ const session = await parseSessionFile(file, options.families);
2060
2149
  if (session) {
2061
2150
  sessions.push(session);
2062
2151
  }
@@ -2141,17 +2230,17 @@ async function loadCodexStats() {
2141
2230
 
2142
2231
  // src/shared/data-loader.ts
2143
2232
  async function loadData(options) {
2144
- const { codexOnly, combined } = options;
2233
+ const { codexOnly, combined, families } = options;
2145
2234
  let claude = null;
2146
2235
  let codex = null;
2147
2236
  if (!codexOnly) {
2148
2237
  if (await claudeCompatibleDataExists()) {
2149
- claude = await loadClaudeStatsFromJsonl();
2238
+ claude = await loadClaudeStatsFromJsonl({ families });
2150
2239
  }
2151
2240
  }
2152
2241
  if (codexOnly || combined) {
2153
2242
  if (await codexDataExists()) {
2154
- codex = await loadCodexStats();
2243
+ codex = await loadCodexStats({ families });
2155
2244
  }
2156
2245
  }
2157
2246
  let source = "claude";
@@ -2161,14 +2250,15 @@ async function loadData(options) {
2161
2250
  }
2162
2251
  function validateData(data, options) {
2163
2252
  if (!data.claude && !data.codex) {
2253
+ const familyPrefix = options.scopeLabel ? `${options.scopeLabel} ` : "";
2164
2254
  if (options.codexOnly) {
2165
2255
  console.error("Error: OpenAI Codex data not found at ~/.codex");
2166
2256
  console.error("Make sure you have used the Codex CLI at least once.");
2167
2257
  } else if (options.combined) {
2168
- console.error("Error: No usage data found");
2258
+ console.error(`Error: No ${familyPrefix}usage data found`);
2169
2259
  console.error("Make sure you have used a supported local CLI source such as Claude Code, OpenCode, Droid, or Codex at least once.");
2170
2260
  } else {
2171
- console.error("Error: No Claude-compatible local data found");
2261
+ console.error(`Error: No ${familyPrefix}usage data found`);
2172
2262
  console.error("Checked ~/.claude, ~/.local/share/opencode/opencode.db, and ~/.factory/sessions.");
2173
2263
  }
2174
2264
  process.exit(1);
@@ -2299,6 +2389,7 @@ function getDisplayModelTokens(modelUsage) {
2299
2389
  if (shouldExcludeModel(modelName)) continue;
2300
2390
  const displayName = getModelDisplayName(modelName);
2301
2391
  const modelTokens = getUsageTokens(usage);
2392
+ if (modelTokens <= 0) continue;
2302
2393
  aggregated.set(displayName, (aggregated.get(displayName) || 0) + modelTokens);
2303
2394
  }
2304
2395
  return Array.from(aggregated.entries()).map(([model, tokens]) => ({ model, tokens })).sort((a, b) => b.tokens - a.tokens);
@@ -2320,6 +2411,7 @@ function getCodexDisplayModelTokens(modelUsage) {
2320
2411
  for (const [modelName, usage] of Object.entries(modelUsage)) {
2321
2412
  const displayName = getCodexModelDisplayName(modelName);
2322
2413
  const modelTokens = usage.inputTokens + usage.outputTokens;
2414
+ if (modelTokens <= 0) continue;
2323
2415
  aggregated.set(displayName, (aggregated.get(displayName) || 0) + modelTokens);
2324
2416
  }
2325
2417
  return Array.from(aggregated.entries()).map(([model, tokens]) => ({ model, tokens })).sort((a, b) => b.tokens - a.tokens);
@@ -2533,14 +2625,16 @@ function formatCount2(n) {
2533
2625
  function displayWrappedStats(stats, url, options) {
2534
2626
  const c = getColors2(options?.theme);
2535
2627
  console.log();
2536
- let title = "\u2728 Claude Code Wrapped 2025 \u2728";
2628
+ let title = stats.scopeLabel ? `\u2728 ${stats.scopeLabel} Wrapped 2025 \u2728` : "\u2728 Claude Code Wrapped 2025 \u2728";
2537
2629
  let sourceLabel = "";
2538
- if (stats.source === "codex") {
2539
- title = "\u2728 Codex CLI Wrapped 2025 \u2728";
2540
- sourceLabel = " (Codex)";
2541
- } else if (stats.source === "combined") {
2542
- title = "\u2728 AI Coding Wrapped 2025 \u2728";
2543
- sourceLabel = " (Claude + Codex)";
2630
+ if (!stats.scopeLabel) {
2631
+ if (stats.source === "codex") {
2632
+ title = "\u2728 Codex CLI Wrapped 2025 \u2728";
2633
+ sourceLabel = " (Codex)";
2634
+ } else if (stats.source === "combined") {
2635
+ title = "\u2728 AI Coding Wrapped 2025 \u2728";
2636
+ sourceLabel = " (Claude + Codex)";
2637
+ }
2544
2638
  }
2545
2639
  console.log(`${c.orange}${c.bold}\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557${c.reset}`);
2546
2640
  console.log(`${c.orange}${c.bold}\u2551${c.reset} ${c.orange}${c.bold}\u2551${c.reset}`);
@@ -2928,6 +3022,26 @@ function resolveOptions(cliArgs, config) {
2928
3022
  };
2929
3023
  }
2930
3024
 
3025
+ // src/shared/args.ts
3026
+ var modelFamilyArgs = {
3027
+ kimi: {
3028
+ type: "boolean",
3029
+ description: "Show only Kimi family stats across local usage sources",
3030
+ default: false
3031
+ },
3032
+ minimax: {
3033
+ type: "boolean",
3034
+ description: "Show only MiniMax family stats across local usage sources",
3035
+ default: false
3036
+ }
3037
+ };
3038
+ function getSelectedModelFamilies(args) {
3039
+ const families = [];
3040
+ if (args.kimi === true) families.push("kimi");
3041
+ if (args.minimax === true) families.push("minimax");
3042
+ return families;
3043
+ }
3044
+
2931
3045
  // src/index.ts
2932
3046
  function createSpinner(label = "Loading vibestats...") {
2933
3047
  const spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -3071,7 +3185,7 @@ async function publishArtifactWithFallback(artifact, baseUrl, fallbackUrl, prefe
3071
3185
  var main = defineCommand({
3072
3186
  meta: {
3073
3187
  name: "vibestats",
3074
- version: "1.3.8",
3188
+ version: "1.3.10",
3075
3189
  description: "AI coding stats - usage tracking and annual wrapped for Claude Code & Codex"
3076
3190
  },
3077
3191
  args: {
@@ -3098,6 +3212,7 @@ var main = defineCommand({
3098
3212
  description: "Show combined Claude Code + Codex stats",
3099
3213
  default: false
3100
3214
  },
3215
+ ...modelFamilyArgs,
3101
3216
  // Output format
3102
3217
  json: {
3103
3218
  type: "boolean",
@@ -3241,6 +3356,8 @@ var main = defineCommand({
3241
3356
  }
3242
3357
  });
3243
3358
  async function runUsage(args, config) {
3359
+ const families = getSelectedModelFamilies(args);
3360
+ const scopeLabel = formatModelFamilyLabel(families);
3244
3361
  const requestedSince = args.since;
3245
3362
  const requestedUntil = args.until;
3246
3363
  const requestedLastDays = parseLastDaysFlag(args);
@@ -3271,13 +3388,15 @@ async function runUsage(args, config) {
3271
3388
  until,
3272
3389
  codexOnly: args.codex,
3273
3390
  combined: args.combined,
3274
- projectFilter: args.project ? process.cwd() : void 0
3391
+ projectFilter: args.project ? process.cwd() : void 0,
3392
+ families,
3393
+ scopeLabel
3275
3394
  })
3276
3395
  );
3277
3396
  if (!stats) {
3278
3397
  if (requestedSince || requestedUntil || requestedLastDays) {
3279
3398
  const rangeStr = `${since || "(start)"} to ${until || "(end)"}`;
3280
- console.error(`Error: No usage data found for date range ${rangeStr}.`);
3399
+ console.error(`Error: No ${scopeLabel ? `${scopeLabel} ` : ""}usage data found for date range ${rangeStr}.`);
3281
3400
  if (args.codex) {
3282
3401
  console.error("Checked: ~/.codex/sessions and ~/.codex/archived_sessions");
3283
3402
  } else if (args.combined) {
@@ -3292,10 +3411,10 @@ async function runUsage(args, config) {
3292
3411
  console.error("Error: OpenAI Codex data not found at ~/.codex");
3293
3412
  console.error("Make sure you have used the Codex CLI at least once.");
3294
3413
  } else if (args.combined) {
3295
- console.error("Error: No usage data found");
3414
+ console.error(`Error: No ${scopeLabel ? `${scopeLabel} ` : ""}usage data found`);
3296
3415
  console.error("Make sure you have used a supported local CLI source such as Claude Code, OpenCode, Droid, or Codex at least once.");
3297
3416
  } else {
3298
- console.error("Error: No Claude-compatible local data found");
3417
+ console.error(`Error: No ${scopeLabel ? `${scopeLabel} ` : ""}usage data found`);
3299
3418
  console.error("Checked ~/.claude, ~/.local/share/opencode/opencode.db, and ~/.factory/sessions.");
3300
3419
  }
3301
3420
  process.exit(1);
@@ -3343,16 +3462,18 @@ async function runUsage(args, config) {
3343
3462
  }
3344
3463
  async function runWrapped(args, config) {
3345
3464
  const options = resolveOptions(args, config);
3465
+ const families = getSelectedModelFamilies(args);
3466
+ const scopeLabel = formatModelFamilyLabel(families);
3346
3467
  const metric = parseActivityMetric(args.metric);
3347
3468
  const days = parseActivityDays(args.days);
3348
3469
  const spinner = createSpinner("Preparing wrapped...");
3349
3470
  const [data, activityStats] = await spinner.whilePromise(
3350
3471
  Promise.all([
3351
- loadData({ codexOnly: args.codex, combined: args.combined }),
3352
- loadActivityStats({ codexOnly: args.codex, combined: args.combined })
3472
+ loadData({ codexOnly: args.codex, combined: args.combined, families, scopeLabel }),
3473
+ loadActivityStats({ codexOnly: args.codex, combined: args.combined, families, scopeLabel })
3353
3474
  ])
3354
3475
  );
3355
- validateData(data, { codexOnly: args.codex, combined: args.combined });
3476
+ validateData(data, { codexOnly: args.codex, combined: args.combined, families, scopeLabel });
3356
3477
  let claudeStats = null;
3357
3478
  let codexStats = null;
3358
3479
  if (data.claude) {
@@ -3369,6 +3490,9 @@ async function runWrapped(args, config) {
3369
3490
  } else {
3370
3491
  stats = claudeStats;
3371
3492
  }
3493
+ if (scopeLabel) {
3494
+ stats.scopeLabel = scopeLabel;
3495
+ }
3372
3496
  if (activityStats) {
3373
3497
  stats.activity = buildActivityGraph(activityStats, metric, days);
3374
3498
  }
@@ -3404,6 +3528,8 @@ async function runWrapped(args, config) {
3404
3528
  }
3405
3529
  }
3406
3530
  async function runActivity(args, config) {
3531
+ const families = getSelectedModelFamilies(args);
3532
+ const scopeLabel = formatModelFamilyLabel(families);
3407
3533
  const metric = parseActivityMetric(args.metric);
3408
3534
  const days = parseActivityDays(args.days);
3409
3535
  const spinner = createSpinner("Preparing activity graph...");
@@ -3413,11 +3539,13 @@ async function runActivity(args, config) {
3413
3539
  combined: args.combined,
3414
3540
  projectFilter: args.project ? process.cwd() : void 0,
3415
3541
  since: args.since,
3416
- until: args.until
3542
+ until: args.until,
3543
+ families,
3544
+ scopeLabel
3417
3545
  })
3418
3546
  );
3419
3547
  if (!stats) {
3420
- console.error("Error: No activity data found.");
3548
+ console.error(`Error: No ${scopeLabel ? `${scopeLabel} ` : ""}activity data found.`);
3421
3549
  process.exit(1);
3422
3550
  }
3423
3551
  const artifact = buildActivityArtifact(stats, metric, days);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibestats",
3
- "version": "1.3.9",
3
+ "version": "1.3.10",
4
4
  "description": "AI coding stats - usage tracking and annual wrapped for Claude Code & Codex",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",