vibestats 1.0.23 → 1.2.0

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 (2) hide show
  1. package/dist/index.js +131 -14
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,10 +6,17 @@ import { defineCommand, runMain } from "citty";
6
6
  // src/usage/loader.ts
7
7
  import { promises as fs } from "fs";
8
8
  import { homedir } from "os";
9
- import { join } from "path";
9
+ import { join, basename } from "path";
10
10
 
11
11
  // src/pricing.ts
12
12
  var MODEL_PRICING = {
13
+ // Opus 4.6 (same pricing as Opus 4.5)
14
+ "claude-opus-4-6-20260101": {
15
+ input: 5,
16
+ output: 25,
17
+ cacheWrite: 6.25,
18
+ cacheRead: 0.5
19
+ },
13
20
  // Opus 4.5 (cheaper than Opus 4.1)
14
21
  "claude-opus-4-5-20251101": {
15
22
  input: 5,
@@ -63,6 +70,9 @@ function getModelPricing(modelName) {
63
70
  if (MODEL_PRICING[modelName]) {
64
71
  return MODEL_PRICING[modelName];
65
72
  }
73
+ if (modelName.includes("opus-4-6") || modelName.includes("opus-4.6")) {
74
+ return MODEL_PRICING["claude-opus-4-6-20260101"];
75
+ }
66
76
  if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) {
67
77
  return MODEL_PRICING["claude-opus-4-5-20251101"];
68
78
  }
@@ -84,6 +94,7 @@ function getModelPricing(modelName) {
84
94
  return MODEL_PRICING["claude-sonnet-4-5-20250929"];
85
95
  }
86
96
  function getModelDisplayName(modelName) {
97
+ if (modelName.includes("opus-4-6") || modelName.includes("opus-4.6")) return "Opus 4.6";
87
98
  if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) return "Opus 4.5";
88
99
  if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1")) return "Opus 4.1";
89
100
  if (modelName.includes("opus")) return "Opus";
@@ -98,7 +109,18 @@ function getModelDisplayName(modelName) {
98
109
 
99
110
  // src/codex-pricing.ts
100
111
  var CODEX_MODEL_PRICING = {
101
- // GPT-5.2 (latest flagship)
112
+ // GPT-5.3 (latest flagship, assumed same pricing as GPT-5.2)
113
+ "gpt-5.3": {
114
+ input: 1.75,
115
+ output: 14,
116
+ cachedInput: 0.175
117
+ },
118
+ "gpt-5.3-codex": {
119
+ input: 1.75,
120
+ output: 14,
121
+ cachedInput: 0.175
122
+ },
123
+ // GPT-5.2
102
124
  // Cached input is ~10% of input price (prompt caching discount)
103
125
  "gpt-5.2": {
104
126
  input: 1.75,
@@ -177,6 +199,9 @@ function getCodexModelPricing(modelName) {
177
199
  return CODEX_MODEL_PRICING[modelName];
178
200
  }
179
201
  const normalized = modelName.toLowerCase();
202
+ if (normalized.includes("5.3")) {
203
+ return CODEX_MODEL_PRICING["gpt-5.3"];
204
+ }
180
205
  if (normalized.includes("5.2")) {
181
206
  return CODEX_MODEL_PRICING["gpt-5.2"];
182
207
  }
@@ -208,6 +233,8 @@ function getCodexModelPricing(modelName) {
208
233
  }
209
234
  function getCodexModelDisplayName(modelName) {
210
235
  const normalized = modelName.toLowerCase();
236
+ if (normalized.includes("5.3") && normalized.includes("pro")) return "GPT-5.3 Pro";
237
+ if (normalized.includes("5.3")) return "GPT-5.3";
211
238
  if (normalized.includes("5.2") && normalized.includes("pro")) return "GPT-5.2 Pro";
212
239
  if (normalized.includes("5.2")) return "GPT-5.2";
213
240
  if (normalized.includes("5.1") && normalized.includes("max")) return "GPT-5.1 Max";
@@ -289,16 +316,38 @@ async function findJsonlFiles(dir, visited = /* @__PURE__ */ new Set(), depth =
289
316
  }
290
317
  return result;
291
318
  }
292
- async function parseClaudeJsonl() {
319
+ async function resolveProjectDir(projectsDir, cwd) {
320
+ let current = cwd;
321
+ while (current && current !== "/") {
322
+ const encoded = current.replace(/\//g, "-");
323
+ const candidate = join(projectsDir, encoded);
324
+ if (await pathExists(candidate)) return candidate;
325
+ const parent = join(current, "..");
326
+ if (parent === current) break;
327
+ current = parent;
328
+ }
329
+ return null;
330
+ }
331
+ async function parseClaudeJsonl(projectFilter) {
293
332
  const entries = [];
294
333
  const seenMessageIds = /* @__PURE__ */ new Set();
295
334
  const claudeDir = getClaudeDir();
296
335
  const projectsDir = join(claudeDir, "projects");
297
336
  if (!await pathExists(projectsDir)) return entries;
298
- const jsonlFiles = await findJsonlFiles(projectsDir);
337
+ let searchDir;
338
+ if (projectFilter) {
339
+ const resolved = await resolveProjectDir(projectsDir, projectFilter);
340
+ if (!resolved) return entries;
341
+ searchDir = resolved;
342
+ } else {
343
+ searchDir = projectsDir;
344
+ }
345
+ const jsonlFiles = await findJsonlFiles(searchDir);
299
346
  for (const filePath of jsonlFiles) {
347
+ const sessionId = basename(filePath, ".jsonl");
300
348
  try {
301
349
  const content = await fs.readFile(filePath, "utf-8");
350
+ if (!content.trim()) continue;
302
351
  const lines = content.split("\n");
303
352
  for (const line of lines) {
304
353
  if (!line.trim()) continue;
@@ -329,7 +378,9 @@ async function parseClaudeJsonl() {
329
378
  cacheWriteTokens,
330
379
  cacheReadTokens,
331
380
  cost,
332
- source: "claude"
381
+ source: "claude",
382
+ sessionId,
383
+ timestamp
333
384
  });
334
385
  } catch {
335
386
  }
@@ -403,6 +454,7 @@ function filterByDateRange(entries, since, until) {
403
454
  }
404
455
  function sortModelsByTier(models) {
405
456
  const tierPriority = {
457
+ "Opus 4.6": 101,
406
458
  "Opus 4.5": 100,
407
459
  "Opus 4.1": 99,
408
460
  "Opus": 98,
@@ -412,6 +464,7 @@ function sortModelsByTier(models) {
412
464
  "Haiku 4.5": 80,
413
465
  "Haiku 3.5": 79,
414
466
  "Haiku": 78,
467
+ "GPT-5.3": 71,
415
468
  "GPT-5.2": 70,
416
469
  "GPT-5.1 Max": 69,
417
470
  "GPT-5.1": 68,
@@ -513,6 +566,50 @@ function aggregateByModel(entries) {
513
566
  }
514
567
  return Array.from(modelMap.values()).sort((a, b) => b.totalTokens - a.totalTokens);
515
568
  }
569
+ function aggregateBySession(entries) {
570
+ const sessionMap = /* @__PURE__ */ new Map();
571
+ for (const e of entries) {
572
+ const sid = e.sessionId || "unknown";
573
+ const existing = sessionMap.get(sid);
574
+ if (existing) {
575
+ existing.inputTokens += e.inputTokens;
576
+ existing.outputTokens += e.outputTokens;
577
+ existing.cacheWriteTokens += e.cacheWriteTokens;
578
+ existing.cacheReadTokens += e.cacheReadTokens;
579
+ existing.totalTokens += e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
580
+ existing.cost += e.cost;
581
+ existing.modelsSet.add(e.model);
582
+ if (e.timestamp && e.timestamp < existing.firstTimestamp) {
583
+ existing.firstTimestamp = e.timestamp;
584
+ }
585
+ } else {
586
+ sessionMap.set(sid, {
587
+ modelsSet: /* @__PURE__ */ new Set([e.model]),
588
+ inputTokens: e.inputTokens,
589
+ outputTokens: e.outputTokens,
590
+ cacheWriteTokens: e.cacheWriteTokens,
591
+ cacheReadTokens: e.cacheReadTokens,
592
+ totalTokens: e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens,
593
+ cost: e.cost,
594
+ firstTimestamp: e.timestamp || ""
595
+ });
596
+ }
597
+ }
598
+ return Array.from(sessionMap.entries()).filter(([, data]) => data.totalTokens > 0).map(([sessionId, data]) => {
599
+ const date = data.firstTimestamp ? toLocalDateString(data.firstTimestamp) : "unknown";
600
+ const shortId = sessionId.slice(0, 12);
601
+ return {
602
+ key: `${date} ${shortId}`,
603
+ inputTokens: data.inputTokens,
604
+ outputTokens: data.outputTokens,
605
+ cacheWriteTokens: data.cacheWriteTokens,
606
+ cacheReadTokens: data.cacheReadTokens,
607
+ totalTokens: data.totalTokens,
608
+ cost: data.cost,
609
+ models: sortModelsByTier(Array.from(data.modelsSet))
610
+ };
611
+ }).sort((a, b) => a.key.localeCompare(b.key));
612
+ }
516
613
  function computeTotals(rows) {
517
614
  return rows.reduce(
518
615
  (acc, row) => ({
@@ -547,10 +644,10 @@ function computeModelBreakdown(entries) {
547
644
  })).sort((a, b) => b.tokens - a.tokens);
548
645
  }
549
646
  async function loadUsageStats(options) {
550
- const { aggregation, since, until, codexOnly, combined } = options;
647
+ const { aggregation, since, until, codexOnly, combined, projectFilter } = options;
551
648
  let entries = [];
552
649
  if (!codexOnly) {
553
- entries = entries.concat(await parseClaudeJsonl());
650
+ entries = entries.concat(await parseClaudeJsonl(projectFilter));
554
651
  }
555
652
  if (codexOnly || combined) {
556
653
  entries = entries.concat(await parseCodexJsonl());
@@ -576,6 +673,9 @@ async function loadUsageStats(options) {
576
673
  case "model":
577
674
  rows = aggregateByModel(entries);
578
675
  break;
676
+ case "session":
677
+ rows = aggregateBySession(entries);
678
+ break;
579
679
  case "total":
580
680
  rows = [];
581
681
  break;
@@ -644,13 +744,16 @@ function displayUsageTable(stats, options = {}) {
644
744
  let title = "Claude Code Usage";
645
745
  if (stats.source === "codex") title = "Codex CLI Usage";
646
746
  else if (stats.source === "combined") title = "AI Coding Usage (Claude + Codex)";
647
- const aggLabel = stats.aggregation === "daily" ? "Daily" : stats.aggregation === "monthly" ? "Monthly" : stats.aggregation === "model" ? "By Model" : "Total";
747
+ const aggLabel = stats.aggregation === "daily" ? "Daily" : stats.aggregation === "monthly" ? "Monthly" : stats.aggregation === "model" ? "By Model" : stats.aggregation === "session" ? "By Session" : "Total";
648
748
  console.log(`${c.orange}${c.bold}${title} - ${aggLabel} Report${c.reset}`);
649
749
  console.log(`${c.gray}Date range: ${stats.dateRange.start} to ${stats.dateRange.end}${c.reset}`);
650
750
  console.log(`${c.gray}${"\u2500".repeat(Math.min(termWidth - 2, 120))}${c.reset}`);
651
751
  console.log();
652
- const headers = useCompact ? ["Date", "Input", "Output", "Total", ...hideCost ? [] : ["Cost"]] : ["Date", "", "Input", "Output", "Cache W", "Cache R", "Total", ...hideCost ? [] : ["Cost"]];
653
- const widths = useCompact ? [12, 14, 12, 16, ...hideCost ? [] : [12]] : [12, 16, 14, 12, 14, 14, 16, ...hideCost ? [] : [12]];
752
+ const isSession = stats.aggregation === "session";
753
+ const firstCol = isSession ? "Session" : "Date";
754
+ const firstWidth = isSession ? 24 : 12;
755
+ const headers = useCompact ? [firstCol, "Input", "Output", "Total", ...hideCost ? [] : ["Cost"]] : [firstCol, "", "Input", "Output", "Cache W", "Cache R", "Total", ...hideCost ? [] : ["Cost"]];
756
+ const widths = useCompact ? [firstWidth, 14, 12, 16, ...hideCost ? [] : [12]] : [firstWidth, 16, 14, 12, 14, 14, 16, ...hideCost ? [] : [12]];
654
757
  const headerRow = headers.map((h, i) => padLeft(h, widths[i])).join(" ");
655
758
  console.log(`${c.bold}${headerRow}${c.reset}`);
656
759
  for (const row of stats.rows) {
@@ -659,7 +762,7 @@ function displayUsageTable(stats, options = {}) {
659
762
  const remainingModels = models.slice(1);
660
763
  if (useCompact) {
661
764
  const cols = [
662
- padRight(row.key.slice(0, 12), widths[0]),
765
+ padRight(row.key.slice(0, firstWidth), widths[0]),
663
766
  padLeft(formatNumber(row.inputTokens), widths[1]),
664
767
  padLeft(formatNumber(row.outputTokens), widths[2]),
665
768
  padLeft(formatNumber(row.totalTokens), widths[3]),
@@ -669,7 +772,7 @@ function displayUsageTable(stats, options = {}) {
669
772
  } else {
670
773
  const modelDisplay = firstModel ? `- ${firstModel}` : "";
671
774
  const cols = [
672
- padRight(row.key.slice(0, 12), widths[0]),
775
+ padRight(row.key.slice(0, firstWidth), widths[0]),
673
776
  padRight(modelDisplay.slice(0, 16), widths[1]),
674
777
  padLeft(formatNumber(row.inputTokens), widths[2]),
675
778
  padLeft(formatNumber(row.outputTokens), widths[3]),
@@ -1957,6 +2060,18 @@ var main = defineCommand({
1957
2060
  description: "Generate a shareable URL (usage mode)",
1958
2061
  default: false
1959
2062
  },
2063
+ // Project and session filtering
2064
+ project: {
2065
+ type: "boolean",
2066
+ alias: "p",
2067
+ description: "Show stats for the current project only",
2068
+ default: false
2069
+ },
2070
+ sessions: {
2071
+ type: "boolean",
2072
+ description: "Show per-session breakdown",
2073
+ default: false
2074
+ },
1960
2075
  // Wrapped-specific options
1961
2076
  url: {
1962
2077
  type: "string",
@@ -1999,7 +2114,8 @@ var main = defineCommand({
1999
2114
  });
2000
2115
  async function runUsage(args, config) {
2001
2116
  let aggregation = "daily";
2002
- if (args.monthly) aggregation = "monthly";
2117
+ if (args.sessions) aggregation = "session";
2118
+ else if (args.monthly) aggregation = "monthly";
2003
2119
  else if (args.model) aggregation = "model";
2004
2120
  else if (args.total) aggregation = "total";
2005
2121
  const spinner = createSpinner("Loading vibestats...");
@@ -2009,7 +2125,8 @@ async function runUsage(args, config) {
2009
2125
  since: args.since,
2010
2126
  until: args.until,
2011
2127
  codexOnly: args.codex,
2012
- combined: args.combined
2128
+ combined: args.combined,
2129
+ projectFilter: args.project ? process.cwd() : void 0
2013
2130
  })
2014
2131
  );
2015
2132
  if (!stats) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibestats",
3
- "version": "1.0.23",
3
+ "version": "1.2.0",
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",