vibestats 1.3.3 → 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 +988 -416
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -3,6 +3,663 @@
3
3
  // src/index.ts
4
4
  import { defineCommand, runMain } from "citty";
5
5
 
6
+ // src/codex-pricing.ts
7
+ var GPT_54_PRICING = { input: 2.5, output: 15, cachedInput: 0.25 };
8
+ var GPT_53_PRICING = { input: 1.75, output: 14, cachedInput: 0.175 };
9
+ var GPT_51_PRICING = { input: 1.25, output: 10, cachedInput: 0.125 };
10
+ var GPT_5_MINI_PRICING = { input: 0.25, output: 2, cachedInput: 0.025 };
11
+ var GPT_5_NANO_PRICING = { input: 0.05, output: 0.4, cachedInput: 5e-3 };
12
+ var GPT_4O_PRICING = { input: 2.5, output: 10, cachedInput: 1.25 };
13
+ var GPT_4O_MINI_PRICING = { input: 0.15, output: 0.6, cachedInput: 0.075 };
14
+ var CODEX_MODEL_DEFINITIONS = [
15
+ {
16
+ key: "gpt-5.4",
17
+ displayName: "GPT-5.4",
18
+ token: "g54",
19
+ sortPriority: 77,
20
+ pricing: GPT_54_PRICING,
21
+ aliases: ["GPT-5.4"],
22
+ exactNames: ["gpt-5.4"]
23
+ },
24
+ {
25
+ key: "gpt-5.3-codex-spark",
26
+ displayName: "GPT-5.3 Codex Spark",
27
+ token: "g53s",
28
+ sortPriority: 76,
29
+ pricing: GPT_53_PRICING,
30
+ aliases: ["GPT-5.3 Codex Spark"],
31
+ exactNames: ["gpt-5.3-codex-spark"],
32
+ includePatterns: [["5.3", "spark"]]
33
+ },
34
+ {
35
+ key: "gpt-5.3-codex",
36
+ displayName: "GPT-5.3 Codex",
37
+ token: "g53c",
38
+ sortPriority: 75,
39
+ pricing: GPT_53_PRICING,
40
+ aliases: ["GPT-5.3 Codex"],
41
+ exactNames: ["gpt-5.3-codex"],
42
+ includePatterns: [["5.3", "codex"]]
43
+ },
44
+ {
45
+ key: "gpt-5.3",
46
+ displayName: "GPT-5.3",
47
+ token: "g53",
48
+ sortPriority: 74,
49
+ pricing: GPT_53_PRICING,
50
+ aliases: ["GPT-5.3"],
51
+ exactNames: ["gpt-5.3"],
52
+ includePatterns: [["5.3"]]
53
+ },
54
+ {
55
+ key: "gpt-5.2-codex",
56
+ displayName: "GPT-5.2 Codex",
57
+ token: "g52c",
58
+ sortPriority: 73,
59
+ pricing: GPT_53_PRICING,
60
+ aliases: ["GPT-5.2 Codex"],
61
+ exactNames: ["gpt-5.2-codex"],
62
+ includePatterns: [["5.2", "codex"]]
63
+ },
64
+ {
65
+ key: "gpt-5.2",
66
+ displayName: "GPT-5.2",
67
+ token: "g52",
68
+ sortPriority: 72,
69
+ pricing: GPT_53_PRICING,
70
+ aliases: ["GPT-5.2"],
71
+ exactNames: ["gpt-5.2"],
72
+ includePatterns: [["5.2"]]
73
+ },
74
+ {
75
+ key: "gpt-5.1-codex-max",
76
+ displayName: "GPT-5.1 Codex Max",
77
+ token: "g51cm",
78
+ sortPriority: 71,
79
+ pricing: GPT_51_PRICING,
80
+ aliases: ["GPT-5.1 Codex Max"],
81
+ exactNames: ["gpt-5.1-codex-max"],
82
+ includePatterns: [["5.1", "codex", "max"]]
83
+ },
84
+ {
85
+ key: "legacy-gpt-5.1-max",
86
+ displayName: "GPT-5.1 Max",
87
+ token: "g51m",
88
+ sortPriority: 70,
89
+ pricing: GPT_51_PRICING,
90
+ aliases: ["GPT-5.1 Max"]
91
+ },
92
+ {
93
+ key: "gpt-5.1-codex-mini",
94
+ displayName: "GPT-5.1 Codex Mini",
95
+ token: "g51cn",
96
+ sortPriority: 69,
97
+ pricing: GPT_5_MINI_PRICING,
98
+ aliases: ["GPT-5.1 Codex Mini"],
99
+ exactNames: ["gpt-5.1-codex-mini"],
100
+ includePatterns: [["5.1", "codex", "mini"]]
101
+ },
102
+ {
103
+ key: "legacy-gpt-5.1-mini",
104
+ displayName: "GPT-5.1 Mini",
105
+ token: "g51n",
106
+ sortPriority: 68,
107
+ pricing: GPT_5_MINI_PRICING,
108
+ aliases: ["GPT-5.1 Mini"]
109
+ },
110
+ {
111
+ key: "gpt-5.1-codex",
112
+ displayName: "GPT-5.1 Codex",
113
+ token: "g51c",
114
+ sortPriority: 67,
115
+ pricing: GPT_51_PRICING,
116
+ aliases: ["GPT-5.1 Codex"],
117
+ exactNames: ["gpt-5.1-codex"],
118
+ includePatterns: [["5.1", "codex"]]
119
+ },
120
+ {
121
+ key: "gpt-5.1",
122
+ displayName: "GPT-5.1",
123
+ token: "g51",
124
+ sortPriority: 66,
125
+ pricing: GPT_51_PRICING,
126
+ aliases: ["GPT-5.1"],
127
+ exactNames: ["gpt-5.1"],
128
+ includePatterns: [["5.1"]]
129
+ },
130
+ {
131
+ key: "gpt-5",
132
+ displayName: "GPT-5",
133
+ token: "g5",
134
+ sortPriority: 64,
135
+ pricing: GPT_51_PRICING,
136
+ aliases: ["GPT-5"],
137
+ exactNames: ["gpt-5"]
138
+ },
139
+ {
140
+ key: "gpt-5-codex",
141
+ displayName: "GPT-5 Codex",
142
+ sortPriority: 64,
143
+ pricing: GPT_51_PRICING,
144
+ aliases: ["GPT-5 Codex"],
145
+ exactNames: ["gpt-5-codex"]
146
+ },
147
+ {
148
+ key: "gpt-5-mini",
149
+ displayName: "GPT-5 Mini",
150
+ token: "g5n",
151
+ sortPriority: 63,
152
+ pricing: GPT_5_MINI_PRICING,
153
+ aliases: ["GPT-5 Mini"],
154
+ exactNames: ["gpt-5-mini"]
155
+ },
156
+ {
157
+ key: "gpt-5-codex-mini",
158
+ displayName: "GPT-5 Mini",
159
+ token: "g5n",
160
+ sortPriority: 63,
161
+ pricing: GPT_5_MINI_PRICING,
162
+ aliases: ["GPT-5 Codex Mini"],
163
+ exactNames: ["gpt-5-codex-mini"]
164
+ },
165
+ {
166
+ key: "gpt-5-nano",
167
+ displayName: "GPT-5 Nano",
168
+ sortPriority: 62,
169
+ pricing: GPT_5_NANO_PRICING,
170
+ aliases: ["GPT-5 Nano"],
171
+ exactNames: ["gpt-5-nano"]
172
+ },
173
+ {
174
+ key: "gpt-4o",
175
+ displayName: "GPT-4o",
176
+ token: "g4o",
177
+ sortPriority: 60,
178
+ pricing: GPT_4O_PRICING,
179
+ aliases: ["GPT-4o"],
180
+ exactNames: ["gpt-4o"]
181
+ },
182
+ {
183
+ key: "gpt-4o-mini",
184
+ displayName: "GPT-4o Mini",
185
+ token: "g4om",
186
+ sortPriority: 59,
187
+ pricing: GPT_4O_MINI_PRICING,
188
+ aliases: ["GPT-4o Mini"],
189
+ exactNames: ["gpt-4o-mini"]
190
+ }
191
+ ];
192
+ function normalizeName(value) {
193
+ return value.trim().toLowerCase();
194
+ }
195
+ function findCodexModelDefinition(modelName) {
196
+ const normalized = normalizeName(modelName);
197
+ const exactMatch = CODEX_MODEL_DEFINITIONS.find((definition) => {
198
+ if (normalizeName(definition.displayName) === normalized) {
199
+ return true;
200
+ }
201
+ if (definition.aliases?.some((alias) => normalizeName(alias) === normalized)) {
202
+ return true;
203
+ }
204
+ if (definition.exactNames?.includes(normalized)) {
205
+ return true;
206
+ }
207
+ return false;
208
+ });
209
+ if (exactMatch) {
210
+ return exactMatch;
211
+ }
212
+ return CODEX_MODEL_DEFINITIONS.find(
213
+ (definition) => definition.includePatterns?.some(
214
+ (pattern) => pattern.every((part) => normalized.includes(part))
215
+ ) ?? false
216
+ );
217
+ }
218
+ function formatUnknownCodexDisplayName(modelName) {
219
+ const normalized = normalizeName(modelName);
220
+ if (normalized.includes("4o-mini")) return "GPT-4o Mini";
221
+ if (normalized.includes("4o")) return "GPT-4o";
222
+ const gpt5Match = normalized.match(/gpt[- ]?5(?:\.(\d+))?/);
223
+ if (!gpt5Match) {
224
+ return modelName;
225
+ }
226
+ const version = gpt5Match[1] ? `.${gpt5Match[1]}` : "";
227
+ if (normalized.includes("mini")) return `GPT-5${version} Mini`;
228
+ if (normalized.includes("nano")) return `GPT-5${version} Nano`;
229
+ if (normalized.includes("codex")) return `GPT-5${version} Codex`;
230
+ return `GPT-5${version}`;
231
+ }
232
+ function encodeRawModelToken(displayName) {
233
+ return `raw_${encodeURIComponent(displayName)}`;
234
+ }
235
+ var CODEX_MODEL_PRICING = CODEX_MODEL_DEFINITIONS.reduce(
236
+ (acc, definition) => {
237
+ if (definition.pricing && definition.exactNames?.length) {
238
+ acc[definition.exactNames[0]] = definition.pricing;
239
+ }
240
+ return acc;
241
+ },
242
+ {}
243
+ );
244
+ function getCodexModelPricing(modelName) {
245
+ return findCodexModelDefinition(modelName)?.pricing ?? GPT_51_PRICING;
246
+ }
247
+ function getCodexModelAbbreviation(modelName) {
248
+ const definition = findCodexModelDefinition(modelName);
249
+ if (definition?.token) {
250
+ return definition.token;
251
+ }
252
+ return encodeRawModelToken(getCodexModelDisplayName(modelName));
253
+ }
254
+ function getCodexModelDisplayName(modelName) {
255
+ return findCodexModelDefinition(modelName)?.displayName ?? formatUnknownCodexDisplayName(modelName);
256
+ }
257
+ function getCodexModelSortPriority(modelName) {
258
+ const definition = findCodexModelDefinition(modelName);
259
+ if (definition) {
260
+ return definition.sortPriority;
261
+ }
262
+ const displayName = getCodexModelDisplayName(modelName);
263
+ const gpt5VersionMatch = displayName.match(/^GPT-5\.(\d+)/);
264
+ if (gpt5VersionMatch) {
265
+ return 64 + parseInt(gpt5VersionMatch[1], 10);
266
+ }
267
+ if (displayName === "GPT-5 Mini") return 63;
268
+ if (displayName === "GPT-5 Nano") return 62;
269
+ if (displayName === "GPT-5") return 64;
270
+ if (displayName === "GPT-4o Mini") return 59;
271
+ if (displayName === "GPT-4o") return 60;
272
+ return 0;
273
+ }
274
+ function calculateCodexCost(modelName, inputTokens, outputTokens, cachedInputTokens) {
275
+ const pricing = getCodexModelPricing(modelName);
276
+ const regularInputTokens = inputTokens - cachedInputTokens;
277
+ const inputCost = regularInputTokens / 1e6 * pricing.input;
278
+ const cachedCost = cachedInputTokens / 1e6 * pricing.cachedInput;
279
+ const outputCost = outputTokens / 1e6 * pricing.output;
280
+ return inputCost + cachedCost + outputCost;
281
+ }
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
+
6
663
  // src/usage/loader.ts
