vibestats 1.0.21 → 1.0.22

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 +171 -89
  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,66 @@ function calculateCodexCost(modelName, inputTokens, outputTokens, cachedInputTok
232
232
  }
233
233
 
234
234
  // src/usage/loader.ts
235
+ var MAX_RECURSION_DEPTH = 10;
235
236
  function getClaudeDir() {
236
237
  return process.env.CLAUDE_HOME || join(homedir(), ".claude");
237
238
  }
238
239
  function getCodexDir() {
239
240
  return join(homedir(), ".codex");
240
241
  }
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;
242
+ async function pathExists(path) {
245
243
  try {
246
- realPath = realpathSync(dir);
244
+ await fs.access(path);
245
+ return true;
247
246
  } catch {
248
- realPath = dir;
247
+ return false;
249
248
  }
249
+ }
250
+ async function safeRealpath(path) {
251
+ try {
252
+ return await fs.realpath(path);
253
+ } catch {
254
+ return path;
255
+ }
256
+ }
257
+ async function findJsonlFiles(dir, visited = /* @__PURE__ */ new Set(), depth = 0, result = []) {
258
+ if (!await pathExists(dir)) return result;
259
+ if (depth > MAX_RECURSION_DEPTH) return result;
260
+ const realPath = await safeRealpath(dir);
250
261
  if (visited.has(realPath)) return result;
251
262
  visited.add(realPath);
263
+ let entries;
252
264
  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
- }
265
+ entries = await fs.readdir(dir);
266
266
  } catch {
267
+ return result;
268
+ }
269
+ for (const entry of entries) {
270
+ const fullPath = join(dir, entry);
271
+ let stat;
272
+ try {
273
+ stat = await fs.stat(fullPath);
274
+ } catch {
275
+ continue;
276
+ }
277
+ if (stat.isDirectory()) {
278
+ await findJsonlFiles(fullPath, visited, depth + 1, result);
279
+ } else if (entry.endsWith(".jsonl")) {
280
+ result.push(fullPath);
281
+ }
267
282
  }
268
283
  return result;
269
284
  }
