vibestats 1.0.21 → 1.0.23

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 +205 -96
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { defineCommand, runMain } from "citty";
5
5
 
6
6
  // src/usage/loader.ts
7
- import { readFileSync, existsSync, readdirSync, statSync, realpathSync } from "fs";
7
+ import { promises as fs } from "fs";
8
8
  import { homedir } from "os";
9
9
  import { join } from "path";
10
10
 
@@ -232,51 +232,73 @@ function calculateCodexCost(modelName, inputTokens, outputTokens, cachedInputTok
232
232
  }
233
233
 
234
234
  // src/usage/loader.ts
235
+ var MAX_RECURSION_DEPTH = 10;
236
+ function toLocalDateString(isoTimestamp) {
237
+ const date = new Date(isoTimestamp);
238
+ const year = date.getFullYear();
239
+ const month = String(date.getMonth() + 1).padStart(2, "0");
240
+ const day = String(date.getDate()).padStart(2, "0");
241
+ return `${year}-${month}-${day}`;
242
+ }
235
243
  function getClaudeDir() {
236
244
  return process.env.CLAUDE_HOME || join(homedir(), ".claude");
237
245
  }
238
246
  function getCodexDir() {
239
247
  return join(homedir(), ".codex");
240
248
  }
241
- function findJsonlFiles(dir, visited = /* @__PURE__ */ new Set(), depth = 0, result = []) {
242
- if (!existsSync(dir)) return result;
243
- if (depth > 10) return result;
244
- let realPath;
249
+ async function pathExists(path) {
250
+ try {
251
+ await fs.access(path);
252
+ return true;
253
+ } catch {
254
+ return false;
255
+ }
256
+ }
257
+ async function safeRealpath(path) {
245
258
  try {
246
- realPath = realpathSync(dir);
259
+ return await fs.realpath(path);
247
260
  } catch {
248
- realPath = dir;
261
+ return path;
249
262
  }
263
+ }
264
+ async function findJsonlFiles(dir, visited = /* @__PURE__ */ new Set(), depth = 0, result = []) {
265
+ if (!await pathExists(dir)) return result;
266
+ if (depth > MAX_RECURSION_DEPTH) return result;
267
+ const realPath = await safeRealpath(dir);
250
268
  if (visited.has(realPath)) return result;
251
269
  visited.add(realPath);
270
+ let entries;
252
271
  try {
253
- const entries = readdirSync(dir);
254
- for (const entry of entries) {
255
- const fullPath = join(dir, entry);
256
- try {
257
- const stat = statSync(fullPath);
258
- if (stat.isDirectory()) {
259
- findJsonlFiles(fullPath, visited, depth + 1, result);
260
- } else if (entry.endsWith(".jsonl")) {
261
- result.push(fullPath);
262
- }
263
- } catch {
264
- }
265
- }
272
+ entries = await fs.readdir(dir);
266
273
  } catch {
274
+ return result;
275
+ }
276
+ for (const entry of entries) {
277
+ const fullPath = join(dir, entry);
278
+ let stat;
279
+ try {
280
+ stat = await fs.stat(fullPath);
281
+ } catch {
282
+ continue;
283
+ }
284
+ if (stat.isDirectory()) {
285
+ await findJsonlFiles(fullPath, visited, depth + 1, result);
286
+ } else if (entry.endsWith(".jsonl")) {
287
+ result.push(fullPath);
288
+ }
267
289
  }
268
290
  return result;
269
291
  }
270
- function parseClaudeJsonl() {
292
+ async function parseClaudeJsonl() {
271
293
  const entries = [];
272
294
  const seenMessageIds = /* @__PURE__ */ new Set();
273
295
  const claudeDir = getClaudeDir();
274
296
  const projectsDir = join(claudeDir, "projects");
275
- if (!existsSync(projectsDir)) return entries;
276
- const jsonlFiles = findJsonlFiles(projectsDir);
297
+ if (!await pathExists(projectsDir)) return entries;
298
+ const jsonlFiles = await findJsonlFiles(projectsDir);
277
299
  for (const filePath of jsonlFiles) {
278
300
  try {
279
- const content = readFileSync(filePath, "utf-8");
301
+ const content = await fs.readFile(filePath, "utf-8");
280
302
  const lines = content.split("\n");
281
303
  for (const line of lines) {
282
304
  if (!line.trim()) continue;
@@ -292,7 +314,7 @@ function parseClaudeJsonl() {
292
314
  const model = entry.message.model || "unknown";
293
315
  const timestamp = entry.timestamp;
294
316
  if (!timestamp) continue;
295
- const date = timestamp.split("T")[0];
317
+ const date = toLocalDateString(timestamp);
296
318
  const pricing = getModelPricing(model);
297
319
  const inputTokens = usage.input_tokens || 0;
298
320
  const outputTokens = usage.output_tokens || 0;
@@ -317,19 +339,20 @@ function parseClaudeJsonl() {
317
339
  }
318
340
  return entries;
319
341
  }
320
- function parseCodexJsonl() {
342
+ async function parseCodexJsonl() {
321
343
  const entries = [];
322
344
  const codexDir = getCodexDir();
323
345
  const sessionsDir = join(codexDir, "sessions");
324
346
  const archivedDir = join(codexDir, "archived_sessions");
325
- const jsonlFiles = [
326
- ...findJsonlFiles(sessionsDir),
327
- ...findJsonlFiles(archivedDir)
328
- ];
347
+ const [sessionFiles, archivedFiles] = await Promise.all([
348
+ findJsonlFiles(sessionsDir),
349
+ findJsonlFiles(archivedDir)
350
+ ]);
351
+ const jsonlFiles = [...sessionFiles, ...archivedFiles];
329
352
  if (jsonlFiles.length === 0) return entries;
330
353
  for (const filePath of jsonlFiles) {
331
354
  try {
332
- const content = readFileSync(filePath, "utf-8");
355
+ const content = await fs.readFile(filePath, "utf-8");
333
356
  const lines = content.split("\n");
334
357
  let currentModel = "gpt-5";
335
358
  for (const line of lines) {
@@ -346,7 +369,7 @@ function parseCodexJsonl() {
346
369
  const usage = info.last_token_usage;
347
370
  const timestamp = entry.timestamp;
348
371
  if (!timestamp) continue;
349
- const date = timestamp.split("T")[0];
372
+ const date = toLocalDateString(timestamp);
350
373
  const pricing = getCodexModelPricing(currentModel);
351
374
  const inputTokens = usage.input_tokens || 0;
352
375
  const outputTokens = usage.output_tokens || 0;
@@ -356,7 +379,6 @@ function parseCodexJsonl() {
356
379
  date,
357
380
  model: getCodexModelDisplayName(currentModel),
358
381
  inputTokens: inputTokens - cachedInputTokens,
359
- // OpenAI input_tokens includes cached, so subtract
360
382
  outputTokens,
361
383
  cacheWriteTokens: 0,
362
384
  cacheReadTokens: cachedInputTokens,
@@ -381,7 +403,6 @@ function filterByDateRange(entries, since, until) {
381
403
  }
382
404
  function sortModelsByTier(models) {
383
405
  const tierPriority = {
384
- // Claude models (higher = first)
385
406
  "Opus 4.5": 100,
386
407
  "Opus 4.1": 99,
387
408
  "Opus": 98,
@@ -391,7 +412,6 @@ function sortModelsByTier(models) {
391
412
  "Haiku 4.5": 80,
392
413
  "Haiku 3.5": 79,
393
414
  "Haiku": 78,
394
- // OpenAI/Codex models
395
415
  "GPT-5.2": 70,
396
416
  "GPT-5.1 Max": 69,
397
417
  "GPT-5.1": 68,
@@ -526,14 +546,14 @@ function computeModelBreakdown(entries) {
526
546
  percentage: totalTokens > 0 ? Math.round(data.tokens / totalTokens * 100) : 0
527
547
  })).sort((a, b) => b.tokens - a.tokens);
528
548
  }
529
- function loadUsageStats(options) {
549
+ async function loadUsageStats(options) {
530
550
  const { aggregation, since, until, codexOnly, combined } = options;
531
551
  let entries = [];
532
552
  if (!codexOnly) {
533
- entries = entries.concat(parseClaudeJsonl());
553
+ entries = entries.concat(await parseClaudeJsonl());
534
554
  }
535
555
  if (codexOnly || combined) {
536
- entries = entries.concat(parseCodexJsonl());
556
+ entries = entries.concat(await parseCodexJsonl());
537
557
  }
538
558
  if (entries.length === 0) {
539
559
  return null;
@@ -733,27 +753,46 @@ function displayTotalOnly(stats, options = {}) {
733
753
  }
734
754
 
735
755
  // src/claude-jsonl-loader.ts
736
- import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
756
+ import { promises as fs2 } from "fs";
737
757
  import { homedir as homedir2 } from "os";
738
758
  import { join as join2 } from "path";
739
759
  function getClaudeDir2() {
740
760
  return process.env.CLAUDE_HOME || join2(homedir2(), ".claude");
741
761
  }
742
- function claudeJsonlDataExists() {
743
- const claudeDir = getClaudeDir2();
744
- const projectsDir = join2(claudeDir, "projects");
745
- return existsSync2(projectsDir);
762
+ function toLocalDateString2(isoTimestamp) {
763
+ const date = new Date(isoTimestamp);
764
+ const year = date.getFullYear();
765
+ const month = String(date.getMonth() + 1).padStart(2, "0");
766
+ const day = String(date.getDate()).padStart(2, "0");
767
+ return `${year}-${month}-${day}`;
768
+ }
769
+ async function pathExists2(path) {
770
+ try {
771
+ await fs2.access(path);
772
+ return true;
773
+ } catch {
774
+ return false;
775
+ }
776
+ }
777
+ async function claudeJsonlDataExists() {
778
+ const projectsDir = join2(getClaudeDir2(), "projects");
779
+ return pathExists2(projectsDir);
746
780
  }
747
- function findJsonlFiles2(dir) {
781
+ async function findJsonlFiles2(dir) {
748
782
  const files = [];
749
- if (!existsSync2(dir)) return files;
750
- const entries = readdirSync2(dir);
783
+ if (!await pathExists2(dir)) return files;
784
+ let entries;
785
+ try {
786
+ entries = await fs2.readdir(dir);
787
+ } catch {
788
+ return files;
789
+ }
751
790
  for (const entry of entries) {
752
791
  const fullPath = join2(dir, entry);
753
792
  try {
754
- const stat = statSync2(fullPath);
793
+ const stat = await fs2.stat(fullPath);
755
794
  if (stat.isDirectory()) {
756
- files.push(...findJsonlFiles2(fullPath));
795
+ files.push(...await findJsonlFiles2(fullPath));
757
796
  } else if (entry.endsWith(".jsonl")) {
758
797
  files.push(fullPath);
759
798
  }
@@ -762,13 +801,13 @@ function findJsonlFiles2(dir) {
762
801
  }
763
802
  return files;
764
803
  }
765
- function loadClaudeStatsFromJsonl() {
804
+ async function loadClaudeStatsFromJsonl() {
766
805
  const claudeDir = getClaudeDir2();
767
806
  const projectsDir = join2(claudeDir, "projects");
768
- if (!claudeJsonlDataExists()) {
807
+ if (!await claudeJsonlDataExists()) {
769
808
  return null;
770
809
  }
771
- const jsonlFiles = findJsonlFiles2(projectsDir);
810
+ const jsonlFiles = await findJsonlFiles2(projectsDir);
772
811
  if (jsonlFiles.length === 0) {
773
812
  return null;
774
813
  }
@@ -780,7 +819,7 @@ function loadClaudeStatsFromJsonl() {
780
819
  const messageIds = /* @__PURE__ */ new Set();
781
820
  for (const filePath of jsonlFiles) {
782
821
  try {
783
- const content = readFileSync2(filePath, "utf-8");
822
+ const content = await fs2.readFile(filePath, "utf-8");
784
823
  const lines = content.split("\n");
785
824
  for (const line of lines) {
786
825
  if (!line.trim()) continue;
@@ -808,7 +847,7 @@ function loadClaudeStatsFromJsonl() {
808
847
  messageIds.add(messageId);
809
848
  }
810
849
  if (timestamp) {
811
- const date = timestamp.split("T")[0];
850
+ const date = toLocalDateString2(timestamp);
812
851
  const hour = new Date(timestamp).getHours().toString();
813
852
  if (!dailyMap.has(date)) {
814
853
  dailyMap.set(date, { messageCount: 0, sessionCount: 0, toolCallCount: 0 });
@@ -839,7 +878,6 @@ function loadClaudeStatsFromJsonl() {
839
878
  cacheCreationInputTokens: usage.cacheCreationInputTokens,
840
879
  webSearchRequests: 0,
841
880
  costUSD: 0,
842
- // Will be calculated by metrics.ts
843
881
  contextWindow: 2e5
844
882
  };
845
883
  }
@@ -847,16 +885,15 @@ function loadClaudeStatsFromJsonl() {
847
885
  date,
848
886
  messageCount: data.messageCount,
849
887
  sessionCount: 1,
850
- // Each day counts as at least 1 session for activity
851
888
  toolCallCount: data.toolCallCount
852
889
  })).sort((a, b) => a.date.localeCompare(b.date));
853
890
  const sessionsByDate = /* @__PURE__ */ new Map();
891
+ void sessionsByDate;
854
892
  return {
855
893
  version: 1,
856
- lastComputedDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
894
+ lastComputedDate: toLocalDateString2((/* @__PURE__ */ new Date()).toISOString()),
857
895
  dailyActivity,
858
896
  dailyModelTokens: [],
859
- // Not needed for wrapped stats
860
897
  modelUsage: statsCacheModelUsage,
861
898
  totalSessions: jsonlFiles.length,
862
899
  totalMessages,
@@ -872,37 +909,62 @@ function loadClaudeStatsFromJsonl() {
872
909
  }
873
910
 
874
911
  // src/codex-loader.ts
875
- import { readFileSync as readFileSync3, existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
912
+ import { promises as fs3 } from "fs";
876
913
  import { homedir as homedir3 } from "os";
877
914
  import { join as join3 } from "path";
878
915
  function getCodexDir2() {
879
916
  return process.env.CODEX_HOME || join3(homedir3(), ".codex");
880
917
  }
881
- function codexDataExists() {
918
+ function toLocalDateString3(isoTimestamp) {
919
+ const date = new Date(isoTimestamp);
920
+ const year = date.getFullYear();
921
+ const month = String(date.getMonth() + 1).padStart(2, "0");
922
+ const day = String(date.getDate()).padStart(2, "0");
923
+ return `${year}-${month}-${day}`;
924
+ }
925
+ async function pathExists3(path) {
926
+ try {
927
+ await fs3.access(path);
928
+ return true;
929
+ } catch {
930
+ return false;
931
+ }
932
+ }
933
+ async function codexDataExists() {
882
934
  const codexDir = getCodexDir2();
883
- if (!existsSync3(codexDir)) return false;
935
+ if (!await pathExists3(codexDir)) return false;
884
936
  const sessionsDir = join3(codexDir, "sessions");
885
937
  const archivedDir = join3(codexDir, "archived_sessions");
886
- return existsSync3(sessionsDir) || existsSync3(archivedDir);
938
+ return await pathExists3(sessionsDir) || await pathExists3(archivedDir);
887
939
  }
888
- function findJsonlFiles3(dir) {
940
+ async function findJsonlFiles3(dir) {
889
941
  const files = [];
890
- if (!existsSync3(dir)) return files;
891
- const entries = readdirSync3(dir);
942
+ if (!await pathExists3(dir)) return files;
943
+ let entries;
944
+ try {
945
+ entries = await fs3.readdir(dir);
946
+ } catch {
947
+ return files;
948
+ }
892
949
  for (const entry of entries) {
893
950
  const fullPath = join3(dir, entry);
894
- const stat = statSync3(fullPath);
951
+ let stat;
952
+ try {
953
+ stat = await fs3.stat(fullPath);
954
+ } catch {
955
+ continue;
956
+ }
895
957
  if (stat.isDirectory()) {
896
- files.push(...findJsonlFiles3(fullPath));
958
+ files.push(...await findJsonlFiles3(fullPath));
897
959
  } else if (entry.endsWith(".jsonl")) {
898
960
  files.push(fullPath);
899
961
  }
900
962
  }
901
963
  return files;
902
964
  }
903
- function parseSessionFile(filePath) {
965
+ async function parseSessionFile(filePath) {
904
966
  try {
905
- const content = readFileSync3(filePath, "utf-8");
967
+ const content = await fs3.readFile(filePath, "utf-8");
906
968
  const lines = content.trim().split("\n");
907
969
  let sessionMeta = null;
908
970
  let currentModel = "gpt-5";
@@ -969,29 +1031,29 @@ function parseSessionFile(filePath) {
969
1031
  model: primaryModel,
970
1032
  tokenUsage: summedUsage,
971
1033
  perModelUsage
972
- // New: per-model token breakdown
973
1034
  };
974
1035
  } catch {
975
1036
  return null;
976
1037
  }
977
1038
  }
978
- function loadCodexStats() {
1039
+ async function loadCodexStats() {
979
1040
  const codexDir = getCodexDir2();
980
- if (!codexDataExists()) {
1041
+ if (!await codexDataExists()) {
981
1042
  return null;
982
1043
  }
983
1044
  const sessionsDir = join3(codexDir, "sessions");
984
1045
  const archivedDir = join3(codexDir, "archived_sessions");
985
- const jsonlFiles = [
986
- ...findJsonlFiles3(sessionsDir),
987
- ...findJsonlFiles3(archivedDir)
988
- ];
1046
+ const [sessionFiles, archivedFiles] = await Promise.all([
1047
+ findJsonlFiles3(sessionsDir),
1048
+ findJsonlFiles3(archivedDir)
1049
+ ]);
1050
+ const jsonlFiles = [...sessionFiles, ...archivedFiles];
989
1051
  if (jsonlFiles.length === 0) {
990
1052
  return null;
991
1053
  }
992
1054
  const sessions = [];
993
1055
  for (const file of jsonlFiles) {
994
- const session = parseSessionFile(file);
1056
+ const session = await parseSessionFile(file);
995
1057
  if (session) {
996
1058
  sessions.push(session);
997
1059
  }
@@ -1034,7 +1096,7 @@ function loadCodexStats() {
1034
1096
  }
1035
1097
  const dailyMap = /* @__PURE__ */ new Map();
1036
1098
  for (const session of sessions) {
1037
- const date = session.timestamp.split("T")[0];
1099
+ const date = toLocalDateString3(session.timestamp);
1038
1100
  dailyMap.set(date, (dailyMap.get(date) || 0) + 1);
1039
1101
  }
1040
1102
  const dailyActivity = Array.from(dailyMap.entries()).map(([date, sessionCount]) => ({ date, sessionCount })).sort((a, b) => a.date.localeCompare(b.date));
@@ -1051,23 +1113,23 @@ function loadCodexStats() {
1051
1113
  modelUsage,
1052
1114
  dailyActivity,
1053
1115
  hourCounts,
1054
- firstSessionDate: sessions[0].timestamp.split("T")[0]
1116
+ firstSessionDate: toLocalDateString3(sessions[0].timestamp)
1055
1117
  };
1056
1118
  }
1057
1119
 
1058
1120
  // src/shared/data-loader.ts
1059
- function loadData(options) {
1121
+ async function loadData(options) {
1060
1122
  const { codexOnly, combined } = options;
1061
1123
  let claude = null;
1062
1124
  let codex = null;
1063
1125
  if (!codexOnly) {
1064
- if (claudeJsonlDataExists()) {
1065
- claude = loadClaudeStatsFromJsonl();
1126
+ if (await claudeJsonlDataExists()) {
1127
+ claude = await loadClaudeStatsFromJsonl();
1066
1128
  }
1067
1129
  }
1068
1130
  if (codexOnly || combined) {
1069
- if (codexDataExists()) {
1070
- codex = loadCodexStats();
1131
+ if (await codexDataExists()) {
1132
+ codex = await loadCodexStats();
1071
1133
  }
1072
1134
  }
1073
1135
  let source = "claude";
@@ -1092,6 +1154,12 @@ function validateData(data, options) {
1092
1154
  }
1093
1155
 
1094
1156
  // src/metrics.ts
1157
+ function toLocalDateString4(date) {
1158
+ const year = date.getFullYear();
1159
+ const month = String(date.getMonth() + 1).padStart(2, "0");
1160
+ const day = String(date.getDate()).padStart(2, "0");
1161
+ return `${year}-${month}-${day}`;
1162
+ }
1095
1163
  function calculateModelCost(modelName, usage) {
1096
1164
  const pricing = getModelPricing(modelName);
1097
1165
  const inputCost = usage.inputTokens / 1e6 * pricing.input;
@@ -1130,7 +1198,7 @@ function calculateStreaks(dailyActivity) {
1130
1198
  let checkDate = today;
1131
1199
  current = 0;
1132
1200
  while (true) {
1133
- const dateStr = checkDate.toISOString().split("T")[0];
1201
+ const dateStr = toLocalDateString4(checkDate);
1134
1202
  if (dateSet.has(dateStr)) {
1135
1203
  current++;
1136
1204
  checkDate = new Date(checkDate.getTime() - 24 * 60 * 60 * 1e3);
@@ -1705,7 +1773,7 @@ async function createShortlink(params, baseUrl) {
1705
1773
  }
1706
1774
 
1707
1775
  // src/config.ts
1708
- import { readFileSync as readFileSync4, existsSync as existsSync4, writeFileSync } from "fs";
1776
+ import { readFileSync, existsSync, writeFileSync } from "fs";
1709
1777
  import { homedir as homedir4 } from "os";
1710
1778
  import { join as join4 } from "path";
1711
1779
  var CONFIG_PATH = join4(homedir4(), ".vibestats.json");
@@ -1718,11 +1786,11 @@ var DEFAULT_CONFIG = {
1718
1786
  hideCost: false
1719
1787
  };
1720
1788
  function loadConfig() {
1721
- if (!existsSync4(CONFIG_PATH)) {
1789
+ if (!existsSync(CONFIG_PATH)) {
1722
1790
  return DEFAULT_CONFIG;
1723
1791
  }
1724
1792
  try {
1725
- const content = readFileSync4(CONFIG_PATH, "utf-8");
1793
+ const content = readFileSync(CONFIG_PATH, "utf-8");
1726
1794
  const userConfig = JSON.parse(content);
1727
1795
  return mergeConfig(DEFAULT_CONFIG, userConfig);
1728
1796
  } catch {
@@ -1741,7 +1809,7 @@ function mergeConfig(defaults, user) {
1741
1809
  };
1742
1810
  }
1743
1811
  function initConfig() {
1744
- if (existsSync4(CONFIG_PATH)) {
1812
+ if (existsSync(CONFIG_PATH)) {
1745
1813
  console.log(`Config file already exists at ${CONFIG_PATH}`);
1746
1814
  return;
1747
1815
  }
@@ -1772,6 +1840,41 @@ function resolveOptions(cliArgs, config) {
1772
1840
  }
1773
1841
 
1774
1842
  // src/index.ts
1843
+ function createSpinner(label = "Loading vibestats...") {
1844
+ const spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1845
+ const orange = "\x1B[38;5;208m";
1846
+ const bold = "\x1B[1m";
1847
+ const reset = "\x1B[0m";
1848
+ let spinnerIdx = 0;
1849
+ let interval = null;
1850
+ const render = () => {
1851
+ const frame = spinnerFrames[spinnerIdx];
1852
+ process.stdout.write(`\r${orange}${frame}${reset} ${bold}${label}${reset} `);
1853
+ spinnerIdx = (spinnerIdx + 1) % spinnerFrames.length;
1854
+ };
1855
+ const start = () => {
1856
+ if (interval) return;
1857
+ process.stdout.write("\x1B[?25l");
1858
+ render();
1859
+ interval = setInterval(render, 80);
1860
+ };
1861
+ const stop = () => {
1862
+ if (!interval) return;
1863
+ clearInterval(interval);
1864
+ interval = null;
1865
+ process.stdout.write("\r\x1B[2K");
1866
+ process.stdout.write("\x1B[?25h");
1867
+ };
1868
+ const whilePromise = async (promise) => {
1869
+ start();
1870
+ try {
1871
+ return await promise;
1872
+ } finally {
1873
+ stop();
1874
+ }
1875
+ };
1876
+ return { whilePromise, stop };
1877
+ }
1775
1878
  var main = defineCommand({
1776
1879
  meta: {
1777
1880
  name: "vibestats",
@@ -1899,13 +2002,16 @@ async function runUsage(args, config) {
1899
2002
  if (args.monthly) aggregation = "monthly";
1900
2003
  else if (args.model) aggregation = "model";
1901
2004
  else if (args.total) aggregation = "total";
1902
- const stats = loadUsageStats({
1903
- aggregation,
1904
- since: args.since,
1905
- until: args.until,
1906
- codexOnly: args.codex,
1907
- combined: args.combined
1908
- });
2005
+ const spinner = createSpinner("Loading vibestats...");
2006
+ const stats = await spinner.whilePromise(
2007
+ loadUsageStats({
2008
+ aggregation,
2009
+ since: args.since,
2010
+ until: args.until,
2011
+ codexOnly: args.codex,
2012
+ combined: args.combined
2013
+ })
2014
+ );
1909
2015
  if (!stats) {
1910
2016
  if (args.codex) {
1911
2017
  console.error("Error: OpenAI Codex data not found at ~/.codex");
@@ -1959,7 +2065,10 @@ async function runUsage(args, config) {
1959
2065
  }
1960
2066
  async function runWrapped(args, config) {
1961
2067
  const options = resolveOptions(args, config);
1962
- const data = loadData({ codexOnly: args.codex, combined: args.combined });
2068
+ const spinner = createSpinner("Preparing wrapped...");
2069
+ const data = await spinner.whilePromise(
2070
+ loadData({ codexOnly: args.codex, combined: args.combined })
2071
+ );
1963
2072
  validateData(data, { codexOnly: args.codex, combined: args.combined });
1964
2073
  let claudeStats = null;
1965
2074
  let codexStats = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibestats",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
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",