7
664
  import { promises as fs } from "fs";
8
665
  import { homedir } from "os";
@@ -24,6 +681,13 @@ var MODEL_PRICING = {
24
681
  cacheWrite: 6.25,
25
682
  cacheRead: 0.5
26
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
+ },
27
691
  // Sonnet 4.5
28
692
  "claude-sonnet-4-5-20250929": {
29
693
  input: 3,
@@ -79,6 +743,9 @@ function getModelPricing(modelName) {
79
743
  if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1") || modelName.includes("opus-4")) {
80
744
  return MODEL_PRICING["claude-opus-4-1-20250805"];
81
745
  }
746
+ if (modelName.includes("sonnet-4-6") || modelName.includes("sonnet-4.6")) {
747
+ return MODEL_PRICING["claude-sonnet-4-6-20260101"];
748
+ }
82
749
  if (modelName.includes("sonnet-4-5") || modelName.includes("sonnet-4.5")) {
83
750
  return MODEL_PRICING["claude-sonnet-4-5-20250929"];
84
751
  }
@@ -98,190 +765,14 @@ function getModelDisplayName(modelName) {
98
765
  if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) return "Opus 4.5";
99
766
  if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1")) return "Opus 4.1";
100
767
  if (modelName.includes("opus")) return "Opus";
768
+ if (modelName.includes("sonnet-4-6") || modelName.includes("sonnet-4.6")) return "Sonnet 4.6";
101
769
  if (modelName.includes("sonnet-4-5") || modelName.includes("sonnet-4.5")) return "Sonnet 4.5";
102
770
  if (modelName.includes("sonnet-3-5") || modelName.includes("sonnet-3.5")) return "Sonnet 3.5";
103
771
  if (modelName.includes("sonnet")) return "Sonnet";
104
772
  if (modelName.includes("haiku-4-5") || modelName.includes("haiku-4.5")) return "Haiku 4.5";
105
773
  if (modelName.includes("haiku-3-5") || modelName.includes("haiku-3.5")) return "Haiku 3.5";
106
774
  if (modelName.includes("haiku")) return "Haiku";
107
- return modelName;
108
- }
109
-
110
- // src/codex-pricing.ts
111
- var CODEX_MODEL_PRICING = {
112
- // GPT-5.3 (latest flagship)
113
- "gpt-5.3": {
114
- input: 1.75,
115
- output: 14,
116
- cachedInput: 0.175
117
- },
118
- "gpt-5.3-codex-spark": {
119
- input: 1.75,
120
- output: 14,
121
- cachedInput: 0.175
122
- },
123
- "gpt-5.3-codex": {
124
- input: 1.75,
125
- output: 14,
126
- cachedInput: 0.175
127
- },
128
- // GPT-5.2
129
- // Cached input is ~10% of input price (prompt caching discount)
130
- "gpt-5.2": {
131
- input: 1.75,
132
- output: 14,
133
- cachedInput: 0.175
134
- },
135
- "gpt-5.2-codex": {
136
- input: 1.75,
137
- output: 14,
138
- cachedInput: 0.175
139
- },
140
- // GPT-5.1 models
141
- "gpt-5.1": {
142
- input: 1.25,
143
- output: 10,
144
- cachedInput: 0.125
145
- },
146
- "gpt-5.1-codex": {
147
- input: 1.25,
148
- output: 10,
149
- cachedInput: 0.125
150
- },
151
- "gpt-5.1-codex-max": {
152
- input: 1.25,
153
- output: 10,
154
- cachedInput: 0.125
155
- },
156
- // GPT-5.1 Mini
157
- "gpt-5.1-codex-mini": {
158
- input: 0.25,
159
- output: 2,
160
- cachedInput: 0.025
161
- },
162
- // GPT-5 base models
163
- "gpt-5": {
164
- input: 1.25,
165
- output: 10,
166
- cachedInput: 0.125
167
- },
168
- "gpt-5-codex": {
169
- input: 1.25,
170
- output: 10,
171
- cachedInput: 0.125
172
- },
173
- // GPT-5 Mini variants
174
- "gpt-5-mini": {
175
- input: 0.25,
176
- output: 2,
177
- cachedInput: 0.025
178
- },
179
- "gpt-5-codex-mini": {
180
- input: 0.25,
181
- output: 2,
182
- cachedInput: 0.025
183
- },
184
- // GPT-5 Nano
185
- "gpt-5-nano": {
186
- input: 0.05,
187
- output: 0.4,
188
- cachedInput: 5e-3
189
- },
190
- // GPT-4o (fallback for older sessions)
191
- "gpt-4o": {
192
- input: 2.5,
193
- output: 10,
194
- cachedInput: 1.25
195
- },
196
- "gpt-4o-mini": {
197
- input: 0.15,
198
- output: 0.6,
199
- cachedInput: 0.075
200
- }
201
- };
202
- function getCodexModelPricing(modelName) {
203
- if (CODEX_MODEL_PRICING[modelName]) {
204
- return CODEX_MODEL_PRICING[modelName];
205
- }
206
- const normalized = modelName.toLowerCase();
207
- if (normalized.includes("5.3") && normalized.includes("spark")) {
208
- return CODEX_MODEL_PRICING["gpt-5.3-codex-spark"];
209
- }
210
- if (normalized.includes("5.3") && normalized.includes("codex")) {
211
- return CODEX_MODEL_PRICING["gpt-5.3-codex"];
212
- }
213
- if (normalized.includes("5.3")) {
214
- return CODEX_MODEL_PRICING["gpt-5.3"];
215
- }
216
- if (normalized.includes("5.2") && normalized.includes("codex")) {
217
- return CODEX_MODEL_PRICING["gpt-5.2-codex"];
218
- }
219
- if (normalized.includes("5.2")) {
220
- return CODEX_MODEL_PRICING["gpt-5.2"];
221
- }
222
- if (normalized.includes("5.1") && normalized.includes("codex") && normalized.includes("mini")) {
223
- return CODEX_MODEL_PRICING["gpt-5.1-codex-mini"];
224
- }
225
- if (normalized.includes("5.1") && normalized.includes("codex") && normalized.includes("max")) {
226
- return CODEX_MODEL_PRICING["gpt-5.1-codex-max"];
227
- }
228
- if (normalized.includes("5.1") && normalized.includes("codex")) {
229
- return CODEX_MODEL_PRICING["gpt-5.1-codex"];
230
- }
231
- if (normalized.includes("5.1") && normalized.includes("max")) {
232
- return CODEX_MODEL_PRICING["gpt-5.1"];
233
- }
234
- if (normalized.includes("5.1") && normalized.includes("mini")) {
235
- return CODEX_MODEL_PRICING["gpt-5.1-codex-mini"];
236
- }
237
- if (normalized.includes("5.1")) {
238
- return CODEX_MODEL_PRICING["gpt-5.1"];
239
- }
240
- if (normalized.includes("nano")) {
241
- return CODEX_MODEL_PRICING["gpt-5-nano"];
242
- }
243
- if (normalized.includes("5") && normalized.includes("mini")) {
244
- return CODEX_MODEL_PRICING["gpt-5-mini"];
245
- }
246
- if (normalized.includes("gpt-5") || normalized.includes("gpt5")) {
247
- return CODEX_MODEL_PRICING["gpt-5"];
248
- }
249
- if (normalized.includes("4o-mini")) {
250
- return CODEX_MODEL_PRICING["gpt-4o-mini"];
251
- }
252
- if (normalized.includes("4o")) {
253
- return CODEX_MODEL_PRICING["gpt-4o"];
254
- }
255
- return CODEX_MODEL_PRICING["gpt-5"];
256
- }
257
- function getCodexModelDisplayName(modelName) {
258
- const normalized = modelName.toLowerCase();
259
- if (normalized.includes("5.3") && normalized.includes("spark")) return "GPT-5.3 Codex Spark";
260
- if (normalized.includes("5.3") && normalized.includes("codex")) return "GPT-5.3 Codex";
261
- if (normalized.includes("5.3")) return "GPT-5.3";
262
- if (normalized.includes("5.2") && normalized.includes("codex")) return "GPT-5.2 Codex";
263
- if (normalized.includes("5.2")) return "GPT-5.2";
264
- if (normalized.includes("5.1") && normalized.includes("codex") && normalized.includes("max")) return "GPT-5.1 Codex Max";
265
- if (normalized.includes("5.1") && normalized.includes("codex") && normalized.includes("mini")) return "GPT-5.1 Codex Mini";
266
- if (normalized.includes("5.1") && normalized.includes("max")) return "GPT-5.1 Max";
267
- if (normalized.includes("5.1") && normalized.includes("mini")) return "GPT-5.1 Mini";
268
- if (normalized.includes("5.1") && normalized.includes("codex")) return "GPT-5.1 Codex";
269
- if (normalized.includes("5.1")) return "GPT-5.1";
270
- if (normalized.includes("nano")) return "GPT-5 Nano";
271
- if (normalized.includes("5") && normalized.includes("mini")) return "GPT-5 Mini";
272
- if (normalized.includes("5") && normalized.includes("codex")) return "GPT-5 Codex";
273
- if (normalized.includes("gpt-5")) return "GPT-5";
274
- if (normalized.includes("4o-mini")) return "GPT-4o Mini";
275
- if (normalized.includes("4o")) return "GPT-4o";
276
- return modelName;
277
- }
278
- function calculateCodexCost(modelName, inputTokens, outputTokens, cachedInputTokens) {
279
- const pricing = getCodexModelPricing(modelName);
280
- const regularInputTokens = inputTokens - cachedInputTokens;
281
- const inputCost = regularInputTokens / 1e6 * pricing.input;
282
- const cachedCost = cachedInputTokens / 1e6 * pricing.cachedInput;
283
- const outputCost = outputTokens / 1e6 * pricing.output;
284
- return inputCost + cachedCost + outputCost;
775
+ return modelName;
285
776
  }