270
- function parseClaudeJsonl() {
285
+ async function parseClaudeJsonl() {
271
286
  const entries = [];
272
287
  const seenMessageIds = /* @__PURE__ */ new Set();
273
288
  const claudeDir = getClaudeDir();
274
289
  const projectsDir = join(claudeDir, "projects");
275
- if (!existsSync(projectsDir)) return entries;
276
- const jsonlFiles = findJsonlFiles(projectsDir);
290
+ if (!await pathExists(projectsDir)) return entries;
291
+ const jsonlFiles = await findJsonlFiles(projectsDir);
277
292
  for (const filePath of jsonlFiles) {
278
293
  try {
279
- const content = readFileSync(filePath, "utf-8");
294
+ const content = await fs.readFile(filePath, "utf-8");
280
295
  const lines = content.split("\n");
281
296
  for (const line of lines) {
282
297
  if (!line.trim()) continue;
@@ -317,19 +332,20 @@ function parseClaudeJsonl() {
317
332
  }
318
333
  return entries;
319
334
  }
320
- function parseCodexJsonl() {
335
+ async function parseCodexJsonl() {
321
336
  const entries = [];
322
337
  const codexDir = getCodexDir();
323
338
  const sessionsDir = join(codexDir, "sessions");
324
339
  const archivedDir = join(codexDir, "archived_sessions");
325
- const jsonlFiles = [
326
- ...findJsonlFiles(sessionsDir),
327
- ...findJsonlFiles(archivedDir)
328
- ];
340
+ const [sessionFiles, archivedFiles] = await Promise.all([
341
+ findJsonlFiles(sessionsDir),
342
+ findJsonlFiles(archivedDir)
343
+ ]);
344
+ const jsonlFiles = [...sessionFiles, ...archivedFiles];
329
345
  if (jsonlFiles.length === 0) return entries;
330
346
  for (const filePath of jsonlFiles) {
331
347
  try {
332
- const content = readFileSync(filePath, "utf-8");
348
+ const content = await fs.readFile(filePath, "utf-8");
333
349
  const lines = content.split("\n");
334
350
  let currentModel = "gpt-5";
335
351
  for (const line of lines) {
@@ -356,7 +372,6 @@ function parseCodexJsonl() {
356
372
  date,
357
373
  model: getCodexModelDisplayName(currentModel),
358
374
  inputTokens: inputTokens - cachedInputTokens,
359
- // OpenAI input_tokens includes cached, so subtract
360
375
  outputTokens,
361
376
  cacheWriteTokens: 0,
362
377
  cacheReadTokens: cachedInputTokens,
@@ -381,7 +396,6 @@ function filterByDateRange(entries, since, until) {
381
396
  }
382
397
  function sortModelsByTier(models) {
383
398
  const tierPriority = {
384
- // Claude models (higher = first)
385
399
  "Opus 4.5": 100,
386
400
  "Opus 4.1": 99,
387
401
  "Opus": 98,
@@ -391,7 +405,6 @@ function sortModelsByTier(models) {
391
405
  "Haiku 4.5": 80,
392
406
  "Haiku 3.5": 79,
393
407
  "Haiku": 78,
394
- // OpenAI/Codex models
395
408
  "GPT-5.2": 70,
396
409
  "GPT-5.1 Max": 69,
397
410
  "GPT-5.1": 68,
@@ -526,14 +539,14 @@ function computeModelBreakdown(entries) {
526
539
  percentage: totalTokens > 0 ? Math.round(data.tokens / totalTokens * 100) : 0
527
540
  })).sort((a, b) => b.tokens - a.tokens);
528
541
  }
529
- function loadUsageStats(options) {
542
+ async function loadUsageStats(options) {
530
543
  const { aggregation, since, until, codexOnly, combined } = options;
531
544
  let entries = [];
532
545
  if (!codexOnly) {
533
- entries = entries.concat(parseClaudeJsonl());
546
+ entries = entries.concat(await parseClaudeJsonl());
534
547
  }
535
548
  if (codexOnly || combined) {
536
- entries = entries.concat(parseCodexJsonl());
549
+ entries = entries.concat(await parseCodexJsonl());
537
550
  }
538
551
  if (entries.length === 0) {
539
552
  return null;
@@ -733,27 +746,39 @@ function displayTotalOnly(stats, options = {}) {
733
746
  }
734
747
 
735
748
  // src/claude-jsonl-loader.ts
736
- import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
749
+ import { promises as fs2 } from "fs";
737
750
  import { homedir as homedir2 } from "os";
738
751
  import { join as join2 } from "path";
739
752
  function getClaudeDir2() {
740
753
  return process.env.CLAUDE_HOME || join2(homedir2(), ".claude");
741
754
  }
742
- function claudeJsonlDataExists() {
743
- const claudeDir = getClaudeDir2();
744
- const projectsDir = join2(claudeDir, "projects");
745
- return existsSync2(projectsDir);
755
+ async function pathExists2(path) {
756
+ try {
757
+ await fs2.access(path);
758
+ return true;
759
+ } catch {
760
+ return false;
761
+ }
762
+ }
763
+ async function claudeJsonlDataExists() {
764
+ const projectsDir = join2(getClaudeDir2(), "projects");
765
+ return pathExists2(projectsDir);
746
766
  }
747
- function findJsonlFiles2(dir) {
767
+ async function findJsonlFiles2(dir) {
748
768
  const files = [];
749
- if (!existsSync2(dir)) return files;
750
- const entries = readdirSync2(dir);
769
+ if (!await pathExists2(dir)) return files;
770
+ let entries;
771
+ try {
772
+ entries = await fs2.readdir(dir);
773
+ } catch {
774
+ return files;
775
+ }
751
776
  for (const entry of entries) {
752
777
  const fullPath = join2(dir, entry);
753
778
  try {
754
- const stat = statSync2(fullPath);
779
+ const stat = await fs2.stat(fullPath);
755
780
  if (stat.isDirectory()) {
756
- files.push(...findJsonlFiles2(fullPath));
781
+ files.push(...await findJsonlFiles2(fullPath));
757
782
  } else if (entry.endsWith(".jsonl")) {
758
783
  files.push(fullPath);
759
784
  }
@@ -762,13 +787,13 @@ function findJsonlFiles2(dir) {
762
787
  }
763
788
  return files;
764
789
  }
765
- function loadClaudeStatsFromJsonl() {
790
+ async function loadClaudeStatsFromJsonl() {
766
791
  const claudeDir = getClaudeDir2();
767
792
  const projectsDir = join2(claudeDir, "projects");
768
- if (!claudeJsonlDataExists()) {
793
+ if (!await claudeJsonlDataExists()) {
769
794
  return null;
770
795
  }
771
- const jsonlFiles = findJsonlFiles2(projectsDir);
796
+ const jsonlFiles = await findJsonlFiles2(projectsDir);
772
797
  if (jsonlFiles.length === 0) {
773
798
  return null;
774
799
  }
@@ -780,7 +805,7 @@ function loadClaudeStatsFromJsonl() {
780
805
  const messageIds = /* @__PURE__ */ new Set();
781
806
  for (const filePath of jsonlFiles) {
782
807
  try {
783
- const content = readFileSync2(filePath, "utf-8");
808
+ const content = await fs2.readFile(filePath, "utf-8");
784
809
  const lines = content.split("\n");
785
810
  for (const line of lines) {
786
811
  if (!line.trim()) continue;
@@ -839,7 +864,6 @@ function loadClaudeStatsFromJsonl() {
839
864
  cacheCreationInputTokens: usage.cacheCreationInputTokens,
840
865
  webSearchRequests: 0,
841
866
  costUSD: 0,
842
- // Will be calculated by metrics.ts
843
867
  contextWindow: 2e5
844
868
  };
845
869
  }
@@ -847,16 +871,15 @@ function loadClaudeStatsFromJsonl() {
847
871
  date,
848
872
  messageCount: data.messageCount,
849
873
  sessionCount: 1,
850
- // Each day counts as at least 1 session for activity
851
874
  toolCallCount: data.toolCallCount
852
875
  })).sort((a, b) => a.date.localeCompare(b.date));
853
876
  const sessionsByDate = /* @__PURE__ */ new Map();
877
+ void sessionsByDate;
854
878
  return {
855
879
  version: 1,
856
880
  lastComputedDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
857
881
  dailyActivity,
858
882
  dailyModelTokens: [],
859
- // Not needed for wrapped stats
860
883
  modelUsage: statsCacheModelUsage,
861
884
  totalSessions: jsonlFiles.length,
862
885
  totalMessages,
@@ -872,37 +895,55 @@ function loadClaudeStatsFromJsonl() {
872
895
  }
873
896
 
874
897
  // src/codex-loader.ts
875
- import { readFileSync as readFileSync3, existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
898
+ import { promises as fs3 } from "fs";
876
899
  import { homedir as homedir3 } from "os";
877
900
  import { join as join3 } from "path";
878
901
  function getCodexDir2() {
879
902
  return process.env.CODEX_HOME || join3(homedir3(), ".codex");
880
903
  }
881
- function codexDataExists() {
904
+ async function pathExists3(path) {
905
+ try {
906
+ await fs3.access(path);
907
+ return true;
908
+ } catch {
909
+ return false;
910
+ }
911
+ }
912
+ async function codexDataExists() {
882
913
  const codexDir = getCodexDir2();
883
- if (!existsSync3(codexDir)) return false;
914
+ if (!await pathExists3(codexDir)) return false;
884
915
  const sessionsDir = join3(codexDir, "sessions");
885
916
  const archivedDir = join3(codexDir, "archived_sessions");
886
- return existsSync3(sessionsDir) || existsSync3(archivedDir);
917
+ return await pathExists3(sessionsDir) || await pathExists3(archivedDir);
887
918
  }
888
- function findJsonlFiles3(dir) {
919
+ async function findJsonlFiles3(dir) {
889
920
  const files = [];
890
- if (!existsSync3(dir)) return files;
891
- const entries = readdirSync3(dir);
921
+ if (!await pathExists3(dir)) return files;
922
+ let entries;
923
+ try {
924
+ entries = await fs3.readdir(dir);
925
+ } catch {
926
+ return files;
927
+ }
892
928
  for (const entry of entries) {
893
929
  const fullPath = join3(dir, entry);
894
- const stat = statSync3(fullPath);
930
+ let stat;
931
+ try {
932
+ stat = await fs3.stat(fullPath);
933
+ } catch {
934
+ continue;
935
+ }
895
936
  if (stat.isDirectory()) {
896
- files.push(...findJsonlFiles3(fullPath));
937
+ files.push(...await findJsonlFiles3(fullPath));
897
938
  } else if (entry.endsWith(".jsonl")) {
898
939
  files.push(fullPath);
899
940
  }
900
941
  }
901
942
  return files;
902
943
  }
903
- function parseSessionFile(filePath) {
944
+ async function parseSessionFile(filePath) {
904
945
  try {
905
- const content = readFileSync3(filePath, "utf-8");
946
+ const content = await fs3.readFile(filePath, "utf-8");
906
947
  const lines = content.trim().split("\n");
907
948
  let sessionMeta = null;
908
949
  let currentModel = "gpt-5";
@@ -969,29 +1010,29 @@ function parseSessionFile(filePath) {
969
1010
  model: primaryModel,
970
1011
  tokenUsage: summedUsage,
971
1012
  perModelUsage
972
- // New: per-model token breakdown
973
1013
  };
974
1014
  } catch {
975
1015
  return null;
976
1016
  }
977
1017
  }
978
- function loadCodexStats() {
1018
+ async function loadCodexStats() {
979
1019
  const codexDir = getCodexDir2();
980
- if (!codexDataExists()) {
1020
+ if (!await codexDataExists()) {
981
1021
  return null;
982
1022
  }
983
1023
  const sessionsDir = join3(codexDir, "sessions");
984
1024
  const archivedDir = join3(codexDir, "archived_sessions");
985
- const jsonlFiles = [
986
- ...findJsonlFiles3(sessionsDir),
987
- ...findJsonlFiles3(archivedDir)
988
- ];
1025
+ const [sessionFiles, archivedFiles] = await Promise.all([
1026
+ findJsonlFiles3(sessionsDir),
1027
+ findJsonlFiles3(archivedDir)
1028
+ ]);
1029
+ const jsonlFiles = [...sessionFiles, ...archivedFiles];
989
1030
  if (jsonlFiles.length === 0) {
990
1031
  return null;
991
1032
  }
992
1033
  const sessions = [];
993
1034
  for (const file of jsonlFiles) {
994
- const session = parseSessionFile(file);
1035
+ const session = await parseSessionFile(file);
995
1036
  if (session) {
996
1037
  sessions.push(session);
997
1038
  }
@@ -1056,18 +1097,18 @@ function loadCodexStats() {
1056
1097
  }
1057
1098
 
1058
1099
  // src/shared/data-loader.ts
1059
- function loadData(options) {
1100
+ async function loadData(options) {
1060
1101
  const { codexOnly, combined } = options;
1061
1102
  let claude = null;
1062
1103
  let codex = null;
1063
1104
  if (!codexOnly) {
1064
- if (claudeJsonlDataExists()) {
1065
- claude = loadClaudeStatsFromJsonl();
1105
+ if (await claudeJsonlDataExists()) {
1106
+ claude = await loadClaudeStatsFromJsonl();
1066
1107
  }
1067
1108
  }
1068
1109
  if (codexOnly || combined) {
1069
- if (codexDataExists()) {
1070
- codex = loadCodexStats();
1110
+ if (await codexDataExists()) {
1111
+ codex = await loadCodexStats();
1071
1112
  }
1072
1113
  }
1073
1114
  let source = "claude";
@@ -1705,7 +1746,7 @@ async function createShortlink(params, baseUrl) {
1705
1746
  }
1706
1747
 
1707
1748
  // src/config.ts
1708
- import { readFileSync as readFileSync4, existsSync as existsSync4, writeFileSync } from "fs";
1749
+ import { readFileSync, existsSync, writeFileSync } from "fs";
1709
1750
  import { homedir as homedir4 } from "os";
1710
1751
  import { join as join4 } from "path";
1711
1752
  var CONFIG_PATH = join4(homedir4(), ".vibestats.json");
@@ -1718,11 +1759,11 @@ var DEFAULT_CONFIG = {
1718
1759
  hideCost: false
1719
1760
  };
1720
1761
  function loadConfig() {
1721
- if (!existsSync4(CONFIG_PATH)) {
1762
+ if (!existsSync(CONFIG_PATH)) {
1722
1763
  return DEFAULT_CONFIG;
1723
1764
  }
1724
1765
  try {
1725
- const content = readFileSync4(CONFIG_PATH, "utf-8");
1766
+ const content = readFileSync(CONFIG_PATH, "utf-8");
1726
1767
  const userConfig = JSON.parse(content);
1727
1768
  return mergeConfig(DEFAULT_CONFIG, userConfig);
1728
1769
  } catch {
@@ -1741,7 +1782,7 @@ function mergeConfig(defaults, user) {
1741
1782
  };
1742
1783
  }
1743
1784
  function initConfig() {
1744
- if (existsSync4(CONFIG_PATH)) {
1785
+ if (existsSync(CONFIG_PATH)) {
1745
1786
  console.log(`Config file already exists at ${CONFIG_PATH}`);
1746
1787
  return;
1747
1788
  }
@@ -1772,6 +1813,41 @@ function resolveOptions(cliArgs, config) {
1772
1813
  }
1773
1814
 
1774
1815
  // src/index.ts
1816
+ function createSpinner(label = "Loading vibestats...") {
1817
+ const spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1818
+ const orange = "\x1B[38;5;208m";
1819
+ const bold = "\x1B[1m";
1820
+ const reset = "\x1B[0m";
1821
+ let spinnerIdx = 0;
1822
+ let interval = null;
1823
+ const render = () => {
1824
+ const frame = spinnerFrames[spinnerIdx];
1825
+ process.stdout.write(`\r${orange}${frame}${reset} ${bold}${label}${reset} `);
1826
+ spinnerIdx = (spinnerIdx + 1) % spinnerFrames.length;
1827
+ };
1828
+ const start = () => {
1829
+ if (interval) return;
1830
+ process.stdout.write("\x1B[?25l");
1831
+ render();
1832
+ interval = setInterval(render, 80);
1833
+ };
1834
+ const stop = () => {
1835
+ if (!interval) return;
1836
+ clearInterval(interval);
1837
+ interval = null;
1838
+ process.stdout.write("\r\x1B[2K");
1839
+ process.stdout.write("\x1B[?25h");
1840
+ };
1841
+ const whilePromise = async (promise) => {
1842
+ start();
1843
+ try {
1844
+ return await promise;
1845
+ } finally {
1846
+ stop();
1847
+ }
1848
+ };
1849
+ return { whilePromise, stop };
1850
+ }
1775
1851
  var main = defineCommand({
1776
1852
  meta: {
1777
1853
  name: "vibestats",
@@ -1899,13 +1975,16 @@ async function runUsage(args, config) {
1899
1975
  if (args.monthly) aggregation = "monthly";
1900
1976
  else if (args.model) aggregation = "model";
1901
1977
  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
- });
1978
+ const spinner = createSpinner("Loading vibestats...");
1979
+ const stats = await spinner.whilePromise(
1980
+ loadUsageStats({
1981
+ aggregation,
1982
+ since: args.since,
1983
+ until: args.until,
1984
+ codexOnly: args.codex,
1985
+ combined: args.combined
1986
+ })
1987
+ );
1909
1988
  if (!stats) {
1910
1989
  if (args.codex) {
1911
1990
  console.error("Error: OpenAI Codex data not found at ~/.codex");
@@ -1959,7 +2038,10 @@ async function runUsage(args, config) {
1959
2038
  }
1960
2039
  async function runWrapped(args, config) {
1961
2040
  const options = resolveOptions(args, config);
1962
- const data = loadData({ codexOnly: args.codex, combined: args.combined });
2041
+ const spinner = createSpinner("Preparing wrapped...");
2042
+ const data = await spinner.whilePromise(
2043
+ loadData({ codexOnly: args.codex, combined: args.combined })
2044
+ );
1963
2045
  validateData(data, { codexOnly: args.codex, combined: args.combined });
1964
2046
  let claudeStats = null;
1965
2047
  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.22",
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",