vibestats 1.3.4 → 1.3.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.
Files changed (2) hide show
  1. package/dist/index.js +819 -320
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,121 +3,6 @@
3
3
  // src/index.ts
4
4
  import { defineCommand, runMain } from "citty";
5
5
 
6
- // src/usage/loader.ts
7
- import { promises as fs } from "fs";
8
- import { homedir } from "os";
9
- import { join, basename } from "path";
10
-
11
- // src/pricing.ts
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
- },
20
- // Opus 4.5 (cheaper than Opus 4.1)
21
- "claude-opus-4-5-20251101": {
22
- input: 5,
23
- output: 25,
24
- cacheWrite: 6.25,
25
- cacheRead: 0.5
26
- },
27
- // Sonnet 4.6 (same pricing as Sonnet 4.5)
28
- "claude-sonnet-4-6-20260101": {
29
- input: 3,
30
- output: 15,
31
- cacheWrite: 3.75,
32
- cacheRead: 0.3
33
- },
34
- // Sonnet 4.5
35
- "claude-sonnet-4-5-20250929": {
36
- input: 3,
37
- output: 15,
38
- cacheWrite: 3.75,
39
- cacheRead: 0.3
40
- },
41
- // Opus 4.1 (MORE expensive than Opus 4.5)
42
- "claude-opus-4-1-20250805": {
43
- input: 15,
44
- output: 75,
45
- cacheWrite: 18.75,
46
- cacheRead: 1.5
47
- },
48
- // Haiku 4.5
49
- "claude-haiku-4-5-20251001": {
50
- input: 1,
51
- output: 5,
52
- cacheWrite: 1.25,
53
- cacheRead: 0.1
54
- },
55
- // Sonnet 3.5 (legacy - same as Sonnet 4.5)
56
- "claude-3-5-sonnet-20241022": {
57
- input: 3,
58
- output: 15,
59
- cacheWrite: 3.75,
60
- cacheRead: 0.3
61
- },
62
- "claude-3-5-sonnet-20240620": {
63
- input: 3,
64
- output: 15,
65
- cacheWrite: 3.75,
66
- cacheRead: 0.3
67
- },
68
- // Haiku 3.5 (legacy - same as Haiku 4.5)
69
- "claude-3-5-haiku-20241022": {
70
- input: 1,
71
- output: 5,
72
- cacheWrite: 1.25,
73
- cacheRead: 0.1
74
- }
75
- };
76
- function getModelPricing(modelName) {
77
- if (MODEL_PRICING[modelName]) {
78
- return MODEL_PRICING[modelName];
79
- }
80
- if (modelName.includes("opus-4-6") || modelName.includes("opus-4.6")) {
81
- return MODEL_PRICING["claude-opus-4-6-20260101"];
82
- }
83
- if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) {
84
- return MODEL_PRICING["claude-opus-4-5-20251101"];
85
- }
86
- if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1") || modelName.includes("opus-4")) {
87
- return MODEL_PRICING["claude-opus-4-1-20250805"];
88
- }
89
- if (modelName.includes("sonnet-4-6") || modelName.includes("sonnet-4.6")) {
90
- return MODEL_PRICING["claude-sonnet-4-6-20260101"];
91
- }
92
- if (modelName.includes("sonnet-4-5") || modelName.includes("sonnet-4.5")) {
93
- return MODEL_PRICING["claude-sonnet-4-5-20250929"];
94
- }
95
- if (modelName.includes("sonnet")) {
96
- return MODEL_PRICING["claude-3-5-sonnet-20241022"];
97
- }
98
- if (modelName.includes("haiku-4-5") || modelName.includes("haiku-4.5")) {
99
- return MODEL_PRICING["claude-haiku-4-5-20251001"];
100
- }
101
- if (modelName.includes("haiku")) {
102
- return MODEL_PRICING["claude-3-5-haiku-20241022"];
103
- }
104
- return MODEL_PRICING["claude-sonnet-4-5-20250929"];
105
- }
106
- function getModelDisplayName(modelName) {
107
- if (modelName.includes("opus-4-6") || modelName.includes("opus-4.6")) return "Opus 4.6";
108
- if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) return "Opus 4.5";
109
- if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1")) return "Opus 4.1";
110
- if (modelName.includes("opus")) return "Opus";
111
- if (modelName.includes("sonnet-4-6") || modelName.includes("sonnet-4.6")) return "Sonnet 4.6";
112
- if (modelName.includes("sonnet-4-5") || modelName.includes("sonnet-4.5")) return "Sonnet 4.5";
113
- if (modelName.includes("sonnet-3-5") || modelName.includes("sonnet-3.5")) return "Sonnet 3.5";
114
- if (modelName.includes("sonnet")) return "Sonnet";
115
- if (modelName.includes("haiku-4-5") || modelName.includes("haiku-4.5")) return "Haiku 4.5";
116
- if (modelName.includes("haiku-3-5") || modelName.includes("haiku-3.5")) return "Haiku 3.5";
117
- if (modelName.includes("haiku")) return "Haiku";
118
- return modelName;
119
- }
120
-
121
6
  // src/codex-pricing.ts
122
7
  var GPT_54_PRICING = { input: 2.5, output: 15, cachedInput: 0.25 };
123
8
  var GPT_53_PRICING = { input: 1.75, output: 14, cachedInput: 0.175 };
@@ -395,6 +280,501 @@ function calculateCodexCost(modelName, inputTokens, outputTokens, cachedInputTok
395
280
  return inputCost + cachedCost + outputCost;
396
281
  }
397
282
 