286
777
 
287
778
  // src/usage/loader.ts
@@ -404,6 +895,7 @@ async function parseClaudeJsonl(projectFilter) {
404
895
  cacheWriteTokens,
405
896
  cacheReadTokens,
406
897
  cost,
898
+ messageCount: 1,
407
899
  source: "claude",
408
900
  sessionId,
409
901
  timestamp
@@ -460,7 +952,10 @@ async function parseCodexJsonl() {
460
952
  cacheWriteTokens: 0,
461
953
  cacheReadTokens: cachedInputTokens,
462
954
  cost,
463
- source: "codex"
955
+ messageCount: 1,
956
+ source: "codex",
957
+ sessionId: basename(filePath, ".jsonl"),
958
+ timestamp
464
959
  });
465
960
  }
466
961
  } catch {
@@ -479,36 +974,22 @@ function filterByDateRange(entries, since, until) {
479
974
  });
480
975
  }
481
976
  function sortModelsByTier(models) {
482
- const tierPriority = {
977
+ const claudeTierPriority = {
483
978
  "Opus 4.6": 101,
484
979
  "Opus 4.5": 100,
485
980
  "Opus 4.1": 99,
486
981
  "Opus": 98,
982
+ "Sonnet 4.6": 91,
487
983
  "Sonnet 4.5": 90,
488
984
  "Sonnet 3.5": 89,
489
985
  "Sonnet": 88,
490
986
  "Haiku 4.5": 80,
491
987
  "Haiku 3.5": 79,
492
- "Haiku": 78,
493
- "GPT-5.3 Codex Spark": 76,
494
- "GPT-5.3 Codex": 75,
495
- "GPT-5.3": 74,
496
- "GPT-5.2 Codex": 73,
497
- "GPT-5.2": 72,
498
- "GPT-5.1 Codex Max": 71,
499
- "GPT-5.1 Max": 70,
500
- "GPT-5.1 Codex Mini": 69,
501
- "GPT-5.1 Mini": 68,
502
- "GPT-5.1 Codex": 67,
503
- "GPT-5.1": 66,
504
- "GPT-5": 64,
505
- "GPT-5 Mini": 63,
506
- "GPT-4o": 60,
507
- "GPT-4o Mini": 59
988
+ "Haiku": 78
508
989
  };
509
990
  return models.sort((a, b) => {
510
- const priorityA = tierPriority[a] ?? 0;
511
- const priorityB = tierPriority[b] ?? 0;
991
+ const priorityA = claudeTierPriority[a] ?? getCodexModelSortPriority(a);
992
+ const priorityB = claudeTierPriority[b] ?? getCodexModelSortPriority(b);
512
993
  return priorityB - priorityA;
513
994
  });
514
995
  }
@@ -675,6 +1156,76 @@ function computeModelBreakdown(entries) {
675
1156
  percentage: totalTokens > 0 ? Math.round(data.tokens / totalTokens * 100) : 0
676
1157
  })).sort((a, b) => b.tokens - a.tokens);
