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.
- package/dist/index.js +205 -96
- 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 {
|
|
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
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
259
|
+
return await fs.realpath(path);
|
|
247
260
|
} catch {
|
|
248
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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
|
|
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
|
|
326
|
-
|
|
327
|
-
|
|
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 =
|
|
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
|
|
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 {
|
|
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
|
|
743
|
-
const
|
|
744
|
-
const
|
|
745
|
-
|
|
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 (!
|
|
750
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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()
|
|
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 {
|
|
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
|
|
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 (!
|
|
935
|
+
if (!await pathExists3(codexDir)) return false;
|
|
884
936
|
const sessionsDir = join3(codexDir, "sessions");
|
|
885
937
|
const archivedDir = join3(codexDir, "archived_sessions");
|
|
886
|
-
return
|
|
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 (!
|
|
891
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
986
|
-
|
|
987
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
1789
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
1722
1790
|
return DEFAULT_CONFIG;
|
|
1723
1791
|
}
|
|
1724
1792
|
try {
|
|
1725
|
-
const content =
|
|
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 (
|
|
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
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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
|
|
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;
|