283
+ // src/url-encoder.ts
284
+ function formatCompactNumber(num) {
285
+ if (num >= 1e9) {
286
+ return `${(num / 1e9).toFixed(1)}B`;
287
+ }
288
+ if (num >= 1e6) {
289
+ return `${(num / 1e6).toFixed(1)}M`;
290
+ }
291
+ if (num >= 1e3) {
292
+ return `${(num / 1e3).toFixed(1)}K`;
293
+ }
294
+ return num.toString();
295
+ }
296
+ var dayToNumber = {
297
+ Sunday: 0,
298
+ Monday: 1,
299
+ Tuesday: 2,
300
+ Wednesday: 3,
301
+ Thursday: 4,
302
+ Friday: 5,
303
+ Saturday: 6
304
+ };
305
+ function encodeStatsToUrl(stats, baseUrl = "https://vibestats.wolfai.dev") {
306
+ const params = new URLSearchParams();
307
+ params.set("s", stats.sessions.toString());
308
+ params.set("t", formatCompactNumber(stats.totalTokens));
309
+ params.set("c", stats.totalCost.toFixed(2));
310
+ params.set("d", stats.daysActive.toString());
311
+ params.set("ls", stats.longestStreak.toString());
312
+ params.set("cs", stats.currentStreak.toString());
313
+ params.set("ph", stats.peakHour.toString());
314
+ params.set("pd", (dayToNumber[stats.peakDay] ?? 0).toString());
315
+ params.set("fm", getModelTokenFromDisplayName(stats.favoriteModel));
316
+ const mbParts = stats.modelBreakdown.slice(0, 3).map((m) => {
317
+ const abbr = getModelTokenFromDisplayName(m.model);
318
+ return `${abbr}:${m.percentage}`;
319
+ });
320
+ params.set("mb", mbParts.join(","));
321
+ if (stats.topTools && stats.topTools.length > 0) {
322
+ params.set("tt", stats.topTools.slice(0, 5).join(","));
323
+ }
324
+ if (stats.developerStyle) {
325
+ const styleMap = {
326
+ reader: "r",
327
+ writer: "w",
328
+ executor: "e",
329
+ balanced: "b"
330
+ };
331
+ params.set("st", styleMap[stats.developerStyle] || "b");
332
+ }
333
+ if (stats.topProject) {
334
+ params.set("tp", stats.topProject);
335
+ }
336
+ if (stats.projectCount) {
337
+ params.set("pc", stats.projectCount.toString());
338
+ }
339
+ params.set("wg", formatCompactNumber(stats.wordsGenerated));
340
+ const firstDate = new Date(stats.firstSessionDate);
341
+ params.set("fad", firstDate.toISOString().split("T")[0].replace(/-/g, ""));
342
+ if (stats.source && stats.source !== "claude") {
343
+ params.set("src", stats.source);
344
+ }
345
+ if (stats.activity) {
346
+ params.set("act", encodePayload(stats.activity));
347
+ }
348
+ return `${baseUrl}/wrapped/?${params.toString()}`;
349
+ }
350
+ function encodeActivityToUrl(payload, baseUrl = "https://vibestats.wolfai.dev") {
351
+ const params = new URLSearchParams();
352
+ params.set("activity", encodePayload(payload));
353
+ if (payload.source !== "claude") {
354
+ params.set("src", payload.source);
355
+ }
356
+ return `${baseUrl}/activity?${params.toString()}`;
357
+ }
358
+ var CLAUDE_DISPLAY_TO_TOKEN = {
359
+ "Opus 4.6": "o46",
360
+ "Opus 4.5": "o45",
361
+ "Opus 4.1": "o41",
362
+ Opus: "opus",
363
+ "Sonnet 4.6": "s46",
364
+ "Sonnet 4.5": "s45",
365
+ "Sonnet 3.5": "s35",
366
+ Sonnet: "sonnet",
367
+ "Haiku 4.5": "h45",
368
+ "Haiku 3.5": "h35",
369
+ Haiku: "haiku"
370
+ };
371
+ function getModelTokenFromDisplayName(displayName) {
372
+ if (CLAUDE_DISPLAY_TO_TOKEN[displayName]) {
373
+ return CLAUDE_DISPLAY_TO_TOKEN[displayName];
374
+ }
375
+ const normalized = displayName.toLowerCase();
376
+ if (normalized.includes("opus") && normalized.includes("4.6")) return "o46";
377
+ if (normalized.includes("opus") && normalized.includes("4.5")) return "o45";
378
+ if (normalized.includes("opus") && normalized.includes("4.1")) return "o41";
379
+ if (normalized.includes("opus")) return "opus";
380
+ if (normalized.includes("sonnet") && normalized.includes("4.6")) return "s46";
381
+ if (normalized.includes("sonnet") && normalized.includes("4.5")) return "s45";
382
+ if (normalized.includes("sonnet") && normalized.includes("3.5")) return "s35";
383
+ if (normalized.includes("sonnet")) return "sonnet";
384
+ if (normalized.includes("haiku") && normalized.includes("4.5")) return "h45";
385
+ if (normalized.includes("haiku") && normalized.includes("3.5")) return "h35";
386
+ if (normalized.includes("haiku")) return "haiku";
387
+ return getCodexModelAbbreviation(displayName);
388
+ }
389
+ function aggregateRowsToMonthly(rows) {
390
+ const monthMap = /* @__PURE__ */ new Map();
391
+ for (const row of rows) {
392
+ const month = row.key.slice(0, 7);
393
+ const existing = monthMap.get(month);
394
+ if (existing) {
395
+ existing.inputTokens += row.inputTokens;
396
+ existing.outputTokens += row.outputTokens;
397
+ existing.cacheWriteTokens += row.cacheWriteTokens;
398
+ existing.cacheReadTokens += row.cacheReadTokens;
399
+ existing.totalTokens += row.totalTokens;
400
+ existing.cost += row.cost;
401
+ } else {
402
+ monthMap.set(month, { ...row, key: month });
403
+ }
404
+ }
405
+ return Array.from(monthMap.values()).sort((a, b) => a.key.localeCompare(b.key));
406
+ }
407
+ function encodeUsageToUrl(stats, baseUrl = "https://vibestats.wolfai.dev") {
408
+ const params = new URLSearchParams();
409
+ if (stats.source !== "claude") {
410
+ params.set("src", stats.source);
411
+ }
412
+ const formatDateCompact = (d) => d.replace(/-/g, "");
413
+ const startMs = new Date(stats.dateRange.start).getTime();
414
+ const endMs = new Date(stats.dateRange.end).getTime();
415
+ const daySpan = Math.ceil((endMs - startMs) / (1e3 * 60 * 60 * 24));
416
+ const useMonthly = stats.aggregation === "daily" && daySpan > 31;
417
+ const aggMap = { daily: "d", monthly: "m", model: "mo", total: "t" };
418
+ const effectiveAgg = useMonthly ? "monthly" : stats.aggregation;
419
+ params.set("agg", aggMap[effectiveAgg] || "d");
420
+ let rowsToEncode;
421
+ if (useMonthly) {
422
+ rowsToEncode = aggregateRowsToMonthly(stats.rows);
423
+ } else {
424
+ rowsToEncode = stats.rows.slice(-31);
425
+ }
426
+ const startDate = useMonthly ? stats.dateRange.start : rowsToEncode[0]?.key || stats.dateRange.start;
427
+ const endDate = useMonthly ? stats.dateRange.end : rowsToEncode[rowsToEncode.length - 1]?.key || stats.dateRange.end;
428
+ params.set("dr", `${formatDateCompact(startDate)}-${formatDateCompact(endDate)}`);
429
+ const rows = rowsToEncode.map((row) => {
430
+ let key = row.key;
431
+ if (effectiveAgg === "daily" && row.key.length === 10) {
432
+ key = row.key.slice(5).replace("-", "");
433
+ }
434
+ return [
435
+ key,
436
+ formatCompactNumber(row.inputTokens),
437
+ formatCompactNumber(row.outputTokens),
438
+ formatCompactNumber(row.cacheWriteTokens),
439
+ formatCompactNumber(row.cacheReadTokens),
440
+ formatCompactNumber(row.totalTokens),
441
+ row.cost.toFixed(2)
442
+ ].join(":");
443
+ });
444
+ params.set("rows", rows.join("|"));
445
+ const t = stats.totals;
446
+ params.set("tot", [
447
+ formatCompactNumber(t.inputTokens),
448
+ formatCompactNumber(t.outputTokens),
449
+ formatCompactNumber(t.cacheWriteTokens),
450
+ formatCompactNumber(t.cacheReadTokens),
451
+ formatCompactNumber(t.totalTokens),
452
+ t.cost.toFixed(2)
453
+ ].join(":"));
454
+ if (stats.modelBreakdown.length > 0) {
455
+ const mb = stats.modelBreakdown.slice(0, 5).map((m) => {
456
+ const abbr = getModelTokenFromDisplayName(m.model);
457
+ return `${abbr}:${m.percentage}:${m.cost.toFixed(2)}`;
458
+ });
459
+ params.set("mb", mb.join(","));
460
+ }
461
+ return `${baseUrl}?${params.toString()}`;
462
+ }
463
+ function encodePayload(payload) {
464
+ return Buffer.from(JSON.stringify(payload), "utf-8").toString("base64url");
465
+ }
466
+
467
+ // src/activity.ts
468
+ var WEEKDAY_LABELS = ["Mon", "", "", "", "", "", "Sun"];
469
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
470
+ function toDateKey(date) {
471
+ const year = date.getFullYear();
472
+ const month = String(date.getMonth() + 1).padStart(2, "0");
473
+ const day = String(date.getDate()).padStart(2, "0");
474
+ return `${year}-${month}-${day}`;
475
+ }
476
+ function startOfDay(date) {
477
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0, 0);
478
+ }
479
+ function addDays(date, days) {
480
+ return new Date(date.getTime() + days * MS_PER_DAY);
481
+ }
482
+ function startOfWeekMonday(date) {
483
+ const current = startOfDay(date);
484
+ const day = current.getDay();
485
+ const delta = day === 0 ? -6 : 1 - day;
486
+ return addDays(current, delta);
487
+ }
488
+ function endOfWeekSunday(date) {
489
+ const monday = startOfWeekMonday(date);
490
+ return addDays(monday, 6);
491
+ }
492
+ function quantile(sortedValues, ratio) {
493
+ if (sortedValues.length === 0) return 0;
494
+ const index = Math.min(
495
+ sortedValues.length - 1,
496
+ Math.max(0, Math.floor((sortedValues.length - 1) * ratio))
497
+ );
498
+ return sortedValues[index] ?? 0;
499
+ }
500
+ function getMetricValue(day, metric) {
501
+ switch (metric) {
502
+ case "messages":
503
+ return day.messageCount;
504
+ case "sessions":
505
+ return day.sessionCount;
506
+ case "tokens":
507
+ default:
508
+ return day.totalTokens;
509
+ }
510
+ }
511
+ function buildLegend(thresholds) {
512
+ return [
513
+ { intensity: 0, min: 0, max: 0, label: "No activity" },
514
+ { intensity: 1, min: 1, max: thresholds[0], label: "Low" },
515
+ { intensity: 2, min: thresholds[0] + 1, max: thresholds[1], label: "Steady" },
516
+ { intensity: 3, min: thresholds[1] + 1, max: thresholds[2], label: "Strong" },
517
+ { intensity: 4, min: thresholds[2] + 1, max: Number.MAX_SAFE_INTEGER, label: "Peak" }
518
+ ];
519
+ }
520
+ function getIntensity(value, thresholds) {
521
+ if (value <= 0) return 0;
522
+ if (value <= thresholds[0]) return 1;
523
+ if (value <= thresholds[1]) return 2;
524
+ if (value <= thresholds[2]) return 3;
525
+ return 4;
526
+ }
527
+ function computeStreaks(days) {
528
+ let current = 0;
529
+ let longest = 0;
530
+ let streak = 0;
531
+ let activeDays = 0;
532
+ for (const day of days) {
533
+ if (day.value > 0) {
534
+ streak += 1;
535
+ activeDays += 1;
536
+ longest = Math.max(longest, streak);
537
+ } else {
538
+ streak = 0;
539
+ }
540
+ }
541
+ for (let index = days.length - 1; index >= 0; index -= 1) {
542
+ if ((days[index]?.value ?? 0) > 0) {
543
+ current += 1;
544
+ } else if (current > 0) {
545
+ break;
546
+ }
547
+ }
548
+ return { current, longest, activeDays };
549
+ }
550
+ function buildActivityGraph(stats, metric, requestedDays = 365) {
551
+ const today = startOfDay(/* @__PURE__ */ new Date());
552
+ const endDate = stats.dateRange.end ? startOfDay(new Date(stats.dateRange.end)) : today;
553
+ const effectiveEnd = endDate > today ? today : endDate;
554
+ const effectiveStart = addDays(effectiveEnd, -(requestedDays - 1));
555
+ const paddedStart = startOfWeekMonday(effectiveStart);
556
+ const paddedEnd = endOfWeekSunday(effectiveEnd);
557
+ const dayMap = new Map(stats.days.map((day) => [day.date, day]));
558
+ const orderedDays = [];
559
+ const positiveValues = [];
560
+ for (let cursor = new Date(paddedStart); cursor <= paddedEnd; cursor = addDays(cursor, 1)) {
561
+ const key = toDateKey(cursor);
562
+ const sourceDay = dayMap.get(key);
563
+ const value = sourceDay ? getMetricValue(sourceDay, metric) : 0;
564
+ if (cursor >= effectiveStart && cursor <= effectiveEnd && value > 0) {
565
+ positiveValues.push(value);
566
+ }
567
+ orderedDays.push({
568
+ date: key,
569
+ value,
570
+ intensity: 0,
571
+ inputTokens: sourceDay?.inputTokens ?? 0,
572
+ outputTokens: sourceDay?.outputTokens ?? 0,
573
+ cacheWriteTokens: sourceDay?.cacheWriteTokens ?? 0,
574
+ cacheReadTokens: sourceDay?.cacheReadTokens ?? 0,
575
+ totalTokens: sourceDay?.totalTokens ?? 0,
576
+ sessionCount: sourceDay?.sessionCount ?? 0,
577
+ messageCount: sourceDay?.messageCount ?? 0
578
+ });
579
+ }
580
+ positiveValues.sort((a, b) => a - b);
581
+ const thresholds = [
582
+ Math.max(1, quantile(positiveValues, 0.25)),
583
+ Math.max(1, quantile(positiveValues, 0.5)),
584
+ Math.max(1, quantile(positiveValues, 0.75))
585
+ ];
586
+ for (const day of orderedDays) {
587
+ day.intensity = getIntensity(day.value, thresholds);
588
+ }
589
+ const weeks = [];
590
+ for (let index = 0; index < orderedDays.length; index += 7) {
591
+ weeks.push(orderedDays.slice(index, index + 7));
592
+ }
593
+ const monthLabels = [];
594
+ const seenMonths = /* @__PURE__ */ new Set();
595
+ for (let weekIndex = 0; weekIndex < weeks.length; weekIndex += 1) {
596
+ const week = weeks[weekIndex];
597
+ const firstRealDay = week?.find((day) => day.date >= toDateKey(effectiveStart) && day.date <= toDateKey(effectiveEnd));
598
+ if (!firstRealDay) continue;
599
+ const monthKey = firstRealDay.date.slice(0, 7);
600
+ if (seenMonths.has(monthKey)) continue;
601
+ seenMonths.add(monthKey);
602
+ const monthDate = /* @__PURE__ */ new Date(`${monthKey}-01T12:00:00`);
603
+ monthLabels.push({
604
+ weekIndex,
605
+ label: monthDate.toLocaleString("en-US", { month: "short" })
606
+ });
607
+ }
608
+ const visibleDays = orderedDays.filter((day) => day.date >= toDateKey(effectiveStart) && day.date <= toDateKey(effectiveEnd));
609
+ const streaks = computeStreaks(visibleDays);
610
+ const recent30Total = visibleDays.slice(-30).reduce((sum, day) => sum + day.value, 0);
611
+ const favoriteModel = stats.modelBreakdown[0];
612
+ return {
613
+ metric,
614
+ startDate: toDateKey(effectiveStart),
615
+ endDate: toDateKey(effectiveEnd),
616
+ weeks,
617
+ monthLabels,
618
+ weekdayLabels: WEEKDAY_LABELS,
619
+ legend: buildLegend(thresholds),
620
+ summary: {
621
+ activeDays: streaks.activeDays,
622
+ currentStreak: streaks.current,
623
+ longestStreak: streaks.longest,
624
+ recent30DayTotal: recent30Total,
625
+ favoriteModel: favoriteModel?.model || "Unknown",
626
+ favoriteModelTokens: favoriteModel?.tokens || 0,
627
+ inputTokens: stats.totals.inputTokens,
628
+ outputTokens: stats.totals.outputTokens,
629
+ cacheWriteTokens: stats.totals.cacheWriteTokens,
630
+ cacheReadTokens: stats.totals.cacheReadTokens,
631
+ totalTokens: stats.totals.totalTokens,
632
+ totalMessages: stats.totals.messageCount,
633
+ totalSessions: stats.totals.sessionCount
634
+ }
635
+ };
636
+ }
637
+ function buildActivityTitle(source, metric) {
638
+ const sourceLabel = source === "codex" ? "Codex" : source === "combined" ? "AI Coding" : "Claude";
639
+ const metricLabel = metric === "tokens" ? "Activity" : metric === "sessions" ? "Session Activity" : "Message Activity";
640
+ return `${sourceLabel} ${metricLabel}`;
641
+ }
642
+ function buildActivityArtifactPayload(stats, metric, requestedDays = 365) {
643
+ return {
644
+ title: buildActivityTitle(stats.source, metric),
645
+ source: stats.source,
646
+ activity: buildActivityGraph(stats, metric, requestedDays)
647
+ };
648
+ }
649
+ function buildActivityArtifact(stats, metric, requestedDays = 365) {
650
+ return {
651
+ type: "activity",
652
+ schemaVersion: 1,
653
+ source: stats.source,
654
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
655
+ renderOptions: {
656
+ canonicalPath: "activity",
657
+ theme: "light"
658
+ },
659
+ payload: buildActivityArtifactPayload(stats, metric, requestedDays)
660
+ };
661
+ }
662
+
663
+ // src/usage/loader.ts
664
+ import { promises as fs } from "fs";
665
+ import { homedir } from "os";
666
+ import { join, basename } from "path";
667
+
668
+ // src/pricing.ts
669
+ var MODEL_PRICING = {
670
+ // Opus 4.6 (same pricing as Opus 4.5)
671
+ "claude-opus-4-6-20260101": {
672
+ input: 5,
673
+ output: 25,
674
+ cacheWrite: 6.25,
675
+ cacheRead: 0.5
676
+ },
677
+ // Opus 4.5 (cheaper than Opus 4.1)
678
+ "claude-opus-4-5-20251101": {
679
+ input: 5,
680
+ output: 25,
681
+ cacheWrite: 6.25,
682
+ cacheRead: 0.5
683
+ },
684
+ // Sonnet 4.6 (same pricing as Sonnet 4.5)
685
+ "claude-sonnet-4-6-20260101": {
686
+ input: 3,
687
+ output: 15,
688
+ cacheWrite: 3.75,
689
+ cacheRead: 0.3
690
+ },
691
+ // Sonnet 4.5
692
+ "claude-sonnet-4-5-20250929": {
693
+ input: 3,
694
+ output: 15,
695
+ cacheWrite: 3.75,
696
+ cacheRead: 0.3
697
+ },
698
+ // Opus 4.1 (MORE expensive than Opus 4.5)
699
+ "claude-opus-4-1-20250805": {
700
+ input: 15,
701
+ output: 75,
702
+ cacheWrite: 18.75,
703
+ cacheRead: 1.5
704
+ },
705
+ // Haiku 4.5
706
+ "claude-haiku-4-5-20251001": {
707
+ input: 1,
708
+ output: 5,
709
+ cacheWrite: 1.25,
710
+ cacheRead: 0.1
711
+ },
712
+ // Sonnet 3.5 (legacy - same as Sonnet 4.5)
713
+ "claude-3-5-sonnet-20241022": {
714
+ input: 3,
715
+ output: 15,
716
+ cacheWrite: 3.75,
717
+ cacheRead: 0.3
718
+ },
719
+ "claude-3-5-sonnet-20240620": {
720
+ input: 3,
721
+ output: 15,
722
+ cacheWrite: 3.75,
723
+ cacheRead: 0.3
724
+ },
725
+ // Haiku 3.5 (legacy - same as Haiku 4.5)
726
+ "claude-3-5-haiku-20241022": {
727
+ input: 1,
728
+ output: 5,
729
+ cacheWrite: 1.25,
730
+ cacheRead: 0.1
731
+ }
732
+ };
733
+ function getModelPricing(modelName) {
734
+ if (MODEL_PRICING[modelName]) {
735
+ return MODEL_PRICING[modelName];
736
+ }
737
+ if (modelName.includes("opus-4-6") || modelName.includes("opus-4.6")) {
738
+ return MODEL_PRICING["claude-opus-4-6-20260101"];
739
+ }
740
+ if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) {
741
+ return MODEL_PRICING["claude-opus-4-5-20251101"];
742
+ }
743
+ if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1") || modelName.includes("opus-4")) {
744
+ return MODEL_PRICING["claude-opus-4-1-20250805"];
745
+ }
746
+ if (modelName.includes("sonnet-4-6") || modelName.includes("sonnet-4.6")) {
747
+ return MODEL_PRICING["claude-sonnet-4-6-20260101"];
748
+ }
749
+ if (modelName.includes("sonnet-4-5") || modelName.includes("sonnet-4.5")) {
750
+ return MODEL_PRICING["claude-sonnet-4-5-20250929"];
751
+ }
752
+ if (modelName.includes("sonnet")) {
753
+ return MODEL_PRICING["claude-3-5-sonnet-20241022"];
754
+ }
755
+ if (modelName.includes("haiku-4-5") || modelName.includes("haiku-4.5")) {
756
+ return MODEL_PRICING["claude-haiku-4-5-20251001"];
757
+ }
758
+ if (modelName.includes("haiku")) {
759
+ return MODEL_PRICING["claude-3-5-haiku-20241022"];
760
+ }
761
+ return MODEL_PRICING["claude-sonnet-4-5-20250929"];
762
+ }
763
+ function getModelDisplayName(modelName) {
764
+ if (modelName.includes("opus-4-6") || modelName.includes("opus-4.6")) return "Opus 4.6";
765
+ if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) return "Opus 4.5";
766
+ if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1")) return "Opus 4.1";
767
+ if (modelName.includes("opus")) return "Opus";
768
+ if (modelName.includes("sonnet-4-6") || modelName.includes("sonnet-4.6")) return "Sonnet 4.6";
769
+ if (modelName.includes("sonnet-4-5") || modelName.includes("sonnet-4.5")) return "Sonnet 4.5";
770
+ if (modelName.includes("sonnet-3-5") || modelName.includes("sonnet-3.5")) return "Sonnet 3.5";
771
+ if (modelName.includes("sonnet")) return "Sonnet";
772
+ if (modelName.includes("haiku-4-5") || modelName.includes("haiku-4.5")) return "Haiku 4.5";
773
+ if (modelName.includes("haiku-3-5") || modelName.includes("haiku-3.5")) return "Haiku 3.5";
774
+ if (modelName.includes("haiku")) return "Haiku";
775
+ return modelName;
776
+ }
777
+
398
778
  // src/usage/loader.ts