677
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
+ }
678
1229
  async function loadUsageStats(options) {
679
1230
  const { aggregation, since, until, codexOnly, combined, projectFilter } = options;
680
1231
  let entries = [];
@@ -730,6 +1281,28 @@ async function loadUsageStats(options) {
730
1281
  modelBreakdown
731
1282
  };
732
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
+ }
1301
+ let source = "claude";
1302
+ if (codexOnly) source = "codex";
1303
+ else if (combined) source = "combined";
1304
+ return computeActivityStats(entries, source);
1305
+ }
733
1306
 
734
1307
  // src/usage/table.ts
735
1308
  var colors = {
@@ -1601,200 +2174,6 @@ function combineWrappedStats(claude, codex) {
1601
2174
  };
1602
2175
  }
1603
2176
 
1604
- // src/url-encoder.ts
1605
- function formatCompactNumber(num) {
1606
- if (num >= 1e9) {
1607
- return `${(num / 1e9).toFixed(1)}B`;
1608
- }
1609
- if (num >= 1e6) {
1610
- return `${(num / 1e6).toFixed(1)}M`;
1611
- }
1612
- if (num >= 1e3) {
1613
- return `${(num / 1e3).toFixed(1)}K`;
1614
- }
1615
- return num.toString();
1616
- }
1617
- var dayToNumber = {
1618
- Sunday: 0,
1619
- Monday: 1,
1620
- Tuesday: 2,
1621
- Wednesday: 3,
1622
- Thursday: 4,
1623
- Friday: 5,
1624
- Saturday: 6
1625
- };
1626
- function encodeStatsToUrl(stats, baseUrl = "https://vibestats.wolfai.dev") {
1627
- const params = new URLSearchParams();
1628
- params.set("s", stats.sessions.toString());
1629
- params.set("t", formatCompactNumber(stats.totalTokens));
1630
- params.set("c", stats.totalCost.toFixed(2));
1631
- params.set("d", stats.daysActive.toString());
1632
- params.set("ls", stats.longestStreak.toString());
1633
- params.set("cs", stats.currentStreak.toString());
1634
- params.set("ph", stats.peakHour.toString());
1635
- params.set("pd", (dayToNumber[stats.peakDay] ?? 0).toString());
1636
- params.set("fm", getModelAbbrevFromDisplayName(stats.favoriteModel));
1637
- const mbParts = stats.modelBreakdown.slice(0, 3).map((m) => {
1638
- const abbr = getModelAbbrevFromDisplayName(m.model);
1639
- return `${abbr}:${m.percentage}`;
1640
- });
1641
- params.set("mb", mbParts.join(","));
1642
- if (stats.topTools && stats.topTools.length > 0) {
1643
- params.set("tt", stats.topTools.slice(0, 5).join(","));
1644
- }
1645
- if (stats.developerStyle) {
1646
- const styleMap = {
1647
- reader: "r",
1648
- writer: "w",
1649
- executor: "e",
1650
- balanced: "b"
1651
- };
1652
- params.set("st", styleMap[stats.developerStyle] || "b");
1653
- }
1654
- if (stats.topProject) {
1655
- params.set("tp", stats.topProject);
1656
- }
1657
- if (stats.projectCount) {
1658
- params.set("pc", stats.projectCount.toString());
1659
- }
1660
- params.set("wg", formatCompactNumber(stats.wordsGenerated));
1661
- const firstDate = new Date(stats.firstSessionDate);
1662
- params.set("fad", firstDate.toISOString().split("T")[0].replace(/-/g, ""));
1663
- if (stats.source && stats.source !== "claude") {
1664
- params.set("src", stats.source);
1665
- }
1666
- return `${baseUrl}/wrapped/?${params.toString()}`;
1667
- }
1668
- function getModelAbbrevFromDisplayName(displayName) {
1669
- const map = {
1670
- // Claude models
1671
- "Opus 4.5": "o45",
1672
- "Opus 4.1": "o41",
1673
- Opus: "opus",
1674
- "Sonnet 4.5": "s45",
1675
- "Sonnet 3.5": "s35",
1676
- Sonnet: "sonnet",
1677
- "Haiku 4.5": "h45",
1678
- "Haiku 3.5": "h35",
1679
- Haiku: "haiku",
1680
- // Codex/OpenAI models
1681
- "GPT-5.3 Codex Spark": "g53s",
1682
- "GPT-5.3 Codex": "g53c",
1683
- "GPT-5.3": "g53",
1684
- "GPT-5.2 Codex": "g52c",
1685
- "GPT-5.2": "g52",
1686
- "GPT-5.1 Codex Max": "g51cm",
1687
- "GPT-5.1 Max": "g51m",
1688
- "GPT-5.1 Codex Mini": "g51cn",
1689
- "GPT-5.1 Mini": "g51n",
1690
- "GPT-5.1 Codex": "g51c",
1691
- "GPT-5.1": "g51",
1692
- "GPT-5": "g5",
1693
- "GPT-5 Mini": "g5n",
1694
- "GPT-4o": "g4o",
1695
- "GPT-4o Mini": "g4om"
1696
- };
1697
- if (map[displayName]) return map[displayName];
1698
- const normalized = displayName.toLowerCase();
1699
- if (normalized.includes("5.3") && normalized.includes("spark")) return "g53s";
1700
- if (normalized.includes("5.3") && normalized.includes("codex")) return "g53c";
1701
- if (normalized.includes("5.2")) return "g52";
1702
- if (normalized.includes("5.1") && normalized.includes("codex") && normalized.includes("max")) return "g51cm";
1703
- if (normalized.includes("5.1") && normalized.includes("max")) return "g51m";
1704
- if (normalized.includes("5.1") && normalized.includes("codex") && normalized.includes("mini")) return "g51cn";
1705
- if (normalized.includes("5.1") && normalized.includes("codex")) return "g51c";
1706
- if (normalized.includes("5.1") && normalized.includes("mini")) return "g51n";
1707
- if (normalized.includes("5.1")) return "g51";
1708
- if (normalized.includes("5") && normalized.includes("mini")) return "g5n";
1709
- if (normalized.includes("gpt-5") || normalized.includes("gpt5")) return "g5";
1710
- if (normalized.includes("4o") && normalized.includes("mini")) return "g4om";
1711
- if (normalized.includes("4o")) return "g4o";
1712
- if (normalized.includes("opus") && normalized.includes("4.5")) return "o45";
1713
- if (normalized.includes("opus") && normalized.includes("4.1")) return "o41";
1714
- if (normalized.includes("opus")) return "opus";
1715
- if (normalized.includes("sonnet") && normalized.includes("4.5")) return "s45";
1716
- if (normalized.includes("sonnet") && normalized.includes("3.5")) return "s35";
1717
- if (normalized.includes("sonnet")) return "sonnet";
1718
- if (normalized.includes("haiku") && normalized.includes("4.5")) return "h45";
1719
- if (normalized.includes("haiku") && normalized.includes("3.5")) return "h35";
1720
- if (normalized.includes("haiku")) return "haiku";
1721
- return displayName.slice(0, 5).toLowerCase();
1722
- }
1723
- function aggregateRowsToMonthly(rows) {
1724
- const monthMap = /* @__PURE__ */ new Map();
1725
- for (const row of rows) {
1726
- const month = row.key.slice(0, 7);
1727
- const existing = monthMap.get(month);
1728
- if (existing) {
1729
- existing.inputTokens += row.inputTokens;
1730
- existing.outputTokens += row.outputTokens;
1731
- existing.cacheWriteTokens += row.cacheWriteTokens;
1732
- existing.cacheReadTokens += row.cacheReadTokens;
1733
- existing.totalTokens += row.totalTokens;
1734
- existing.cost += row.cost;
1735
- } else {
1736
- monthMap.set(month, { ...row, key: month });
1737
- }
1738
- }
1739
- return Array.from(monthMap.values()).sort((a, b) => a.key.localeCompare(b.key));
1740
- }
1741
- function encodeUsageToUrl(stats, baseUrl = "https://vibestats.wolfai.dev") {
1742
- const params = new URLSearchParams();
1743
- if (stats.source !== "claude") {
1744
- params.set("src", stats.source);
1745
- }
1746
- const formatDateCompact = (d) => d.replace(/-/g, "");
1747
- const startMs = new Date(stats.dateRange.start).getTime();
1748
- const endMs = new Date(stats.dateRange.end).getTime();
1749
- const daySpan = Math.ceil((endMs - startMs) / (1e3 * 60 * 60 * 24));
1750
- const useMonthly = stats.aggregation === "daily" && daySpan > 31;
1751
- const aggMap = { daily: "d", monthly: "m", model: "mo", total: "t" };
1752
- const effectiveAgg = useMonthly ? "monthly" : stats.aggregation;
1753
- params.set("agg", aggMap[effectiveAgg] || "d");
1754
- let rowsToEncode;
1755
- if (useMonthly) {
1756
- rowsToEncode = aggregateRowsToMonthly(stats.rows);
1757
- } else {
1758
- rowsToEncode = stats.rows.slice(-31);
1759
- }
1760
- const startDate = useMonthly ? stats.dateRange.start : rowsToEncode[0]?.key || stats.dateRange.start;
1761
- const endDate = useMonthly ? stats.dateRange.end : rowsToEncode[rowsToEncode.length - 1]?.key || stats.dateRange.end;
1762
- params.set("dr", `${formatDateCompact(startDate)}-${formatDateCompact(endDate)}`);
1763
- const rows = rowsToEncode.map((row) => {
1764
- let key = row.key;
1765
- if (effectiveAgg === "daily" && row.key.length === 10) {
1766
- key = row.key.slice(5).replace("-", "");
1767
- }
1768
- return [
1769
- key,
1770
- formatCompactNumber(row.inputTokens),
1771
- formatCompactNumber(row.outputTokens),
1772
- formatCompactNumber(row.cacheWriteTokens),
1773
- formatCompactNumber(row.cacheReadTokens),
1774
- formatCompactNumber(row.totalTokens),
1775
- row.cost.toFixed(2)
1776
- ].join(":");
1777
- });
1778
- params.set("rows", rows.join("|"));
1779
- const t = stats.totals;
1780
- params.set("tot", [
1781
- formatCompactNumber(t.inputTokens),
1782
- formatCompactNumber(t.outputTokens),
1783
- formatCompactNumber(t.cacheWriteTokens),
1784
- formatCompactNumber(t.cacheReadTokens),
1785
- formatCompactNumber(t.totalTokens),
1786
- t.cost.toFixed(2)
1787
- ].join(":"));
1788
- if (stats.modelBreakdown.length > 0) {
1789
- const mb = stats.modelBreakdown.slice(0, 5).map((m) => {
1790
- const abbr = getModelAbbrevFromDisplayName(m.model);
1791
- return `${abbr}:${m.percentage}:${m.cost.toFixed(2)}`;
1792
- });
1793
- params.set("mb", mb.join(","));
1794
- }
1795
- return `${baseUrl}?${params.toString()}`;
1796
- }
1797
-
1798
2177
  // src/display.ts
