vibestats 1.3.8 → 1.3.9
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/README.md +25 -1
- package/dist/index.js +519 -298
- package/package.json +7 -8
package/README.md
CHANGED
|
@@ -21,6 +21,10 @@ vibestats --total # Show only totals
|
|
|
21
21
|
|
|
22
22
|
# Wrapped summary
|
|
23
23
|
vibestats --wrapped # Annual wrapped summary
|
|
24
|
+
|
|
25
|
+
# Claude local diagnostics
|
|
26
|
+
vibestats --claude-system
|
|
27
|
+
vibestats --claude-limits
|
|
24
28
|
```
|
|
25
29
|
|
|
26
30
|
## CLI Flags
|
|
@@ -41,6 +45,8 @@ vibestats --wrapped # Annual wrapped summary
|
|
|
41
45
|
| `--quiet`, `-q` | Quiet output (totals line) |
|
|
42
46
|
| `--share`, `-s` | Generate a shareable usage URL |
|
|
43
47
|
| `--project`, `-p` | Current project only (Claude Code) |
|
|
48
|
+
| `--claude-system` | Inspect `~/.claude.json` account and app state |
|
|
49
|
+
| `--claude-limits` | Inspect `~/.claude/usage-data`, cache freshness, and local limit signals |
|
|
44
50
|
|
|
45
51
|
### Wrapped Mode
|
|
46
52
|
|
|
@@ -90,6 +96,20 @@ Creates `~/.vibestats.json`:
|
|
|
90
96
|
| Claude Code | `~/.claude/projects/**/*.jsonl` |
|
|
91
97
|
| OpenAI Codex | `~/.codex/sessions/*.jsonl` |
|
|
92
98
|
|
|
99
|
+
Additional Claude diagnostic files:
|
|
100
|
+
- `~/.claude.json`
|
|
101
|
+
- `~/.claude/stats-cache.json`
|
|
102
|
+
- `~/.claude/usage-data/session-meta/*.json`
|
|
103
|
+
- `~/.claude/usage-data/facets/*.json`
|
|
104
|
+
|
|
105
|
+
## Session Semantics
|
|
106
|
+
|
|
107
|
+
- `sessions` means canonical top-level sessions
|
|
108
|
+
- subagents are counted separately and shown as a compact session mix notice
|
|
109
|
+
- token and cost totals still include subagent usage
|
|
110
|
+
- Claude subagents are inferred from sidechain/session metadata
|
|
111
|
+
- Codex subagents are inferred from spawned-thread metadata
|
|
112
|
+
|
|
93
113
|
## Requirements
|
|
94
114
|
|
|
95
115
|
- Node.js 18+
|
|
@@ -97,7 +117,11 @@ Creates `~/.vibestats.json`:
|
|
|
97
117
|
|
|
98
118
|
## View Online
|
|
99
119
|
|
|
100
|
-
Visit [vibestats.wolfai.dev](https://vibestats.wolfai.dev) to view
|
|
120
|
+
Visit [vibestats.wolfai.dev](https://vibestats.wolfai.dev) to view shares in the browser.
|
|
121
|
+
|
|
122
|
+
- [vibestats.wolfai.dev/wrapped](https://vibestats.wolfai.dev/wrapped)
|
|
123
|
+
- [vibestats.wolfai.dev/activity](https://vibestats.wolfai.dev/activity)
|
|
124
|
+
- [vibestats.wolfai.dev/changelog](https://vibestats.wolfai.dev/changelog)
|
|
101
125
|
|
|
102
126
|
## License
|
|
103
127
|
|
package/dist/index.js
CHANGED
|
@@ -635,7 +635,7 @@ function buildActivityGraph(stats, metric, requestedDays = 365) {
|
|
|
635
635
|
};
|
|
636
636
|
}
|
|
637
637
|
function buildActivityTitle(source, metric) {
|
|
638
|
-
const sourceLabel = source === "codex" ? "Codex" : source === "combined" ? "AI Coding" : "
|
|
638
|
+
const sourceLabel = source === "codex" ? "Codex" : source === "combined" ? "AI Coding" : "Local AI";
|
|
639
639
|
const metricLabel = metric === "tokens" ? "Activity" : metric === "sessions" ? "Session Activity" : "Message Activity";
|
|
640
640
|
return `${sourceLabel} ${metricLabel}`;
|
|
641
641
|
}
|
|
@@ -661,12 +661,38 @@ function buildActivityArtifact(stats, metric, requestedDays = 365) {
|
|
|
661
661
|
}
|
|
662
662
|
|
|
663
663
|
// src/usage/loader.ts
|
|
664
|
+
import { promises as fs2 } from "fs";
|
|
665
|
+
import { homedir as homedir2 } from "os";
|
|
666
|
+
import { join as join2, basename as basename2 } from "path";
|
|
667
|
+
|
|
668
|
+
// src/anthropic-sources.ts
|
|
669
|
+
import { execFile } from "child_process";
|
|
664
670
|
import { promises as fs } from "fs";
|
|
665
671
|
import { homedir } from "os";
|
|
666
|
-
import {
|
|
672
|
+
import { basename, join } from "path";
|
|
673
|
+
import { promisify } from "util";
|
|
667
674
|
|
|
668
675
|
// src/pricing.ts
|
|
669
676
|
var MODEL_PRICING = {
|
|
677
|
+
// Fireworks router pricing as of 2026-03-30:
|
|
678
|
+
// https://fireworks.ai/pricing
|
|
679
|
+
// Fireworks exposes a single "cached input" rate, so cache creation
|
|
680
|
+
// tokens are treated as regular input and cache reads use the discounted rate.
|
|
681
|
+
"accounts/fireworks/routers/kimi-k2p5-turbo": {
|
|
682
|
+
input: 0.99,
|
|
683
|
+
output: 4.94,
|
|
684
|
+
cacheWrite: 0.99,
|
|
685
|
+
cacheRead: 0.16
|
|
686
|
+
},
|
|
687
|
+
// Fireworks prices the MiniMax M2 family at one rate. We apply that
|
|
688
|
+
// official family pricing to MiniMax-M2.7 because Fireworks does not
|
|
689
|
+
// publish a distinct M2.7 per-token row.
|
|
690
|
+
"MiniMax-M2.7": {
|
|
691
|
+
input: 0.3,
|
|
692
|
+
output: 1.2,
|
|
693
|
+
cacheWrite: 0.3,
|
|
694
|
+
cacheRead: 0.03
|
|
695
|
+
},
|
|
670
696
|
// Opus 4.6 (same pricing as Opus 4.5)
|
|
671
697
|
"claude-opus-4-6-20260101": {
|
|
672
698
|
input: 5,
|
|
@@ -730,10 +756,17 @@ var MODEL_PRICING = {
|
|
|
730
756
|
cacheRead: 0.1
|
|
731
757
|
}
|
|
732
758
|
};
|
|
733
|
-
function
|
|
759
|
+
function tryGetModelPricing(modelName) {
|
|
734
760
|
if (MODEL_PRICING[modelName]) {
|
|
735
761
|
return MODEL_PRICING[modelName];
|
|
736
762
|
}
|
|
763
|
+
const normalized = modelName.toLowerCase();
|
|
764
|
+
if (normalized.includes("kimi-k2p5-turbo")) {
|
|
765
|
+
return MODEL_PRICING["accounts/fireworks/routers/kimi-k2p5-turbo"];
|
|
766
|
+
}
|
|
767
|
+
if (normalized.includes("minimax-m2.7")) {
|
|
768
|
+
return MODEL_PRICING["MiniMax-M2.7"];
|
|
769
|
+
}
|
|
737
770
|
if (modelName.includes("opus-4-6") || modelName.includes("opus-4.6")) {
|
|
738
771
|
return MODEL_PRICING["claude-opus-4-6-20260101"];
|
|
739
772
|
}
|
|
@@ -758,9 +791,16 @@ function getModelPricing(modelName) {
|
|
|
758
791
|
if (modelName.includes("haiku")) {
|
|
759
792
|
return MODEL_PRICING["claude-3-5-haiku-20241022"];
|
|
760
793
|
}
|
|
761
|
-
return
|
|
794
|
+
return null;
|
|
762
795
|
}
|
|
763
796
|
function getModelDisplayName(modelName) {
|
|
797
|
+
const normalized = modelName.toLowerCase();
|
|
798
|
+
if (normalized.includes("accounts/fireworks/routers/kimi-k2p5-turbo") || normalized.includes("custom:kimi-k2.5-turbo")) {
|
|
799
|
+
return "Kimi K2.5 Turbo";
|
|
800
|
+
}
|
|
801
|
+
if (normalized.includes("minimax-m2.7") || normalized.includes("custom:minimax-m2.7")) {
|
|
802
|
+
return "MiniMax-M2.7";
|
|
803
|
+
}
|
|
764
804
|
if (modelName.includes("opus-4-6") || modelName.includes("opus-4.6")) return "Opus 4.6";
|
|
765
805
|
if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) return "Opus 4.5";
|
|
766
806
|
if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1")) return "Opus 4.1";
|
|
@@ -775,8 +815,10 @@ function getModelDisplayName(modelName) {
|
|
|
775
815
|
return modelName;
|
|
776
816
|
}
|
|
777
817
|
|
|
778
|
-
// src/
|
|
779
|
-
var
|
|
818
|
+
// src/anthropic-sources.ts
|
|
819
|
+
var execFileAsync = promisify(execFile);
|
|
820
|
+
var SQLITE_SEPARATOR = "";
|
|
821
|
+
var SQLITE_MAX_BUFFER = 64 * 1024 * 1024;
|
|
780
822
|
function toLocalDateString(isoTimestamp) {
|
|
781
823
|
const date = new Date(isoTimestamp);
|
|
782
824
|
const year = date.getFullYear();
|
|
@@ -784,12 +826,6 @@ function toLocalDateString(isoTimestamp) {
|
|
|
784
826
|
const day = String(date.getDate()).padStart(2, "0");
|
|
785
827
|
return `${year}-${month}-${day}`;
|
|
786
828
|
}
|
|
787
|
-
function getClaudeDir() {
|
|
788
|
-
return process.env.CLAUDE_HOME || join(homedir(), ".claude");
|
|
789
|
-
}
|
|
790
|
-
function getCodexDir() {
|
|
791
|
-
return process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
792
|
-
}
|
|
793
829
|
async function pathExists(path) {
|
|
794
830
|
try {
|
|
795
831
|
await fs.access(path);
|
|
@@ -805,9 +841,8 @@ async function safeRealpath(path) {
|
|
|
805
841
|
return path;
|
|
806
842
|
}
|
|
807
843
|
}
|
|
808
|
-
async function
|
|
844
|
+
async function findFiles(dir, matcher, visited = /* @__PURE__ */ new Set(), result = []) {
|
|
809
845
|
if (!await pathExists(dir)) return result;
|
|
810
|
-
if (depth > MAX_RECURSION_DEPTH) return result;
|
|
811
846
|
const realPath = await safeRealpath(dir);
|
|
812
847
|
if (visited.has(realPath)) return result;
|
|
813
848
|
visited.add(realPath);
|
|
@@ -826,13 +861,37 @@ async function findJsonlFiles(dir, visited = /* @__PURE__ */ new Set(), depth =
|
|
|
826
861
|
continue;
|
|
827
862
|
}
|
|
828
863
|
if (stat.isDirectory()) {
|
|
829
|
-
await
|
|
830
|
-
} else if (entry
|
|
864
|
+
await findFiles(fullPath, matcher, visited, result);
|
|
865
|
+
} else if (matcher(entry)) {
|
|
831
866
|
result.push(fullPath);
|
|
832
867
|
}
|
|
833
868
|
}
|
|
834
869
|
return result;
|
|
835
870
|
}
|
|
871
|
+
function normalizeAnthropicModelName(modelName) {
|
|
872
|
+
const trimmed = modelName.trim();
|
|
873
|
+
const normalized = trimmed.toLowerCase();
|
|
874
|
+
if (/^custom:minimax-m2\.7(?:-\d+)?$/.test(normalized)) {
|
|
875
|
+
return "MiniMax-M2.7";
|
|
876
|
+
}
|
|
877
|
+
if (/^custom:kimi-k2\.5-turbo(?:-\d+)?$/.test(normalized)) {
|
|
878
|
+
return "accounts/fireworks/routers/kimi-k2p5-turbo";
|
|
879
|
+
}
|
|
880
|
+
return trimmed;
|
|
881
|
+
}
|
|
882
|
+
function calculateKnownCost(modelName, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, explicitCost) {
|
|
883
|
+
if (typeof explicitCost === "number" && explicitCost > 0) {
|
|
884
|
+
return explicitCost;
|
|
885
|
+
}
|
|
886
|
+
const pricing = tryGetModelPricing(modelName);
|
|
887
|
+
if (!pricing) {
|
|
888
|
+
return 0;
|
|
889
|
+
}
|
|
890
|
+
return inputTokens / 1e6 * pricing.input + outputTokens / 1e6 * pricing.output + cacheWriteTokens / 1e6 * pricing.cacheWrite + cacheReadTokens / 1e6 * pricing.cacheRead;
|
|
891
|
+
}
|
|
892
|
+
function entryTotalTokens(entry) {
|
|
893
|
+
return entry.inputTokens + entry.outputTokens + entry.cacheWriteTokens + entry.cacheReadTokens;
|
|
894
|
+
}
|
|
836
895
|
async function resolveProjectDir(projectsDir, cwd) {
|
|
837
896
|
let current = cwd;
|
|
838
897
|
while (current && current !== "/") {
|
|
@@ -845,79 +904,413 @@ async function resolveProjectDir(projectsDir, cwd) {
|
|
|
845
904
|
}
|
|
846
905
|
return null;
|
|
847
906
|
}
|
|
848
|
-
|
|
907
|
+
function getClaudeDir() {
|
|
908
|
+
return process.env.CLAUDE_HOME || join(homedir(), ".claude");
|
|
909
|
+
}
|
|
910
|
+
function getOpenCodeDbPath() {
|
|
911
|
+
if (process.env.OPENCODE_DB_PATH) {
|
|
912
|
+
return process.env.OPENCODE_DB_PATH;
|
|
913
|
+
}
|
|
914
|
+
return join(homedir(), ".local", "share", "opencode", "opencode.db");
|
|
915
|
+
}
|
|
916
|
+
function getFactorySessionsDir() {
|
|
917
|
+
if (process.env.FACTORY_SESSIONS_DIR) {
|
|
918
|
+
return process.env.FACTORY_SESSIONS_DIR;
|
|
919
|
+
}
|
|
920
|
+
return join(homedir(), ".factory", "sessions");
|
|
921
|
+
}
|
|
922
|
+
async function claudeCompatibleDataExists() {
|
|
923
|
+
const [claudeProjects, opencodeDb, factorySessions] = await Promise.all([
|
|
924
|
+
pathExists(join(getClaudeDir(), "projects")),
|
|
925
|
+
pathExists(getOpenCodeDbPath()),
|
|
926
|
+
pathExists(getFactorySessionsDir())
|
|
927
|
+
]);
|
|
928
|
+
return claudeProjects || opencodeDb || factorySessions;
|
|
929
|
+
}
|
|
930
|
+
async function parseClaudeProjectEntries(projectFilter) {
|
|
849
931
|
const entries = [];
|
|
850
|
-
const seenMessageIds = /* @__PURE__ */ new Set();
|
|
851
932
|
const claudeDir = getClaudeDir();
|
|
852
933
|
const projectsDir = join(claudeDir, "projects");
|
|
853
934
|
if (!await pathExists(projectsDir)) return entries;
|
|
854
|
-
let searchDir;
|
|
935
|
+
let searchDir = projectsDir;
|
|
855
936
|
if (projectFilter) {
|
|
856
937
|
const resolved = await resolveProjectDir(projectsDir, projectFilter);
|
|
857
938
|
if (!resolved) return entries;
|
|
858
939
|
searchDir = resolved;
|
|
859
|
-
} else {
|
|
860
|
-
searchDir = projectsDir;
|
|
861
940
|
}
|
|
862
|
-
const jsonlFiles = await
|
|
941
|
+
const jsonlFiles = await findFiles(searchDir, (entry) => entry.endsWith(".jsonl"));
|
|
863
942
|
for (const filePath of jsonlFiles) {
|
|
864
943
|
try {
|
|
865
944
|
const content = await fs.readFile(filePath, "utf-8");
|
|
866
|
-
if (!content.trim()) continue;
|
|
867
945
|
const lines = content.split("\n");
|
|
868
946
|
const fallbackSessionId = basename(filePath, ".jsonl");
|
|
869
947
|
const isSubagentFile = filePath.includes("/subagents/");
|
|
870
|
-
|
|
948
|
+
const bestByMessageKey = /* @__PURE__ */ new Map();
|
|
949
|
+
for (let index = 0; index < lines.length; index++) {
|
|
950
|
+
const line = lines[index];
|
|
871
951
|
if (!line.trim()) continue;
|
|
872
952
|
try {
|
|
873
953
|
const entry = JSON.parse(line);
|
|
874
|
-
if (entry.type !== "assistant" || !entry.message?.usage) continue;
|
|
875
|
-
const
|
|
876
|
-
if (messageId) {
|
|
877
|
-
if (seenMessageIds.has(messageId)) continue;
|
|
878
|
-
seenMessageIds.add(messageId);
|
|
879
|
-
}
|
|
880
|
-
const usage = entry.message.usage;
|
|
881
|
-
const model = entry.message.model || "unknown";
|
|
882
|
-
const timestamp = entry.timestamp;
|
|
883
|
-
if (!timestamp) continue;
|
|
954
|
+
if (entry.type !== "assistant" || !entry.message?.usage || !entry.timestamp) continue;
|
|
955
|
+
const rawModel = normalizeAnthropicModelName(entry.message.model || "unknown");
|
|
884
956
|
const sessionId = entry.sessionId || fallbackSessionId;
|
|
885
957
|
const sessionKind = entry.isSidechain === true || isSubagentFile ? "subagent" : "main";
|
|
886
|
-
const
|
|
887
|
-
const
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
958
|
+
const usage = entry.message.usage;
|
|
959
|
+
const messageKey = entry.message.id ? `${sessionId}:${entry.message.id}` : `${filePath}:${index}`;
|
|
960
|
+
const candidate = {
|
|
961
|
+
date: toLocalDateString(entry.timestamp),
|
|
962
|
+
timestamp: entry.timestamp,
|
|
963
|
+
rawModel,
|
|
964
|
+
model: getModelDisplayName(rawModel),
|
|
965
|
+
inputTokens: usage.input_tokens || 0,
|
|
966
|
+
outputTokens: usage.output_tokens || 0,
|
|
967
|
+
cacheWriteTokens: usage.cache_creation_input_tokens || 0,
|
|
968
|
+
cacheReadTokens: usage.cache_read_input_tokens || 0,
|
|
969
|
+
cost: calculateKnownCost(
|
|
970
|
+
rawModel,
|
|
971
|
+
usage.input_tokens || 0,
|
|
972
|
+
usage.output_tokens || 0,
|
|
973
|
+
usage.cache_creation_input_tokens || 0,
|
|
974
|
+
usage.cache_read_input_tokens || 0
|
|
975
|
+
),
|
|
901
976
|
messageCount: 1,
|
|
902
977
|
source: "claude",
|
|
978
|
+
sourceKey: "claude",
|
|
903
979
|
sessionId,
|
|
904
980
|
sessionKind,
|
|
905
|
-
subagentId: sessionKind === "subagent" ? filePath : void 0
|
|
906
|
-
|
|
907
|
-
|
|
981
|
+
subagentId: sessionKind === "subagent" ? filePath : void 0
|
|
982
|
+
};
|
|
983
|
+
const existing = bestByMessageKey.get(messageKey);
|
|
984
|
+
if (!existing || entryTotalTokens(candidate) > entryTotalTokens(existing)) {
|
|
985
|
+
bestByMessageKey.set(messageKey, candidate);
|
|
986
|
+
}
|
|
908
987
|
} catch {
|
|
909
988
|
}
|
|
910
989
|
}
|
|
990
|
+
entries.push(...bestByMessageKey.values());
|
|
911
991
|
} catch {
|
|
912
992
|
}
|
|
913
993
|
}
|
|
914
994
|
return entries;
|
|
915
995
|
}
|
|
996
|
+
async function parseOpenCodeEntries() {
|
|
997
|
+
const dbPath = getOpenCodeDbPath();
|
|
998
|
+
if (!await pathExists(dbPath)) return [];
|
|
999
|
+
const sql = `
|
|
1000
|
+
SELECT
|
|
1001
|
+
COALESCE(m.id, ''),
|
|
1002
|
+
COALESCE(m.session_id, ''),
|
|
1003
|
+
COALESCE(s.parent_id, ''),
|
|
1004
|
+
COALESCE(json_extract(m.data, '$.modelID'), ''),
|
|
1005
|
+
COALESCE(json_extract(m.data, '$.time.created'), 0),
|
|
1006
|
+
COALESCE(json_extract(m.data, '$.tokens.input'), 0),
|
|
1007
|
+
COALESCE(json_extract(m.data, '$.tokens.output'), 0),
|
|
1008
|
+
COALESCE(json_extract(m.data, '$.tokens.cache.write'), 0),
|
|
1009
|
+
COALESCE(json_extract(m.data, '$.tokens.cache.read'), 0),
|
|
1010
|
+
COALESCE(json_extract(m.data, '$.cost'), 0)
|
|
1011
|
+
FROM message m
|
|
1012
|
+
JOIN session s ON s.id = m.session_id
|
|
1013
|
+
WHERE json_extract(m.data, '$.role') = 'assistant'
|
|
1014
|
+
AND json_extract(m.data, '$.modelID') IS NOT NULL;
|
|
1015
|
+
`;
|
|
1016
|
+
let stdout = "";
|
|
1017
|
+
try {
|
|
1018
|
+
const result = await execFileAsync(
|
|
1019
|
+
"sqlite3",
|
|
1020
|
+
["-separator", SQLITE_SEPARATOR, dbPath, sql],
|
|
1021
|
+
{ maxBuffer: SQLITE_MAX_BUFFER }
|
|
1022
|
+
);
|
|
1023
|
+
stdout = result.stdout;
|
|
1024
|
+
} catch {
|
|
1025
|
+
return [];
|
|
1026
|
+
}
|
|
1027
|
+
const entries = [];
|
|
1028
|
+
for (const line of stdout.split("\n")) {
|
|
1029
|
+
if (!line.trim()) continue;
|
|
1030
|
+
const [
|
|
1031
|
+
messageId,
|
|
1032
|
+
sessionId,
|
|
1033
|
+
parentId,
|
|
1034
|
+
modelId,
|
|
1035
|
+
createdMsText,
|
|
1036
|
+
inputText,
|
|
1037
|
+
outputText,
|
|
1038
|
+
cacheWriteText,
|
|
1039
|
+
cacheReadText,
|
|
1040
|
+
costText
|
|
1041
|
+
] = line.split(SQLITE_SEPARATOR);
|
|
1042
|
+
const createdMs = Number(createdMsText);
|
|
1043
|
+
if (!sessionId || !modelId || !Number.isFinite(createdMs) || createdMs <= 0) continue;
|
|
1044
|
+
const rawModel = normalizeAnthropicModelName(modelId);
|
|
1045
|
+
const timestamp = new Date(createdMs).toISOString();
|
|
1046
|
+
const inputTokens = Number(inputText) || 0;
|
|
1047
|
+
const outputTokens = Number(outputText) || 0;
|
|
1048
|
+
const cacheWriteTokens = Number(cacheWriteText) || 0;
|
|
1049
|
+
const cacheReadTokens = Number(cacheReadText) || 0;
|
|
1050
|
+
const explicitCost = Number(costText) || 0;
|
|
1051
|
+
const sessionKind = parentId ? "subagent" : "main";
|
|
1052
|
+
entries.push({
|
|
1053
|
+
date: toLocalDateString(timestamp),
|
|
1054
|
+
timestamp,
|
|
1055
|
+
rawModel,
|
|
1056
|
+
model: getModelDisplayName(rawModel),
|
|
1057
|
+
inputTokens,
|
|
1058
|
+
outputTokens,
|
|
1059
|
+
cacheWriteTokens,
|
|
1060
|
+
cacheReadTokens,
|
|
1061
|
+
cost: calculateKnownCost(rawModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, explicitCost),
|
|
1062
|
+
messageCount: 1,
|
|
1063
|
+
source: "claude",
|
|
1064
|
+
sourceKey: "opencode",
|
|
1065
|
+
sessionId,
|
|
1066
|
+
sessionKind,
|
|
1067
|
+
subagentId: sessionKind === "subagent" ? messageId || sessionId : void 0
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
return entries;
|
|
1071
|
+
}
|
|
1072
|
+
async function readFactorySessionMeta(sessionFilePath) {
|
|
1073
|
+
let messageCount = 0;
|
|
1074
|
+
let timestamp;
|
|
1075
|
+
let sessionKind = "main";
|
|
1076
|
+
let subagentId;
|
|
1077
|
+
try {
|
|
1078
|
+
const content = await fs.readFile(sessionFilePath, "utf-8");
|
|
1079
|
+
for (const line of content.split("\n")) {
|
|
1080
|
+
if (!line.trim()) continue;
|
|
1081
|
+
try {
|
|
1082
|
+
const entry = JSON.parse(line);
|
|
1083
|
+
if (!timestamp && typeof entry.timestamp === "string") {
|
|
1084
|
+
timestamp = entry.timestamp;
|
|
1085
|
+
}
|
|
1086
|
+
if (entry.type === "session_start") {
|
|
1087
|
+
const parent = typeof entry.callingSessionId === "string" ? entry.callingSessionId : void 0;
|
|
1088
|
+
if (parent) {
|
|
1089
|
+
sessionKind = "subagent";
|
|
1090
|
+
subagentId = String(entry.id || sessionFilePath);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
const message = entry.message;
|
|
1094
|
+
if (message?.role === "assistant") {
|
|
1095
|
+
messageCount++;
|
|
1096
|
+
}
|
|
1097
|
+
} catch {
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
} catch {
|
|
1101
|
+
}
|
|
1102
|
+
return { messageCount: Math.max(messageCount, 1), timestamp, sessionKind, subagentId };
|
|
1103
|
+
}
|
|
1104
|
+
async function parseFactoryEntries() {
|
|
1105
|
+
const sessionsDir = getFactorySessionsDir();
|
|
1106
|
+
if (!await pathExists(sessionsDir)) return [];
|
|
1107
|
+
const settingsFiles = await findFiles(sessionsDir, (entry) => entry.endsWith(".settings.json"));
|
|
1108
|
+
const entries = [];
|
|
1109
|
+
for (const settingsPath of settingsFiles) {
|
|
1110
|
+
try {
|
|
1111
|
+
const settings = JSON.parse(await fs.readFile(settingsPath, "utf-8"));
|
|
1112
|
+
const usage = settings.tokenUsage;
|
|
1113
|
+
const configuredModel = settings.model;
|
|
1114
|
+
if (!usage || !configuredModel) continue;
|
|
1115
|
+
const rawModel = normalizeAnthropicModelName(configuredModel);
|
|
1116
|
+
const inputTokens = usage.inputTokens || 0;
|
|
1117
|
+
const cacheWriteTokens = usage.cacheCreationTokens || 0;
|
|
1118
|
+
const cacheReadTokens = usage.cacheReadTokens || 0;
|
|
1119
|
+
const outputTokens = (usage.outputTokens || 0) + (usage.thinkingTokens || 0);
|
|
1120
|
+
const totalTokens = inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens;
|
|
1121
|
+
if (totalTokens === 0) continue;
|
|
1122
|
+
const sessionFilePath = settingsPath.replace(/\.settings\.json$/, ".jsonl");
|
|
1123
|
+
const sessionId = basename(settingsPath, ".settings.json");
|
|
1124
|
+
const meta = await readFactorySessionMeta(sessionFilePath);
|
|
1125
|
+
const timestamp = settings.providerLockTimestamp || meta.timestamp;
|
|
1126
|
+
if (!timestamp) continue;
|
|
1127
|
+
entries.push({
|
|
1128
|
+
date: toLocalDateString(timestamp),
|
|
1129
|
+
timestamp,
|
|
1130
|
+
rawModel,
|
|
1131
|
+
model: getModelDisplayName(rawModel),
|
|
1132
|
+
inputTokens,
|
|
1133
|
+
outputTokens,
|
|
1134
|
+
cacheWriteTokens,
|
|
1135
|
+
cacheReadTokens,
|
|
1136
|
+
cost: calculateKnownCost(rawModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens),
|
|
1137
|
+
messageCount: meta.messageCount,
|
|
1138
|
+
source: "claude",
|
|
1139
|
+
sourceKey: "factory",
|
|
1140
|
+
sessionId,
|
|
1141
|
+
sessionKind: meta.sessionKind,
|
|
1142
|
+
subagentId: meta.subagentId
|
|
1143
|
+
});
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return entries;
|
|
1148
|
+
}
|
|
1149
|
+
async function collectAnthropicUsageEntries(options = {}) {
|
|
1150
|
+
const [claudeEntries, opencodeEntries, factoryEntries] = await Promise.all([
|
|
1151
|
+
parseClaudeProjectEntries(options.projectFilter),
|
|
1152
|
+
parseOpenCodeEntries(),
|
|
1153
|
+
parseFactoryEntries()
|
|
1154
|
+
]);
|
|
1155
|
+
return [...claudeEntries, ...opencodeEntries, ...factoryEntries];
|
|
1156
|
+
}
|
|
1157
|
+
async function loadClaudeStatsFromJsonl() {
|
|
1158
|
+
const entries = await collectAnthropicUsageEntries();
|
|
1159
|
+
if (entries.length === 0) {
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
const modelUsage = /* @__PURE__ */ new Map();
|
|
1163
|
+
const dailyMap = /* @__PURE__ */ new Map();
|
|
1164
|
+
const sessionsByDate = /* @__PURE__ */ new Map();
|
|
1165
|
+
const mainSessions = /* @__PURE__ */ new Set();
|
|
1166
|
+
const subagentSessions = /* @__PURE__ */ new Set();
|
|
1167
|
+
const hourCounts = {};
|
|
1168
|
+
let firstTimestamp = null;
|
|
1169
|
+
let totalMessages = 0;
|
|
1170
|
+
for (const entry of entries) {
|
|
1171
|
+
const existing = modelUsage.get(entry.rawModel) ?? {
|
|
1172
|
+
inputTokens: 0,
|
|
1173
|
+
outputTokens: 0,
|
|
1174
|
+
cacheReadInputTokens: 0,
|
|
1175
|
+
cacheCreationInputTokens: 0,
|
|
1176
|
+
webSearchRequests: 0,
|
|
1177
|
+
costUSD: 0,
|
|
1178
|
+
contextWindow: 2e5
|
|
1179
|
+
};
|
|
1180
|
+
existing.inputTokens += entry.inputTokens;
|
|
1181
|
+
existing.outputTokens += entry.outputTokens;
|
|
1182
|
+
existing.cacheCreationInputTokens += entry.cacheWriteTokens;
|
|
1183
|
+
existing.cacheReadInputTokens += entry.cacheReadTokens;
|
|
1184
|
+
existing.costUSD += entry.cost;
|
|
1185
|
+
modelUsage.set(entry.rawModel, existing);
|
|
1186
|
+
const daily = dailyMap.get(entry.date) ?? { messageCount: 0, sessionCount: 0, toolCallCount: 0 };
|
|
1187
|
+
daily.messageCount += entry.messageCount;
|
|
1188
|
+
dailyMap.set(entry.date, daily);
|
|
1189
|
+
const dateSessions = sessionsByDate.get(entry.date) ?? /* @__PURE__ */ new Set();
|
|
1190
|
+
dateSessions.add(`${entry.sourceKey}:${entry.sessionId}`);
|
|
1191
|
+
sessionsByDate.set(entry.date, dateSessions);
|
|
1192
|
+
const hour = new Date(entry.timestamp).getHours().toString();
|
|
1193
|
+
hourCounts[hour] = (hourCounts[hour] || 0) + entry.messageCount;
|
|
1194
|
+
totalMessages += entry.messageCount;
|
|
1195
|
+
mainSessions.add(`${entry.sourceKey}:${entry.sessionId}`);
|
|
1196
|
+
if (entry.sessionKind === "subagent") {
|
|
1197
|
+
subagentSessions.add(`${entry.sourceKey}:${entry.subagentId || entry.sessionId}`);
|
|
1198
|
+
}
|
|
1199
|
+
if (!firstTimestamp || entry.timestamp < firstTimestamp) {
|
|
1200
|
+
firstTimestamp = entry.timestamp;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const dailyActivity = Array.from(dailyMap.entries()).map(([date, data]) => ({
|
|
1204
|
+
date,
|
|
1205
|
+
messageCount: data.messageCount,
|
|
1206
|
+
sessionCount: sessionsByDate.get(date)?.size || 0,
|
|
1207
|
+
toolCallCount: data.toolCallCount
|
|
1208
|
+
})).sort((a, b) => a.date.localeCompare(b.date));
|
|
1209
|
+
const sessionCounts = {
|
|
1210
|
+
main: mainSessions.size,
|
|
1211
|
+
subagent: subagentSessions.size,
|
|
1212
|
+
total: mainSessions.size + subagentSessions.size
|
|
1213
|
+
};
|
|
1214
|
+
return {
|
|
1215
|
+
version: 1,
|
|
1216
|
+
lastComputedDate: toLocalDateString((/* @__PURE__ */ new Date()).toISOString()),
|
|
1217
|
+
dailyActivity,
|
|
1218
|
+
dailyModelTokens: [],
|
|
1219
|
+
modelUsage: Object.fromEntries(modelUsage.entries()),
|
|
1220
|
+
totalSessions: sessionCounts.main,
|
|
1221
|
+
totalMessages,
|
|
1222
|
+
longestSession: {
|
|
1223
|
+
sessionId: "",
|
|
1224
|
+
duration: 0,
|
|
1225
|
+
messageCount: 0,
|
|
1226
|
+
timestamp: ""
|
|
1227
|
+
},
|
|
1228
|
+
firstSessionDate: firstTimestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1229
|
+
hourCounts,
|
|
1230
|
+
sessionCounts
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/usage/loader.ts
|
|
1235
|
+
var MAX_RECURSION_DEPTH = 10;
|
|
1236
|
+
function toLocalDateString2(isoTimestamp) {
|
|
1237
|
+
const date = new Date(isoTimestamp);
|
|
1238
|
+
const year = date.getFullYear();
|
|
1239
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1240
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1241
|
+
return `${year}-${month}-${day}`;
|
|
1242
|
+
}
|
|
1243
|
+
function getCodexDir() {
|
|
1244
|
+
return process.env.CODEX_HOME || join2(homedir2(), ".codex");
|
|
1245
|
+
}
|
|
1246
|
+
async function pathExists2(path) {
|
|
1247
|
+
try {
|
|
1248
|
+
await fs2.access(path);
|
|
1249
|
+
return true;
|
|
1250
|
+
} catch {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async function safeRealpath2(path) {
|
|
1255
|
+
try {
|
|
1256
|
+
return await fs2.realpath(path);
|
|
1257
|
+
} catch {
|
|
1258
|
+
return path;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
async function findJsonlFiles(dir, visited = /* @__PURE__ */ new Set(), depth = 0, result = []) {
|
|
1262
|
+
if (!await pathExists2(dir)) return result;
|
|
1263
|
+
if (depth > MAX_RECURSION_DEPTH) return result;
|
|
1264
|
+
const realPath = await safeRealpath2(dir);
|
|
1265
|
+
if (visited.has(realPath)) return result;
|
|
1266
|
+
visited.add(realPath);
|
|
1267
|
+
let entries;
|
|
1268
|
+
try {
|
|
1269
|
+
entries = await fs2.readdir(dir);
|
|
1270
|
+
} catch {
|
|
1271
|
+
return result;
|
|
1272
|
+
}
|
|
1273
|
+
for (const entry of entries) {
|
|
1274
|
+
const fullPath = join2(dir, entry);
|
|
1275
|
+
let stat;
|
|
1276
|
+
try {
|
|
1277
|
+
stat = await fs2.stat(fullPath);
|
|
1278
|
+
} catch {
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
if (stat.isDirectory()) {
|
|
1282
|
+
await findJsonlFiles(fullPath, visited, depth + 1, result);
|
|
1283
|
+
} else if (entry.endsWith(".jsonl")) {
|
|
1284
|
+
result.push(fullPath);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return result;
|
|
1288
|
+
}
|
|
1289
|
+
async function parseClaudeJsonl(projectFilter) {
|
|
1290
|
+
const entries = await collectAnthropicUsageEntries({ projectFilter });
|
|
1291
|
+
return entries.map((entry) => ({
|
|
1292
|
+
date: entry.date,
|
|
1293
|
+
model: entry.model,
|
|
1294
|
+
rawModel: entry.rawModel,
|
|
1295
|
+
inputTokens: entry.inputTokens,
|
|
1296
|
+
outputTokens: entry.outputTokens,
|
|
1297
|
+
cacheWriteTokens: entry.cacheWriteTokens,
|
|
1298
|
+
cacheReadTokens: entry.cacheReadTokens,
|
|
1299
|
+
cost: entry.cost,
|
|
1300
|
+
messageCount: entry.messageCount,
|
|
1301
|
+
source: entry.source,
|
|
1302
|
+
sourceKey: entry.sourceKey,
|
|
1303
|
+
sessionId: entry.sessionId,
|
|
1304
|
+
sessionKind: entry.sessionKind,
|
|
1305
|
+
subagentId: entry.subagentId,
|
|
1306
|
+
timestamp: entry.timestamp
|
|
1307
|
+
}));
|
|
1308
|
+
}
|
|
916
1309
|
async function parseCodexJsonl() {
|
|
917
1310
|
const entries = [];
|
|
918
1311
|
const codexDir = getCodexDir();
|
|
919
|
-
const sessionsDir =
|
|
920
|
-
const archivedDir =
|
|
1312
|
+
const sessionsDir = join2(codexDir, "sessions");
|
|
1313
|
+
const archivedDir = join2(codexDir, "archived_sessions");
|
|
921
1314
|
const [sessionFiles, archivedFiles] = await Promise.all([
|
|
922
1315
|
findJsonlFiles(sessionsDir),
|
|
923
1316
|
findJsonlFiles(archivedDir)
|
|
@@ -926,10 +1319,10 @@ async function parseCodexJsonl() {
|
|
|
926
1319
|
if (jsonlFiles.length === 0) return entries;
|
|
927
1320
|
for (const filePath of jsonlFiles) {
|
|
928
1321
|
try {
|
|
929
|
-
const content = await
|
|
1322
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
930
1323
|
const lines = content.split("\n");
|
|
931
1324
|
let currentModel = "gpt-5";
|
|
932
|
-
let fileSessionId =
|
|
1325
|
+
let fileSessionId = basename2(filePath, ".jsonl");
|
|
933
1326
|
let canonicalSessionId = fileSessionId;
|
|
934
1327
|
let sessionKind = "main";
|
|
935
1328
|
for (const line of lines) {
|
|
@@ -955,7 +1348,7 @@ async function parseCodexJsonl() {
|
|
|
955
1348
|
const usage = info.last_token_usage;
|
|
956
1349
|
const timestamp = entry.timestamp;
|
|
957
1350
|
if (!timestamp) continue;
|
|
958
|
-
const date =
|
|
1351
|
+
const date = toLocalDateString2(timestamp);
|
|
959
1352
|
const pricing = getCodexModelPricing(currentModel);
|
|
960
1353
|
const inputTokens = usage.input_tokens || 0;
|
|
961
1354
|
const outputTokens = usage.output_tokens || 0;
|
|
@@ -989,11 +1382,12 @@ function computeSessionCounts(entries) {
|
|
|
989
1382
|
const mainSessions = /* @__PURE__ */ new Set();
|
|
990
1383
|
const subagentSessions = /* @__PURE__ */ new Set();
|
|
991
1384
|
for (const entry of entries) {
|
|
1385
|
+
const sourceKey = entry.sourceKey || entry.source;
|
|
992
1386
|
if (entry.sessionId) {
|
|
993
|
-
mainSessions.add(`${
|
|
1387
|
+
mainSessions.add(`${sourceKey}:${entry.sessionId}`);
|
|
994
1388
|
}
|
|
995
1389
|
if (entry.sessionKind === "subagent" && entry.subagentId) {
|
|
996
|
-
subagentSessions.add(`${
|
|
1390
|
+
subagentSessions.add(`${sourceKey}:${entry.subagentId}`);
|
|
997
1391
|
}
|
|
998
1392
|
}
|
|
999
1393
|
return {
|
|
@@ -1032,13 +1426,14 @@ function sortModelsByTier(models) {
|
|
|
1032
1426
|
function aggregateByDay(entries) {
|
|
1033
1427
|
const dayMap = /* @__PURE__ */ new Map();
|
|
1034
1428
|
for (const e of entries) {
|
|
1429
|
+
const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
1035
1430
|
const existing = dayMap.get(e.date);
|
|
1036
1431
|
if (existing) {
|
|
1037
1432
|
existing.inputTokens += e.inputTokens;
|
|
1038
1433
|
existing.outputTokens += e.outputTokens;
|
|
1039
1434
|
existing.cacheWriteTokens += e.cacheWriteTokens;
|
|
1040
1435
|
existing.cacheReadTokens += e.cacheReadTokens;
|
|
1041
|
-
existing.totalTokens +=
|
|
1436
|
+
existing.totalTokens += entryTotal;
|
|
1042
1437
|
existing.cost += e.cost;
|
|
1043
1438
|
existing.modelsSet.add(e.model);
|
|
1044
1439
|
} else {
|
|
@@ -1048,7 +1443,7 @@ function aggregateByDay(entries) {
|
|
|
1048
1443
|
outputTokens: e.outputTokens,
|
|
1049
1444
|
cacheWriteTokens: e.cacheWriteTokens,
|
|
1050
1445
|
cacheReadTokens: e.cacheReadTokens,
|
|
1051
|
-
totalTokens:
|
|
1446
|
+
totalTokens: entryTotal,
|
|
1052
1447
|
cost: e.cost,
|
|
1053
1448
|
modelsSet: /* @__PURE__ */ new Set([e.model])
|
|
1054
1449
|
});
|
|
@@ -1063,13 +1458,14 @@ function aggregateByMonth(entries) {
|
|
|
1063
1458
|
const monthMap = /* @__PURE__ */ new Map();
|
|
1064
1459
|
for (const e of entries) {
|
|
1065
1460
|
const month = e.date.slice(0, 7);
|
|
1461
|
+
const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
1066
1462
|
const existing = monthMap.get(month);
|
|
1067
1463
|
if (existing) {
|
|
1068
1464
|
existing.inputTokens += e.inputTokens;
|
|
1069
1465
|
existing.outputTokens += e.outputTokens;
|
|
1070
1466
|
existing.cacheWriteTokens += e.cacheWriteTokens;
|
|
1071
1467
|
existing.cacheReadTokens += e.cacheReadTokens;
|
|
1072
|
-
existing.totalTokens +=
|
|
1468
|
+
existing.totalTokens += entryTotal;
|
|
1073
1469
|
existing.cost += e.cost;
|
|
1074
1470
|
existing.modelsSet.add(e.model);
|
|
1075
1471
|
} else {
|
|
@@ -1079,7 +1475,7 @@ function aggregateByMonth(entries) {
|
|
|
1079
1475
|
outputTokens: e.outputTokens,
|
|
1080
1476
|
cacheWriteTokens: e.cacheWriteTokens,
|
|
1081
1477
|
cacheReadTokens: e.cacheReadTokens,
|
|
1082
|
-
totalTokens:
|
|
1478
|
+
totalTokens: entryTotal,
|
|
1083
1479
|
cost: e.cost,
|
|
1084
1480
|
modelsSet: /* @__PURE__ */ new Set([e.model])
|
|
1085
1481
|
});
|
|
@@ -1093,13 +1489,14 @@ function aggregateByMonth(entries) {
|
|
|
1093
1489
|
function aggregateByModel(entries) {
|
|
1094
1490
|
const modelMap = /* @__PURE__ */ new Map();
|
|
1095
1491
|
for (const e of entries) {
|
|
1492
|
+
const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
1096
1493
|
const existing = modelMap.get(e.model);
|
|
1097
1494
|
if (existing) {
|
|
1098
1495
|
existing.inputTokens += e.inputTokens;
|
|
1099
1496
|
existing.outputTokens += e.outputTokens;
|
|
1100
1497
|
existing.cacheWriteTokens += e.cacheWriteTokens;
|
|
1101
1498
|
existing.cacheReadTokens += e.cacheReadTokens;
|
|
1102
|
-
existing.totalTokens +=
|
|
1499
|
+
existing.totalTokens += entryTotal;
|
|
1103
1500
|
existing.cost += e.cost;
|
|
1104
1501
|
} else {
|
|
1105
1502
|
modelMap.set(e.model, {
|
|
@@ -1108,7 +1505,7 @@ function aggregateByModel(entries) {
|
|
|
1108
1505
|
outputTokens: e.outputTokens,
|
|
1109
1506
|
cacheWriteTokens: e.cacheWriteTokens,
|
|
1110
1507
|
cacheReadTokens: e.cacheReadTokens,
|
|
1111
|
-
totalTokens:
|
|
1508
|
+
totalTokens: entryTotal,
|
|
1112
1509
|
cost: e.cost
|
|
1113
1510
|
});
|
|
1114
1511
|
}
|
|
@@ -1118,14 +1515,16 @@ function aggregateByModel(entries) {
|
|
|
1118
1515
|
function aggregateBySession(entries) {
|
|
1119
1516
|
const sessionMap = /* @__PURE__ */ new Map();
|
|
1120
1517
|
for (const e of entries) {
|
|
1121
|
-
const
|
|
1518
|
+
const sourceKey = e.sourceKey || e.source;
|
|
1519
|
+
const sid = e.sessionId ? `${sourceKey}:${e.sessionId}` : "unknown";
|
|
1520
|
+
const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
|
|
1122
1521
|
const existing = sessionMap.get(sid);
|
|
1123
1522
|
if (existing) {
|
|
1124
1523
|
existing.inputTokens += e.inputTokens;
|
|
1125
1524
|
existing.outputTokens += e.outputTokens;
|
|
1126
1525
|
existing.cacheWriteTokens += e.cacheWriteTokens;
|
|
1127
1526
|
existing.cacheReadTokens += e.cacheReadTokens;
|
|
1128
|
-
existing.totalTokens +=
|
|
1527
|
+
existing.totalTokens += entryTotal;
|
|
1129
1528
|
existing.cost += e.cost;
|
|
1130
1529
|
existing.modelsSet.add(e.model);
|
|
1131
1530
|
if (e.timestamp && e.timestamp < existing.firstTimestamp) {
|
|
@@ -1139,14 +1538,14 @@ function aggregateBySession(entries) {
|
|
|
1139
1538
|
outputTokens: e.outputTokens,
|
|
1140
1539
|
cacheWriteTokens: e.cacheWriteTokens,
|
|
1141
1540
|
cacheReadTokens: e.cacheReadTokens,
|
|
1142
|
-
totalTokens:
|
|
1541
|
+
totalTokens: entryTotal,
|
|
1143
1542
|
cost: e.cost,
|
|
1144
1543
|
firstTimestamp: e.timestamp || ""
|
|
1145
1544
|
});
|
|
1146
1545
|
}
|
|
1147
1546
|
}
|
|
1148
1547
|
return Array.from(sessionMap.entries()).filter(([, data]) => data.totalTokens > 0).map(([sessionId, data]) => {
|
|
1149
|
-
const date = data.firstTimestamp ?
|
|
1548
|
+
const date = data.firstTimestamp ? toLocalDateString2(data.firstTimestamp) : "unknown";
|
|
1150
1549
|
const shortId = data.displaySessionId.slice(0, 12);
|
|
1151
1550
|
return {
|
|
1152
1551
|
key: `${date} ${shortId}`,
|
|
@@ -1506,180 +1905,6 @@ function displayTotalOnly(stats, options = {}) {
|
|
|
1506
1905
|
console.log();
|
|
1507
1906
|
}
|
|
1508
1907
|
|
|
1509
|
-
// src/claude-jsonl-loader.ts
|
|
1510
|
-
import { promises as fs2 } from "fs";
|
|
1511
|
-
import { homedir as homedir2 } from "os";
|
|
1512
|
-
import { basename as basename2, join as join2 } from "path";
|
|
1513
|
-
function getClaudeDir2() {
|
|
1514
|
-
return process.env.CLAUDE_HOME || join2(homedir2(), ".claude");
|
|
1515
|
-
}
|
|
1516
|
-
function toLocalDateString2(isoTimestamp) {
|
|
1517
|
-
const date = new Date(isoTimestamp);
|
|
1518
|
-
const year = date.getFullYear();
|
|
1519
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1520
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
1521
|
-
return `${year}-${month}-${day}`;
|
|
1522
|
-
}
|
|
1523
|
-
async function pathExists2(path) {
|
|
1524
|
-
try {
|
|
1525
|
-
await fs2.access(path);
|
|
1526
|
-
return true;
|
|
1527
|
-
} catch {
|
|
1528
|
-
return false;
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
async function claudeJsonlDataExists() {
|
|
1532
|
-
const projectsDir = join2(getClaudeDir2(), "projects");
|
|
1533
|
-
return pathExists2(projectsDir);
|
|
1534
|
-
}
|
|
1535
|
-
async function findJsonlFiles2(dir) {
|
|
1536
|
-
const files = [];
|
|
1537
|
-
if (!await pathExists2(dir)) return files;
|
|
1538
|
-
let entries;
|
|
1539
|
-
try {
|
|
1540
|
-
entries = await fs2.readdir(dir);
|
|
1541
|
-
} catch {
|
|
1542
|
-
return files;
|
|
1543
|
-
}
|
|
1544
|
-
for (const entry of entries) {
|
|
1545
|
-
const fullPath = join2(dir, entry);
|
|
1546
|
-
try {
|
|
1547
|
-
const stat = await fs2.stat(fullPath);
|
|
1548
|
-
if (stat.isDirectory()) {
|
|
1549
|
-
files.push(...await findJsonlFiles2(fullPath));
|
|
1550
|
-
} else if (entry.endsWith(".jsonl")) {
|
|
1551
|
-
files.push(fullPath);
|
|
1552
|
-
}
|
|
1553
|
-
} catch {
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
return files;
|
|
1557
|
-
}
|
|
1558
|
-
async function loadClaudeStatsFromJsonl() {
|
|
1559
|
-
const claudeDir = getClaudeDir2();
|
|
1560
|
-
const projectsDir = join2(claudeDir, "projects");
|
|
1561
|
-
if (!await claudeJsonlDataExists()) {
|
|
1562
|
-
return null;
|
|
1563
|
-
}
|
|
1564
|
-
const jsonlFiles = await findJsonlFiles2(projectsDir);
|
|
1565
|
-
if (jsonlFiles.length === 0) {
|
|
1566
|
-
return null;
|
|
1567
|
-
}
|
|
1568
|
-
const modelUsage = {};
|
|
1569
|
-
const dailyMap = /* @__PURE__ */ new Map();
|
|
1570
|
-
const sessionsByDate = /* @__PURE__ */ new Map();
|
|
1571
|
-
const mainSessions = /* @__PURE__ */ new Set();
|
|
1572
|
-
const subagentSessions = /* @__PURE__ */ new Set();
|
|
1573
|
-
const hourCounts = {};
|
|
1574
|
-
let firstTimestamp = null;
|
|
1575
|
-
let totalMessages = 0;
|
|
1576
|
-
const messageIds = /* @__PURE__ */ new Set();
|
|
1577
|
-
for (const filePath of jsonlFiles) {
|
|
1578
|
-
try {
|
|
1579
|
-
const content = await fs2.readFile(filePath, "utf-8");
|
|
1580
|
-
const lines = content.split("\n");
|
|
1581
|
-
const fallbackSessionId = basename2(filePath, ".jsonl");
|
|
1582
|
-
const isSubagentFile = filePath.includes("/subagents/");
|
|
1583
|
-
for (const line of lines) {
|
|
1584
|
-
if (!line.trim()) continue;
|
|
1585
|
-
try {
|
|
1586
|
-
const entry = JSON.parse(line);
|
|
1587
|
-
if (entry.type !== "assistant") continue;
|
|
1588
|
-
if (!entry.message?.usage) continue;
|
|
1589
|
-
const usage = entry.message.usage;
|
|
1590
|
-
const model = entry.message.model || "unknown";
|
|
1591
|
-
const timestamp = entry.timestamp;
|
|
1592
|
-
const messageId = entry.message.id;
|
|
1593
|
-
const sessionId = entry.sessionId || fallbackSessionId;
|
|
1594
|
-
const sessionKind = entry.isSidechain === true || isSubagentFile ? "subagent" : "main";
|
|
1595
|
-
if (!modelUsage[model]) {
|
|
1596
|
-
modelUsage[model] = {
|
|
1597
|
-
inputTokens: 0,
|
|
1598
|
-
outputTokens: 0,
|
|
1599
|
-
cacheReadInputTokens: 0,
|
|
1600
|
-
cacheCreationInputTokens: 0
|
|
1601
|
-
};
|
|
1602
|
-
}
|
|
1603
|
-
modelUsage[model].inputTokens += usage.input_tokens || 0;
|
|
1604
|
-
modelUsage[model].outputTokens += usage.output_tokens || 0;
|
|
1605
|
-
modelUsage[model].cacheReadInputTokens += usage.cache_read_input_tokens || 0;
|
|
1606
|
-
modelUsage[model].cacheCreationInputTokens += usage.cache_creation_input_tokens || 0;
|
|
1607
|
-
if (messageId) {
|
|
1608
|
-
messageIds.add(messageId);
|
|
1609
|
-
}
|
|
1610
|
-
if (timestamp) {
|
|
1611
|
-
const date = toLocalDateString2(timestamp);
|
|
1612
|
-
const hour = new Date(timestamp).getHours().toString();
|
|
1613
|
-
if (!dailyMap.has(date)) {
|
|
1614
|
-
dailyMap.set(date, { messageCount: 0, sessionCount: 0, toolCallCount: 0 });
|
|
1615
|
-
}
|
|
1616
|
-
const daily = dailyMap.get(date);
|
|
1617
|
-
daily.messageCount++;
|
|
1618
|
-
const dateSessions = sessionsByDate.get(date) || /* @__PURE__ */ new Set();
|
|
1619
|
-
dateSessions.add(sessionId);
|
|
1620
|
-
sessionsByDate.set(date, dateSessions);
|
|
1621
|
-
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
|
|
1622
|
-
}
|
|
1623
|
-
mainSessions.add(sessionId);
|
|
1624
|
-
if (sessionKind === "subagent") {
|
|
1625
|
-
subagentSessions.add(filePath);
|
|
1626
|
-
}
|
|
1627
|
-
if (timestamp && (!firstTimestamp || timestamp < firstTimestamp)) {
|
|
1628
|
-
firstTimestamp = timestamp;
|
|
1629
|
-
}
|
|
1630
|
-
} catch {
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
} catch {
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
totalMessages = messageIds.size;
|
|
1637
|
-
if (totalMessages === 0) {
|
|
1638
|
-
return null;
|
|
1639
|
-
}
|
|
1640
|
-
const statsCacheModelUsage = {};
|
|
1641
|
-
for (const [model, usage] of Object.entries(modelUsage)) {
|
|
1642
|
-
statsCacheModelUsage[model] = {
|
|
1643
|
-
inputTokens: usage.inputTokens,
|
|
1644
|
-
outputTokens: usage.outputTokens,
|
|
1645
|
-
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
1646
|
-
cacheCreationInputTokens: usage.cacheCreationInputTokens,
|
|
1647
|
-
webSearchRequests: 0,
|
|
1648
|
-
costUSD: 0,
|
|
1649
|
-
contextWindow: 2e5
|
|
1650
|
-
};
|
|
1651
|
-
}
|
|
1652
|
-
const dailyActivity = Array.from(dailyMap.entries()).map(([date, data]) => ({
|
|
1653
|
-
date,
|
|
1654
|
-
messageCount: data.messageCount,
|
|
1655
|
-
sessionCount: sessionsByDate.get(date)?.size || 0,
|
|
1656
|
-
toolCallCount: data.toolCallCount
|
|
1657
|
-
})).sort((a, b) => a.date.localeCompare(b.date));
|
|
1658
|
-
const sessionCounts = {
|
|
1659
|
-
main: mainSessions.size,
|
|
1660
|
-
subagent: subagentSessions.size,
|
|
1661
|
-
total: mainSessions.size + subagentSessions.size
|
|
1662
|
-
};
|
|
1663
|
-
return {
|
|
1664
|
-
version: 1,
|
|
1665
|
-
lastComputedDate: toLocalDateString2((/* @__PURE__ */ new Date()).toISOString()),
|
|
1666
|
-
dailyActivity,
|
|
1667
|
-
dailyModelTokens: [],
|
|
1668
|
-
modelUsage: statsCacheModelUsage,
|
|
1669
|
-
totalSessions: sessionCounts.main,
|
|
1670
|
-
totalMessages,
|
|
1671
|
-
longestSession: {
|
|
1672
|
-
sessionId: "",
|
|
1673
|
-
duration: 0,
|
|
1674
|
-
messageCount: 0,
|
|
1675
|
-
timestamp: ""
|
|
1676
|
-
},
|
|
1677
|
-
firstSessionDate: firstTimestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1678
|
-
hourCounts,
|
|
1679
|
-
sessionCounts
|
|
1680
|
-
};
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
1908
|
// src/codex-loader.ts
|
|
1684
1909
|
import { promises as fs3 } from "fs";
|
|
1685
1910
|
import { homedir as homedir3 } from "os";
|
|
@@ -1709,7 +1934,7 @@ async function codexDataExists() {
|
|
|
1709
1934
|
const archivedDir = join3(codexDir, "archived_sessions");
|
|
1710
1935
|
return await pathExists3(sessionsDir) || await pathExists3(archivedDir);
|
|
1711
1936
|
}
|
|
1712
|
-
async function
|
|
1937
|
+
async function findJsonlFiles2(dir) {
|
|
1713
1938
|
const files = [];
|
|
1714
1939
|
if (!await pathExists3(dir)) return files;
|
|
1715
1940
|
let entries;
|
|
@@ -1727,7 +1952,7 @@ async function findJsonlFiles3(dir) {
|
|
|
1727
1952
|
continue;
|
|
1728
1953
|
}
|
|
1729
1954
|
if (stat.isDirectory()) {
|
|
1730
|
-
files.push(...await
|
|
1955
|
+
files.push(...await findJsonlFiles2(fullPath));
|
|
1731
1956
|
} else if (entry.endsWith(".jsonl")) {
|
|
1732
1957
|
files.push(fullPath);
|
|
1733
1958
|
}
|
|
@@ -1822,8 +2047,8 @@ async function loadCodexStats() {
|
|
|
1822
2047
|
const sessionsDir = join3(codexDir, "sessions");
|
|
1823
2048
|
const archivedDir = join3(codexDir, "archived_sessions");
|
|
1824
2049
|
const [sessionFiles, archivedFiles] = await Promise.all([
|
|
1825
|
-
|
|
1826
|
-
|
|
2050
|
+
findJsonlFiles2(sessionsDir),
|
|
2051
|
+
findJsonlFiles2(archivedDir)
|
|
1827
2052
|
]);
|
|
1828
2053
|
const jsonlFiles = [...sessionFiles, ...archivedFiles];
|
|
1829
2054
|
if (jsonlFiles.length === 0) {
|
|
@@ -1920,7 +2145,7 @@ async function loadData(options) {
|
|
|
1920
2145
|
let claude = null;
|
|
1921
2146
|
let codex = null;
|
|
1922
2147
|
if (!codexOnly) {
|
|
1923
|
-
if (await
|
|
2148
|
+
if (await claudeCompatibleDataExists()) {
|
|
1924
2149
|
claude = await loadClaudeStatsFromJsonl();
|
|
1925
2150
|
}
|
|
1926
2151
|
}
|
|
@@ -1941,10 +2166,10 @@ function validateData(data, options) {
|
|
|
1941
2166
|
console.error("Make sure you have used the Codex CLI at least once.");
|
|
1942
2167
|
} else if (options.combined) {
|
|
1943
2168
|
console.error("Error: No usage data found");
|
|
1944
|
-
console.error("Make sure you have used Claude Code or Codex
|
|
2169
|
+
console.error("Make sure you have used a supported local CLI source such as Claude Code, OpenCode, Droid, or Codex at least once.");
|
|
1945
2170
|
} else {
|
|
1946
|
-
console.error("Error: Claude
|
|
1947
|
-
console.error("
|
|
2171
|
+
console.error("Error: No Claude-compatible local data found");
|
|
2172
|
+
console.error("Checked ~/.claude, ~/.local/share/opencode/opencode.db, and ~/.factory/sessions.");
|
|
1948
2173
|
}
|
|
1949
2174
|
process.exit(1);
|
|
1950
2175
|
}
|
|
@@ -1958,7 +2183,10 @@ function toLocalDateString4(date) {
|
|
|
1958
2183
|
return `${year}-${month}-${day}`;
|
|
1959
2184
|
}
|
|
1960
2185
|
function calculateModelCost(modelName, usage) {
|
|
1961
|
-
const pricing =
|
|
2186
|
+
const pricing = tryGetModelPricing(modelName);
|
|
2187
|
+
if (!pricing) {
|
|
2188
|
+
return 0;
|
|
2189
|
+
}
|
|
1962
2190
|
const inputCost = usage.inputTokens / 1e6 * pricing.input;
|
|
1963
2191
|
const outputCost = usage.outputTokens / 1e6 * pricing.output;
|
|
1964
2192
|
const cacheWriteCost = usage.cacheCreationInputTokens / 1e6 * pricing.cacheWrite;
|
|
@@ -2062,34 +2290,39 @@ function shouldExcludeModel(modelName) {
|
|
|
2062
2290
|
const lower = modelName.toLowerCase();
|
|
2063
2291
|
return EXCLUDED_MODELS.some((excluded) => lower.includes(excluded));
|
|
2064
2292
|
}
|
|
2065
|
-
function
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2293
|
+
function getUsageTokens(usage) {
|
|
2294
|
+
return usage.inputTokens + usage.outputTokens + usage.cacheCreationInputTokens + usage.cacheReadInputTokens;
|
|
2295
|
+
}
|
|
2296
|
+
function getDisplayModelTokens(modelUsage) {
|
|
2297
|
+
const aggregated = /* @__PURE__ */ new Map();
|
|
2069
2298
|
for (const [modelName, usage] of Object.entries(modelUsage)) {
|
|
2070
2299
|
if (shouldExcludeModel(modelName)) continue;
|
|
2071
|
-
const
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
percentage: Math.round(modelTokens / totalTokens * 100),
|
|
2075
|
-
tokens: modelTokens
|
|
2076
|
-
});
|
|
2300
|
+
const displayName = getModelDisplayName(modelName);
|
|
2301
|
+
const modelTokens = getUsageTokens(usage);
|
|
2302
|
+
aggregated.set(displayName, (aggregated.get(displayName) || 0) + modelTokens);
|
|
2077
2303
|
}
|
|
2078
|
-
|
|
2079
|
-
|
|
2304
|
+
return Array.from(aggregated.entries()).map(([model, tokens]) => ({ model, tokens })).sort((a, b) => b.tokens - a.tokens);
|
|
2305
|
+
}
|
|
2306
|
+
function getModelBreakdown(modelUsage) {
|
|
2307
|
+
const breakdown = getDisplayModelTokens(modelUsage);
|
|
2308
|
+
const totalTokens = breakdown.reduce((sum, entry) => sum + entry.tokens, 0);
|
|
2309
|
+
if (totalTokens === 0) return [];
|
|
2310
|
+
return breakdown.map(({ model, tokens }) => ({
|
|
2311
|
+
model,
|
|
2312
|
+
percentage: Math.round(tokens / totalTokens * 100)
|
|
2313
|
+
}));
|
|
2080
2314
|
}
|
|
2081
2315
|
function getFavoriteModel(modelUsage) {
|
|
2082
|
-
|
|
2083
|
-
|
|
2316
|
+
return getDisplayModelTokens(modelUsage)[0]?.model || "";
|
|
2317
|
+
}
|
|
2318
|
+
function getCodexDisplayModelTokens(modelUsage) {
|
|
2319
|
+
const aggregated = /* @__PURE__ */ new Map();
|
|
2084
2320
|
for (const [modelName, usage] of Object.entries(modelUsage)) {
|
|
2085
|
-
|
|
2086
|
-
const modelTokens = usage.inputTokens + usage.outputTokens
|
|
2087
|
-
|
|
2088
|
-
maxTokens = modelTokens;
|
|
2089
|
-
favoriteModel = modelName;
|
|
2090
|
-
}
|
|
2321
|
+
const displayName = getCodexModelDisplayName(modelName);
|
|
2322
|
+
const modelTokens = usage.inputTokens + usage.outputTokens;
|
|
2323
|
+
aggregated.set(displayName, (aggregated.get(displayName) || 0) + modelTokens);
|
|
2091
2324
|
}
|
|
2092
|
-
return
|
|
2325
|
+
return Array.from(aggregated.entries()).map(([model, tokens]) => ({ model, tokens })).sort((a, b) => b.tokens - a.tokens);
|
|
2093
2326
|
}
|
|
2094
2327
|
function calculateWordsGenerated(modelUsage) {
|
|
2095
2328
|
let totalOutputTokens = 0;
|
|
@@ -2196,23 +2429,11 @@ function computeCodexWrappedStats(cache) {
|
|
|
2196
2429
|
busiestMonth = month;
|
|
2197
2430
|
}
|
|
2198
2431
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
breakdown.push({
|
|
2205
|
-
model: getCodexModelDisplayName(modelName),
|
|
2206
|
-
percentage: totalTokens > 0 ? Math.round(modelTokens / totalTokens * 100) : 0,
|
|
2207
|
-
tokens: modelTokens
|
|
2208
|
-
});
|
|
2209
|
-
if (modelTokens > maxModelTokens) {
|
|
2210
|
-
maxModelTokens = modelTokens;
|
|
2211
|
-
favoriteModel = modelName;
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
breakdown.sort((a, b) => b.tokens - a.tokens);
|
|
2215
|
-
const modelBreakdown = breakdown.map(({ model, percentage }) => ({ model, percentage }));
|
|
2432
|
+
const breakdown = getCodexDisplayModelTokens(cache.modelUsage);
|
|
2433
|
+
const modelBreakdown = breakdown.map(({ model, tokens }) => ({
|
|
2434
|
+
model,
|
|
2435
|
+
percentage: totalTokens > 0 ? Math.round(tokens / totalTokens * 100) : 0
|
|
2436
|
+
}));
|
|
2216
2437
|
const wordsGenerated = Math.round(totalOutputTokens * 0.75);
|
|
2217
2438
|
return {
|
|
2218
2439
|
sessions: cache.totalSessions,
|
|
@@ -2225,7 +2446,7 @@ function computeCodexWrappedStats(cache) {
|
|
|
2225
2446
|
peakHour,
|
|
2226
2447
|
peakDay,
|
|
2227
2448
|
busiestMonth,
|
|
2228
|
-
favoriteModel:
|
|
2449
|
+
favoriteModel: breakdown[0]?.model || "",
|
|
2229
2450
|
modelBreakdown,
|
|
2230
2451
|
wordsGenerated,
|
|
2231
2452
|
firstSessionDate: cache.firstSessionDate,
|
|
@@ -2491,7 +2712,7 @@ async function publishArtifact(artifact, baseUrl, legacyUrl) {
|
|
|
2491
2712
|
import { promises as fs4 } from "fs";
|
|
2492
2713
|
import { homedir as homedir4 } from "os";
|
|
2493
2714
|
import { join as join4 } from "path";
|
|
2494
|
-
function
|
|
2715
|
+
function getClaudeDir2() {
|
|
2495
2716
|
return process.env.CLAUDE_HOME || join4(homedir4(), ".claude");
|
|
2496
2717
|
}
|
|
2497
2718
|
async function pathExists4(path) {
|
|
@@ -2555,7 +2776,7 @@ async function inspectClaudeSystem(home = homedir4()) {
|
|
|
2555
2776
|
projectCount: json?.projects ? Object.keys(json.projects).length : void 0
|
|
2556
2777
|
};
|
|
2557
2778
|
}
|
|
2558
|
-
async function inspectClaudeUsage(claudeDir =
|
|
2779
|
+
async function inspectClaudeUsage(claudeDir = getClaudeDir2()) {
|
|
2559
2780
|
const statsCachePath = join4(claudeDir, "stats-cache.json");
|
|
2560
2781
|
const statsCache = await readJsonFile(statsCachePath);
|
|
2561
2782
|
const facetsDir = join4(claudeDir, "usage-data", "facets");
|
|
@@ -3060,9 +3281,9 @@ async function runUsage(args, config) {
|
|
|
3060
3281
|
if (args.codex) {
|
|
3061
3282
|
console.error("Checked: ~/.codex/sessions and ~/.codex/archived_sessions");
|
|
3062
3283
|
} else if (args.combined) {
|
|
3063
|
-
console.error("Checked: ~/.claude/projects and ~/.codex/sessions (plus archived sessions)");
|
|
3284
|
+
console.error("Checked: ~/.claude/projects, ~/.local/share/opencode/opencode.db, ~/.factory/sessions, and ~/.codex/sessions (plus archived sessions)");
|
|
3064
3285
|
} else {
|
|
3065
|
-
console.error("Checked: ~/.claude/projects");
|
|
3286
|
+
console.error("Checked: ~/.claude/projects, ~/.local/share/opencode/opencode.db, and ~/.factory/sessions");
|
|
3066
3287
|
}
|
|
3067
3288
|
console.error("Try widening the range or removing the date filter.");
|
|
3068
3289
|
process.exit(1);
|
|
@@ -3072,10 +3293,10 @@ async function runUsage(args, config) {
|
|
|
3072
3293
|
console.error("Make sure you have used the Codex CLI at least once.");
|
|
3073
3294
|
} else if (args.combined) {
|
|
3074
3295
|
console.error("Error: No usage data found");
|
|
3075
|
-
console.error("Make sure you have used Claude Code or Codex
|
|
3296
|
+
console.error("Make sure you have used a supported local CLI source such as Claude Code, OpenCode, Droid, or Codex at least once.");
|
|
3076
3297
|
} else {
|
|
3077
|
-
console.error("Error: Claude
|
|
3078
|
-
console.error("
|
|
3298
|
+
console.error("Error: No Claude-compatible local data found");
|
|
3299
|
+
console.error("Checked ~/.claude, ~/.local/share/opencode/opencode.db, and ~/.factory/sessions.");
|
|
3079
3300
|
}
|
|
3080
3301
|
process.exit(1);
|
|
3081
3302
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibestats",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.9",
|
|
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",
|
|
@@ -18,12 +18,6 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
|
-
"scripts": {
|
|
22
|
-
"dev": "tsx src/index.ts",
|
|
23
|
-
"build": "tsup src/index.ts --format esm --dts --clean --shims",
|
|
24
|
-
"test": "tsx --test src/**/*.test.ts",
|
|
25
|
-
"prepublishOnly": "pnpm build"
|
|
26
|
-
},
|
|
27
21
|
"keywords": [
|
|
28
22
|
"claude",
|
|
29
23
|
"claude-code",
|
|
@@ -51,5 +45,10 @@
|
|
|
51
45
|
},
|
|
52
46
|
"engines": {
|
|
53
47
|
"node": ">=18.0.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"dev": "tsx src/index.ts",
|
|
51
|
+
"build": "tsup src/index.ts --format esm --dts --clean --shims",
|
|
52
|
+
"test": "tsx --test src/**/*.test.ts"
|
|
54
53
|
}
|
|
55
|
-
}
|
|
54
|
+
}
|