399
779
  var MAX_RECURSION_DEPTH = 10;
400
780
  function toLocalDateString(isoTimestamp) {
@@ -515,6 +895,7 @@ async function parseClaudeJsonl(projectFilter) {
515
895
  cacheWriteTokens,
516
896
  cacheReadTokens,
517
897
  cost,
898
+ messageCount: 1,
518
899
  source: "claude",
519
900
  sessionId,
520
901
  timestamp
@@ -571,7 +952,10 @@ async function parseCodexJsonl() {
571
952
  cacheWriteTokens: 0,
572
953
  cacheReadTokens: cachedInputTokens,
573
954
  cost,
574
- source: "codex"
955
+ messageCount: 1,
956
+ source: "codex",
957
+ sessionId: basename(filePath, ".jsonl"),
958
+ timestamp
575
959
  });
576
960
  }
577
961
  } catch {
@@ -772,6 +1156,76 @@ function computeModelBreakdown(entries) {
772
1156
  percentage: totalTokens > 0 ? Math.round(data.tokens / totalTokens * 100) : 0
773
1157
  })).sort((a, b) => b.tokens - a.tokens);
774
1158
  }
1159
+ function computeActivityStats(entries, source) {
1160
+ const dayMap = /* @__PURE__ */ new Map();
1161
+ for (const entry of entries) {
1162
+ const existing = dayMap.get(entry.date);
1163
+ if (existing) {
1164
+ existing.inputTokens += entry.inputTokens;
1165
+ existing.outputTokens += entry.outputTokens;
1166
+ existing.cacheWriteTokens += entry.cacheWriteTokens;
1167
+ existing.cacheReadTokens += entry.cacheReadTokens;
1168
+ existing.totalTokens += entry.inputTokens + entry.outputTokens + entry.cacheWriteTokens + entry.cacheReadTokens;
1169
+ existing.messageCount += entry.messageCount;
1170
+ if (entry.sessionId) {
1171
+ existing.sessionCount += 0;
1172
+ }
1173
+ } else {
1174
+ dayMap.set(entry.date, {
1175
+ date: entry.date,
1176
+ inputTokens: entry.inputTokens,
1177
+ outputTokens: entry.outputTokens,
1178
+ cacheWriteTokens: entry.cacheWriteTokens,
1179
+ cacheReadTokens: entry.cacheReadTokens,
1180
+ totalTokens: entry.inputTokens + entry.outputTokens + entry.cacheWriteTokens + entry.cacheReadTokens,
1181
+ sessionCount: 0,
1182
+ messageCount: entry.messageCount
1183
+ });
1184
+ }
1185
+ }
1186
+ const sessionMap = /* @__PURE__ */ new Map();
1187
+ for (const entry of entries) {
1188
+ if (!entry.sessionId) continue;
1189
+ const sessions = sessionMap.get(entry.date) || /* @__PURE__ */ new Set();
1190
+ sessions.add(entry.sessionId);
1191
+ sessionMap.set(entry.date, sessions);
1192
+ }
1193
+ const days = Array.from(dayMap.values()).map((day) => ({
1194
+ ...day,
1195
+ sessionCount: sessionMap.get(day.date)?.size || 0
1196
+ })).sort((a, b) => a.date.localeCompare(b.date));
1197
+ const totals = days.reduce(
1198
+ (acc, day) => ({
1199
+ inputTokens: acc.inputTokens + day.inputTokens,
1200
+ outputTokens: acc.outputTokens + day.outputTokens,
1201
+ cacheWriteTokens: acc.cacheWriteTokens + day.cacheWriteTokens,
1202
+ cacheReadTokens: acc.cacheReadTokens + day.cacheReadTokens,
1203
+ totalTokens: acc.totalTokens + day.totalTokens,
1204
+ sessionCount: acc.sessionCount + day.sessionCount,
1205
+ messageCount: acc.messageCount + day.messageCount
1206
+ }),
1207
+ {
1208
+ inputTokens: 0,
1209
+ outputTokens: 0,
1210
+ cacheWriteTokens: 0,
1211
+ cacheReadTokens: 0,
1212
+ totalTokens: 0,
1213
+ sessionCount: 0,
1214
+ messageCount: 0
1215
+ }
1216
+ );
1217
+ const dates = days.map((entry) => entry.date);
1218
+ return {
1219
+ source,
1220
+ dateRange: {
1221
+ start: dates[0] || "",
1222
+ end: dates[dates.length - 1] || ""
1223
+ },
1224
+ days,
1225
+ totals,
1226
+ modelBreakdown: computeModelBreakdown(entries)
1227
+ };
1228
+ }
775
1229
  async function loadUsageStats(options) {
776
1230
  const { aggregation, since, until, codexOnly, combined, projectFilter } = options;
777
1231
  let entries = [];
@@ -813,19 +1267,41 @@ async function loadUsageStats(options) {
813
1267
  rows = aggregateByDay(entries);
814
1268
  break;
815
1269
  }
816
- const totals = aggregation === "total" ? computeTotals(aggregateByDay(entries)) : computeTotals(rows);
817
- const modelBreakdown = computeModelBreakdown(entries);
1270
+ const totals = aggregation === "total" ? computeTotals(aggregateByDay(entries)) : computeTotals(rows);
1271
+ const modelBreakdown = computeModelBreakdown(entries);
1272
+ let source = "claude";
1273
+ if (codexOnly) source = "codex";
1274
+ else if (combined) source = "combined";
1275
+ return {
1276
+ rows,
1277
+ totals,
1278
+ source,
1279
+ aggregation,
1280
+ dateRange,
1281
+ modelBreakdown
1282
+ };
1283
+ }
1284
+ async function loadActivityStats(options) {
1285
+ const { since, until, codexOnly, combined, projectFilter } = options;
1286
+ let entries = [];
1287
+ if (!codexOnly) {
1288
+ entries = entries.concat(await parseClaudeJsonl(projectFilter));
1289
+ }
1290
+ if (codexOnly || combined) {
1291
+ entries = entries.concat(await parseCodexJsonl());
1292
+ }
1293
+ if (entries.length === 0) {
1294
+ return null;
1295
+ }
1296
+ entries = filterByDateRange(entries, since, until);
1297
+ entries = entries.filter((entry) => !entry.model.toLowerCase().includes("synthetic"));
1298
+ if (entries.length === 0) {
1299
+ return null;
1300
+ }
818
1301
  let source = "claude";
819
1302
  if (codexOnly) source = "codex";
820
1303
  else if (combined) source = "combined";
821
- return {
822
- rows,
823
- totals,
824
- source,
825
- aggregation,
826
- dateRange,
827
- modelBreakdown
828
- };
1304
+ return computeActivityStats(entries, source);
829
1305
  }