1799
2178
  var colors2 = {
1800
2179
  reset: "\x1B[0m",
@@ -1893,27 +2272,72 @@ function formatHour(hour) {
1893
2272
  if (hour === 12) return "12pm";
1894
2273
  return `${hour - 12}pm`;
1895
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
+ }
1896
2309
 
1897
- // src/shortener.ts
1898
- async function createShortlink(params, baseUrl) {
1899
- 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) {
1900
2318
  try {
1901
- const apiUrl = new URL("/api/shorten", baseUrl);
2319
+ const apiUrl = new URL("/vibestats/shares", baseUrl);
1902
2320
  const response = await fetch(apiUrl.toString(), {
1903
2321
  method: "POST",
1904
2322
  headers: {
1905
2323
  "Content-Type": "application/json"
1906
2324
  },
1907
- body: JSON.stringify({ params: queryString })
2325
+ body: JSON.stringify({
2326
+ artifact,
2327
+ legacyQuery: extractLegacyQuery(legacyUrl)
2328
+ })
1908
2329
  });
1909
2330
  if (!response.ok) {
1910
2331
  return null;
1911
2332
  }
1912
2333
  const data = await response.json();
1913
- if (data.slug) {
1914
- return `${baseUrl}/s/${data.slug}`;
1915
- }
1916
- 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
+ };
1917
2341
  } catch {
1918
2342
  return null;
1919
2343
  }
@@ -2067,6 +2491,65 @@ function parseLastDaysFlag(args) {
2067
2491
  shorthandDays.sort((a, b) => a - b);
2068
2492
  return shorthandDays[0];
2069
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
+ }
2070
2553
  var main = defineCommand({
2071
2554
  meta: {
2072
2555
  name: "vibestats",
@@ -2081,6 +2564,11 @@ var main = defineCommand({
2081
2564
  description: "Show annual wrapped summary instead of usage stats",
2082
2565
  default: false
2083
2566
  },
2567
+ activity: {
2568
+ type: "boolean",
2569
+ description: "Show the GitHub-style activity graph summary",
2570
+ default: false
2571
+ },
2084
2572
  // Data source
2085
2573
  codex: {
2086
2574
  type: "boolean",
@@ -2147,6 +2635,14 @@ var main = defineCommand({
2147
2635
  description: "Use compact table format (hide cache columns)",
2148
2636
  default: false
2149
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
+ },
2150
2646
  // Share option for usage mode
2151
2647
  share: {
2152
2648
  type: "boolean",
@@ -2199,7 +2695,9 @@ var main = defineCommand({
2199
2695
  console.log(JSON.stringify(config, null, 2));
2200
2696
  return;
2201
2697
  }
2202
- if (args.wrapped) {
2698
+ if (args.activity) {
2699
+ await runActivity(args, config);
2700
+ } else if (args.wrapped) {
2203
2701
  await runWrapped(args, config);
2204
2702
  } else {
2205
2703
  await runUsage(args, config);
@@ -2270,10 +2768,13 @@ async function runUsage(args, config) {
2270
2768
  let shareUrl = null;
2271
2769
  if (args.share) {
2272
2770
  const fullUrl = encodeUsageToUrl(stats, baseUrl);
2273
- if (!args["no-short"]) {
2274
- shareUrl = await createShortlink(fullUrl, baseUrl);
2275
- }
2276
- 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;
2277
2778
  }
2278
2779
  if (args.quiet && args.share && shareUrl) {
2279
2780
  console.log(shareUrl);
@@ -2306,9 +2807,14 @@ async function runUsage(args, config) {
2306
2807
  }
2307
2808
  async function runWrapped(args, config) {
2308
2809
  const options = resolveOptions(args, config);
2810
+ const metric = parseActivityMetric(args.metric);
2811
+ const days = parseActivityDays(args.days);
2309
2812
  const spinner = createSpinner("Preparing wrapped...");
2310
- const data = await spinner.whilePromise(
2311
- 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
+ ])
2312
2818
  );
2313
2819
  validateData(data, { codexOnly: args.codex, combined: args.combined });
2314
2820
  let claudeStats = null;
@@ -2327,23 +2833,89 @@ async function runWrapped(args, config) {
2327
2833
  } else {
2328
2834
  stats = claudeStats;
2329
2835
  }
2330
- const url = encodeStatsToUrl(stats, options.baseUrl);
2331
- let shortUrl = null;
2332
- if (!args["no-short"]) {
2333
- shortUrl = await createShortlink(url, options.baseUrl);
2836
+ if (activityStats) {
2837
+ stats.activity = buildActivityGraph(activityStats, metric, days);
2334
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
+ );
2335
2846
  if (options.outputFormat === "json") {
2336
- 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
+ );
2337
2859
  } else if (options.outputFormat === "quiet") {
2338
- console.log(shortUrl || url);
2860
+ console.log(published.shareUrl);
2339
2861
  } else {
2340
- displayWrappedStats(stats, url, {
2862
+ displayWrappedStats(stats, published.canonicalUrl, {
2341
2863
  theme: options.theme,
2342
2864
  hideCost: options.hideCost,
2343
- shortUrl
2865
+ shortUrl: published.shareUrl === published.canonicalUrl ? null : published.shareUrl,
2866
+ imageUrl: published.imageUrl
2344
2867
  });
2345
2868
  }
2346
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
+ }
2347
2919
  function formatNumber2(n) {
2348
2920
  if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
2349
2921
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;