830
1306
 
831
1307
  // src/usage/table.ts
@@ -1698,176 +2174,6 @@ function combineWrappedStats(claude, codex) {
1698
2174
  };
1699
2175
  }
1700
2176
 
1701
- // src/url-encoder.ts
1702
- function formatCompactNumber(num) {
1703
- if (num >= 1e9) {
1704
- return `${(num / 1e9).toFixed(1)}B`;
1705
- }
1706
- if (num >= 1e6) {
1707
- return `${(num / 1e6).toFixed(1)}M`;
1708
- }
1709
- if (num >= 1e3) {
1710
- return `${(num / 1e3).toFixed(1)}K`;
1711
- }
1712
- return num.toString();
1713
- }
1714
- var dayToNumber = {
1715
- Sunday: 0,
1716
- Monday: 1,
1717
- Tuesday: 2,
1718
- Wednesday: 3,
1719
- Thursday: 4,
1720
- Friday: 5,
1721
- Saturday: 6
1722
- };
1723
- function encodeStatsToUrl(stats, baseUrl = "https://vibestats.wolfai.dev") {
1724
- const params = new URLSearchParams();
1725
- params.set("s", stats.sessions.toString());
1726
- params.set("t", formatCompactNumber(stats.totalTokens));
1727
- params.set("c", stats.totalCost.toFixed(2));
1728
- params.set("d", stats.daysActive.toString());
1729
- params.set("ls", stats.longestStreak.toString());
1730
- params.set("cs", stats.currentStreak.toString());
1731
- params.set("ph", stats.peakHour.toString());
1732
- params.set("pd", (dayToNumber[stats.peakDay] ?? 0).toString());
1733
- params.set("fm", getModelTokenFromDisplayName(stats.favoriteModel));
1734
- const mbParts = stats.modelBreakdown.slice(0, 3).map((m) => {
1735
- const abbr = getModelTokenFromDisplayName(m.model);
1736
- return `${abbr}:${m.percentage}`;
1737
- });
1738
- params.set("mb", mbParts.join(","));
1739
- if (stats.topTools && stats.topTools.length > 0) {
1740
- params.set("tt", stats.topTools.slice(0, 5).join(","));
1741
- }
1742
- if (stats.developerStyle) {
1743
- const styleMap = {
1744
- reader: "r",
1745
- writer: "w",
1746
- executor: "e",
1747
- balanced: "b"
1748
- };
1749
- params.set("st", styleMap[stats.developerStyle] || "b");
1750
- }
1751
- if (stats.topProject) {
1752
- params.set("tp", stats.topProject);
1753
- }
1754
- if (stats.projectCount) {
1755
- params.set("pc", stats.projectCount.toString());
1756
- }
1757
- params.set("wg", formatCompactNumber(stats.wordsGenerated));
1758
- const firstDate = new Date(stats.firstSessionDate);
1759
- params.set("fad", firstDate.toISOString().split("T")[0].replace(/-/g, ""));
1760
- if (stats.source && stats.source !== "claude") {
1761
- params.set("src", stats.source);
1762
- }
1763
- return `${baseUrl}/wrapped/?${params.toString()}`;
1764
- }
1765
- var CLAUDE_DISPLAY_TO_TOKEN = {
1766
- "Opus 4.6": "o46",
1767
- "Opus 4.5": "o45",
1768
- "Opus 4.1": "o41",
1769
- Opus: "opus",
1770
- "Sonnet 4.6": "s46",
1771
- "Sonnet 4.5": "s45",
1772
- "Sonnet 3.5": "s35",
1773
- Sonnet: "sonnet",
1774
- "Haiku 4.5": "h45",
1775
- "Haiku 3.5": "h35",
1776
- Haiku: "haiku"
1777
- };
1778
- function getModelTokenFromDisplayName(displayName) {
1779
- if (CLAUDE_DISPLAY_TO_TOKEN[displayName]) {
1780
- return CLAUDE_DISPLAY_TO_TOKEN[displayName];
1781
- }
1782
- const normalized = displayName.toLowerCase();
1783
- if (normalized.includes("opus") && normalized.includes("4.6")) return "o46";
1784
- if (normalized.includes("opus") && normalized.includes("4.5")) return "o45";
1785
- if (normalized.includes("opus") && normalized.includes("4.1")) return "o41";
1786
- if (normalized.includes("opus")) return "opus";
1787
- if (normalized.includes("sonnet") && normalized.includes("4.6")) return "s46";
1788
- if (normalized.includes("sonnet") && normalized.includes("4.5")) return "s45";
1789
- if (normalized.includes("sonnet") && normalized.includes("3.5")) return "s35";
1790
- if (normalized.includes("sonnet")) return "sonnet";
1791
- if (normalized.includes("haiku") && normalized.includes("4.5")) return "h45";
1792
- if (normalized.includes("haiku") && normalized.includes("3.5")) return "h35";
1793
- if (normalized.includes("haiku")) return "haiku";
1794
- return getCodexModelAbbreviation(displayName);
1795
- }
1796
- function aggregateRowsToMonthly(rows) {
1797
- const monthMap = /* @__PURE__ */ new Map();
1798
- for (const row of rows) {
1799
- const month = row.key.slice(0, 7);
1800
- const existing = monthMap.get(month);
1801
- if (existing) {
1802
- existing.inputTokens += row.inputTokens;
1803
- existing.outputTokens += row.outputTokens;
1804
- existing.cacheWriteTokens += row.cacheWriteTokens;
1805
- existing.cacheReadTokens += row.cacheReadTokens;
1806
- existing.totalTokens += row.totalTokens;
1807
- existing.cost += row.cost;
1808
- } else {
1809
- monthMap.set(month, { ...row, key: month });
1810
- }
1811
- }
1812
- return Array.from(monthMap.values()).sort((a, b) => a.key.localeCompare(b.key));
1813
- }
1814
- function encodeUsageToUrl(stats, baseUrl = "https://vibestats.wolfai.dev") {
1815
- const params = new URLSearchParams();
1816
- if (stats.source !== "claude") {
1817
- params.set("src", stats.source);
1818
- }
1819
- const formatDateCompact = (d) => d.replace(/-/g, "");
1820
- const startMs = new Date(stats.dateRange.start).getTime();
1821
- const endMs = new Date(stats.dateRange.end).getTime();
1822
- const daySpan = Math.ceil((endMs - startMs) / (1e3 * 60 * 60 * 24));
1823
- const useMonthly = stats.aggregation === "daily" && daySpan > 31;
1824
- const aggMap = { daily: "d", monthly: "m", model: "mo", total: "t" };
1825
- const effectiveAgg = useMonthly ? "monthly" : stats.aggregation;
1826
- params.set("agg", aggMap[effectiveAgg] || "d");
1827
- let rowsToEncode;
1828
- if (useMonthly) {
1829
- rowsToEncode = aggregateRowsToMonthly(stats.rows);
1830
- } else {
1831
- rowsToEncode = stats.rows.slice(-31);
1832
- }
1833
- const startDate = useMonthly ? stats.dateRange.start : rowsToEncode[0]?.key || stats.dateRange.start;
1834
- const endDate = useMonthly ? stats.dateRange.end : rowsToEncode[rowsToEncode.length - 1]?.key || stats.dateRange.end;
1835
- params.set("dr", `${formatDateCompact(startDate)}-${formatDateCompact(endDate)}`);
1836
- const rows = rowsToEncode.map((row) => {
1837
- let key = row.key;
1838
- if (effectiveAgg === "daily" && row.key.length === 10) {
1839
- key = row.key.slice(5).replace("-", "");
1840
- }
1841
- return [
1842
- key,
1843
- formatCompactNumber(row.inputTokens),
1844
- formatCompactNumber(row.outputTokens),
1845
- formatCompactNumber(row.cacheWriteTokens),
1846
- formatCompactNumber(row.cacheReadTokens),
1847
- formatCompactNumber(row.totalTokens),
1848
- row.cost.toFixed(2)
1849
- ].join(":");
1850
- });
1851
- params.set("rows", rows.join("|"));
1852
- const t = stats.totals;
1853
- params.set("tot", [
1854
- formatCompactNumber(t.inputTokens),
1855
- formatCompactNumber(t.outputTokens),
1856
- formatCompactNumber(t.cacheWriteTokens),
1857
- formatCompactNumber(t.cacheReadTokens),
1858
- formatCompactNumber(t.totalTokens),
1859
- t.cost.toFixed(2)
1860
- ].join(":"));
1861
- if (stats.modelBreakdown.length > 0) {
1862
- const mb = stats.modelBreakdown.slice(0, 5).map((m) => {
1863
- const abbr = getModelTokenFromDisplayName(m.model);
1864
- return `${abbr}:${m.percentage}:${m.cost.toFixed(2)}`;
1865
- });
1866
- params.set("mb", mb.join(","));
1867
- }
1868
- return `${baseUrl}?${params.toString()}`;
1869
- }
1870
-
1871
2177
  // src/display.ts
1872
2178
  var colors2 = {
1873
2179
  reset: "\x1B[0m",
@@ -1966,27 +2272,72 @@ function formatHour(hour) {
1966
2272
  if (hour === 12) return "12pm";
1967
2273
  return `${hour - 12}pm`;
1968
2274
  }
2275
+ function displayActivityStats(artifact, url, options) {
2276
+ const c = getColors2(options?.theme);
2277
+ const summary = artifact.activity.summary;
2278
+ console.log();
2279
+ console.log(`${c.cyan}${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}`);
2280
+ console.log(`${c.cyan}${c.bold}\u2551${c.reset} ${c.white}${c.bold}${artifact.title}${c.reset} ${c.cyan}${c.bold}\u2551${c.reset}`);
2281
+ console.log(`${c.cyan}${c.bold}\u255A\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\u255D${c.reset}`);
2282
+ console.log();
2283
+ console.log(`${c.bold}\u{1F4C8} Activity Heatmap${c.reset}`);
2284
+ console.log(`${c.gray}${"\u2500".repeat(60)}${c.reset}`);
2285
+ console.log(` Window: ${artifact.activity.startDate} \u2192 ${artifact.activity.endDate}`);
2286
+ console.log(` Metric: ${artifact.activity.metric}`);
2287
+ console.log(` Active days: ${c.cyan}${c.bold}${summary.activeDays}${c.reset}`);
2288
+ console.log();
2289
+ console.log(`${c.bold}\u26A1 Highlights${c.reset}`);
2290
+ console.log(`${c.gray}${"\u2500".repeat(60)}${c.reset}`);
2291
+ console.log(` Current streak: ${c.green}${summary.currentStreak} days${c.reset}`);
2292
+ console.log(` Longest streak: ${c.cyan}${summary.longestStreak} days${c.reset}`);
2293
+ console.log(` Recent 30 days: ${c.white}${c.bold}${formatCompactNumber(summary.recent30DayTotal)}${c.reset}`);
2294
+ console.log(` Favorite model: ${c.amber}${summary.favoriteModel}${c.reset}`);
2295
+ console.log();
2296
+ console.log(`${c.bold}\u{1F517} Share Activity${c.reset}`);
2297
+ console.log(`${c.gray}${"\u2500".repeat(60)}${c.reset}`);
2298
+ if (options?.shortUrl) {
2299
+ console.log(` ${c.cyan}${c.bold}${options.shortUrl}${c.reset}`);
2300
+ console.log(` ${c.dim}(Canonical URL: ${url})${c.reset}`);
2301
+ } else {
2302
+ console.log(` ${c.cyan}${url}${c.reset}`);
2303
+ }
2304
+ if (options?.imageUrl) {
2305
+ console.log(` Image: ${c.white}${options.imageUrl}${c.reset}`);
2306
+ }
2307
+ console.log();
2308
+ }
1969
2309
 
1970
- // src/shortener.ts
1971
- async function createShortlink(params, baseUrl) {
1972
- const queryString = params.includes("?") ? params.split("?")[1] : params;
2310
+ // src/share-client.ts
2311
+ function extractLegacyQuery(legacyUrl) {
2312
+ if (!legacyUrl) return null;
2313
+ const queryIndex = legacyUrl.indexOf("?");
2314
+ if (queryIndex === -1) return null;
2315
+ return legacyUrl.slice(queryIndex + 1) || null;
2316
+ }
2317
+ async function publishArtifact(artifact, baseUrl, legacyUrl) {
1973
2318
  try {
1974
- const apiUrl = new URL("/api/shorten", baseUrl);
2319
+ const apiUrl = new URL("/vibestats/shares", baseUrl);
1975
2320
  const response = await fetch(apiUrl.toString(), {
1976
2321
  method: "POST",
1977
2322
  headers: {
1978
2323
  "Content-Type": "application/json"
1979
2324
  },
1980
- body: JSON.stringify({ params: queryString })
2325
+ body: JSON.stringify({
2326
+ artifact,
2327
+ legacyQuery: extractLegacyQuery(legacyUrl)
2328
+ })
1981
2329
  });
1982
2330
  if (!response.ok) {
1983
2331
  return null;
1984
2332
  }
1985
2333
  const data = await response.json();
1986
- if (data.slug) {
1987
- return `${baseUrl}/s/${data.slug}`;
1988
- }
1989
- return null;
2334
+ return {
2335
+ id: data.id || data.slug,
2336
+ slug: data.slug,
2337
+ url: data.url,
2338
+ shortUrl: data.shortUrl,
2339
+ imageUrl: data.imageUrl || null
2340
+ };
1990
2341
  } catch {
1991
2342
  return null;
1992
2343
  }
@@ -2140,6 +2491,65 @@ function parseLastDaysFlag(args) {
2140
2491
  shorthandDays.sort((a, b) => a - b);
2141
2492
  return shorthandDays[0];
2142
2493
  }
2494
+ function parseActivityMetric(value) {
2495
+ if (value === "sessions" || value === "messages" || value === "tokens") {
2496
+ return value;
2497
+ }
2498
+ return "tokens";
2499
+ }
2500
+ function parseActivityDays(value) {
2501
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
2502
+ return Math.round(value);
2503
+ }
2504
+ if (typeof value === "string") {
2505
+ const parsed = Number.parseInt(value, 10);
2506
+ if (Number.isFinite(parsed) && parsed > 0) {
2507
+ return parsed;
2508
+ }
2509
+ }
2510
+ return 365;
2511
+ }
2512
+ function buildUsageArtifact(stats) {
2513
+ return {
2514
+ type: "usage",
2515
+ schemaVersion: 1,
2516
+ source: stats.source,
2517
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2518
+ renderOptions: {
2519
+ canonicalPath: "usage",
2520
+ theme: "light"
2521
+ },
2522
+ payload: stats
2523
+ };
2524
+ }
2525
+ function buildWrappedArtifact(stats) {
2526
+ return {
2527
+ type: "wrapped",
2528
+ schemaVersion: 1,
2529
+ source: stats.source || "claude",
2530
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2531
+ renderOptions: {
2532
+ canonicalPath: "wrapped",
2533
+ theme: "light"
2534
+ },
2535
+ payload: stats
2536
+ };
2537
+ }
2538
+ async function publishArtifactWithFallback(artifact, baseUrl, fallbackUrl, preferCanonical = false) {
2539
+ const published = await publishArtifact(artifact, baseUrl, fallbackUrl);
2540
+ if (!published) {
2541
+ return {
2542
+ canonicalUrl: fallbackUrl,
2543
+ shareUrl: fallbackUrl,
2544
+ imageUrl: null
2545
+ };
2546
+ }
2547
+ return {
2548
+ canonicalUrl: published.url,
2549
+ shareUrl: preferCanonical ? published.url : published.shortUrl,
2550
+ imageUrl: published.imageUrl || null
2551
+ };
2552
+ }
2143
2553
  var main = defineCommand({
2144
2554
  meta: {
2145
2555
  name: "vibestats",
@@ -2154,6 +2564,11 @@ var main = defineCommand({
2154
2564
  description: "Show annual wrapped summary instead of usage stats",
2155
2565
  default: false
2156
2566
  },
2567
+ activity: {
2568
+ type: "boolean",
2569
+ description: "Show the GitHub-style activity graph summary",
2570
+ default: false
2571
+ },
2157
2572
  // Data source
2158
2573
  codex: {
2159
2574
  type: "boolean",
@@ -2220,6 +2635,14 @@ var main = defineCommand({
2220
2635
  description: "Use compact table format (hide cache columns)",
2221
2636
  default: false
2222
2637
  },
2638
+ metric: {
2639
+ type: "string",
2640
+ description: "Activity metric: tokens, sessions, or messages"
2641
+ },
2642
+ days: {
2643
+ type: "string",
2644
+ description: "Number of days to include in the activity graph"
2645
+ },
2223
2646
  // Share option for usage mode
2224
2647
  share: {
2225
2648
  type: "boolean",
@@ -2272,7 +2695,9 @@ var main = defineCommand({
2272
2695
  console.log(JSON.stringify(config, null, 2));
2273
2696
  return;
2274
2697
  }
2275
- if (args.wrapped) {
2698
+ if (args.activity) {
2699
+ await runActivity(args, config);
2700
+ } else if (args.wrapped) {
2276
2701
  await runWrapped(args, config);
2277
2702
  } else {
2278
2703
  await runUsage(args, config);
@@ -2343,10 +2768,13 @@ async function runUsage(args, config) {
2343
2768
  let shareUrl = null;
2344
2769
  if (args.share) {
2345
2770
  const fullUrl = encodeUsageToUrl(stats, baseUrl);
2346
- if (!args["no-short"]) {
2347
- shareUrl = await createShortlink(fullUrl, baseUrl);
2348
- }
2349
- shareUrl = shareUrl || fullUrl;
2771
+ const shared = await publishArtifactWithFallback(
2772
+ buildUsageArtifact(stats),
2773
+ baseUrl,
2774
+ fullUrl,
2775
+ Boolean(args["no-short"])
2776
+ );
2777
+ shareUrl = shared.shareUrl;
2350
2778
  }
2351
2779
  if (args.quiet && args.share && shareUrl) {
2352
2780
  console.log(shareUrl);
@@ -2379,9 +2807,14 @@ async function runUsage(args, config) {
2379
2807
  }
2380
2808
  async function runWrapped(args, config) {
2381
2809
  const options = resolveOptions(args, config);
2810
+ const metric = parseActivityMetric(args.metric);
2811
+ const days = parseActivityDays(args.days);
2382
2812
  const spinner = createSpinner("Preparing wrapped...");
2383
- const data = await spinner.whilePromise(
2384
- loadData({ codexOnly: args.codex, combined: args.combined })
2813
+ const [data, activityStats] = await spinner.whilePromise(
2814
+ Promise.all([
2815
+ loadData({ codexOnly: args.codex, combined: args.combined }),
2816
+ loadActivityStats({ codexOnly: args.codex, combined: args.combined })
2817
+ ])
2385
2818
  );
2386
2819
  validateData(data, { codexOnly: args.codex, combined: args.combined });
2387
2820
  let claudeStats = null;
@@ -2400,23 +2833,89 @@ async function runWrapped(args, config) {
2400
2833
  } else {
2401
2834
  stats = claudeStats;
2402
2835
  }
2403
- const url = encodeStatsToUrl(stats, options.baseUrl);
2404
- let shortUrl = null;
2405
- if (!args["no-short"]) {
2406
- shortUrl = await createShortlink(url, options.baseUrl);
2836
+ if (activityStats) {
2837
+ stats.activity = buildActivityGraph(activityStats, metric, days);
2407
2838
  }
2839
+ const legacyUrl = encodeStatsToUrl(stats, options.baseUrl);
2840
+ const published = await publishArtifactWithFallback(
2841
+ buildWrappedArtifact(stats),
2842
+ options.baseUrl,
2843
+ legacyUrl,
2844
+ Boolean(args["no-short"])
2845
+ );
2408
2846
  if (options.outputFormat === "json") {
2409
- console.log(JSON.stringify({ ...stats, url, shortUrl }, null, 2));
2847
+ console.log(
2848
+ JSON.stringify(
2849
+ {
2850
+ ...stats,
2851
+ url: published.canonicalUrl,
2852
+ shortUrl: published.shareUrl === published.canonicalUrl ? null : published.shareUrl,
2853
+ imageUrl: published.imageUrl
2854
+ },
2855
+ null,
2856
+ 2
2857
+ )
2858
+ );
2410
2859
  } else if (options.outputFormat === "quiet") {
2411
- console.log(shortUrl || url);
2860
+ console.log(published.shareUrl);
2412
2861
  } else {
2413
- displayWrappedStats(stats, url, {
2862
+ displayWrappedStats(stats, published.canonicalUrl, {
2414
2863
  theme: options.theme,
2415
2864
  hideCost: options.hideCost,
2416
- shortUrl
2865
+ shortUrl: published.shareUrl === published.canonicalUrl ? null : published.shareUrl,
2866
+ imageUrl: published.imageUrl
2417
2867
  });
2418
2868
  }
2419
2869
  }
2870
+ async function runActivity(args, config) {
2871
+ const metric = parseActivityMetric(args.metric);
2872
+ const days = parseActivityDays(args.days);
2873
+ const spinner = createSpinner("Preparing activity graph...");
2874
+ const stats = await spinner.whilePromise(
2875
+ loadActivityStats({
2876
+ codexOnly: args.codex,
2877
+ combined: args.combined,
2878
+ projectFilter: args.project ? process.cwd() : void 0,
2879
+ since: args.since,
2880
+ until: args.until
2881
+ })
2882
+ );
2883
+ if (!stats) {
2884
+ console.error("Error: No activity data found.");
2885
+ process.exit(1);
2886
+ }
2887
+ const artifact = buildActivityArtifact(stats, metric, days);
2888
+ const baseUrl = args.url || config.baseUrl || "https://vibestats.wolfai.dev";
2889
+ const fallbackUrl = encodeActivityToUrl(artifact.payload, baseUrl);
2890
+ let shared = null;
2891
+ if (args.share) {
2892
+ shared = await publishArtifactWithFallback(artifact, baseUrl, fallbackUrl, Boolean(args["no-short"]));
2893
+ }
2894
+ if (args.json) {
2895
+ console.log(
2896
+ JSON.stringify(
2897
+ {
2898
+ ...artifact.payload,
2899
+ url: shared?.canonicalUrl || null,
2900
+ shortUrl: shared && shared.shareUrl !== shared.canonicalUrl ? shared.shareUrl : null,
2901
+ imageUrl: shared?.imageUrl || null
2902
+ },
2903
+ null,
2904
+ 2
2905
+ )
2906
+ );
2907
+ return;
2908
+ }
2909
+ if (args.quiet) {
2910
+ console.log(shared?.shareUrl || fallbackUrl);
2911
+ return;
2912
+ }
2913
+ displayActivityStats(artifact.payload, shared?.canonicalUrl || fallbackUrl, {
2914
+ theme: config.theme,
2915
+ shortUrl: shared && shared.shareUrl !== shared.canonicalUrl ? shared.shareUrl : null,
2916
+ imageUrl: shared?.imageUrl || null
2917
+ });
2918
+ }
2420
2919
  function formatNumber2(n) {
2421
2920
  if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
2422
2921
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibestats",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
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",