@tomdraper/claude-usage 0.1.0
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/LICENSE +21 -0
- package/README.md +50 -0
- package/bin/claude-usage.js +3 -0
- package/dist/cli/index.js +591 -0
- package/dist/dashboard/assets/index-BI_JjCY6.css +2 -0
- package/dist/dashboard/assets/index-BtSpo2ZF.js +4 -0
- package/dist/dashboard/favicon.svg +16 -0
- package/dist/dashboard/index.html +20 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tom Draper
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<h1 align="center">Claude Usage</h1>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<code>npx @tomdraper/claude-usage</code>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<img width="1303" height="957" alt="Claude-Usage-05-14-2026_07_47_AM" src="https://github.com/user-attachments/assets/699fc000-daca-4bfd-b85f-f7fe492eeac2" />
|
|
10
|
+
|
|
11
|
+
<br>
|
|
12
|
+
<br>
|
|
13
|
+
|
|
14
|
+
Claude Code stores detailed session data locally on your machine. Claude Usage reads these JSONL logs and launches a powerful, filter-first dashboard for exploring your usage. 100% local, no data leaves your machine.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Run without installing:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx @tomdraper/claude-usage
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or install globally:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install -g @tomdraper/claude-usage
|
|
28
|
+
claude-usage
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Estimations
|
|
32
|
+
|
|
33
|
+
Estimated (proportional token-share distribution across each day's activity)
|
|
34
|
+
- Per-model messages
|
|
35
|
+
- Per-model sessions
|
|
36
|
+
- Per-model costs, when the stats cache reports zero cost for a model (falls back to API pricing tables)
|
|
37
|
+
|
|
38
|
+
Model cost estimates are calculated using Anthropic API pricing. Actual costs for Pro, Max, and Teams users are typically far lower, since subscription usage is heavily subsidized.
|
|
39
|
+
|
|
40
|
+
These are estimated rather than read directly from JSONL files because older sessions may not include fields like `model`, `gitBranch`, `entrypoint`, or `cwd`, making JSONL-derived counts unreliable across full history. The stats cache has accurate totals going further back, so per-model figures are derived by distributing each day's totals proportionally by token share.
|
|
41
|
+
|
|
42
|
+
## Contributions
|
|
43
|
+
|
|
44
|
+
Contributions, issues and feature requests are welcome.
|
|
45
|
+
|
|
46
|
+
- Fork it (https://github.com/tom-draper/claude-usage)
|
|
47
|
+
- Create your feature branch (`git checkout -b my-new-feature`)
|
|
48
|
+
- Commit your changes (`git commit -am 'Add some feature'`)
|
|
49
|
+
- Push to the branch (`git push origin my-new-feature`)
|
|
50
|
+
- Create a new Pull Request
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// src/cli/index.ts
|
|
26
|
+
var import_http = __toESM(require("http"));
|
|
27
|
+
var import_fs2 = __toESM(require("fs"));
|
|
28
|
+
var import_path2 = __toESM(require("path"));
|
|
29
|
+
var import_child_process = require("child_process");
|
|
30
|
+
|
|
31
|
+
// src/cli/parser.ts
|
|
32
|
+
var import_fs = __toESM(require("fs"));
|
|
33
|
+
var import_path = __toESM(require("path"));
|
|
34
|
+
var import_os = __toESM(require("os"));
|
|
35
|
+
var import_readline = __toESM(require("readline"));
|
|
36
|
+
var CLAUDE_DIR = import_path.default.join(import_os.default.homedir(), ".claude");
|
|
37
|
+
var STATS_CACHE = import_path.default.join(CLAUDE_DIR, "stats-cache.json");
|
|
38
|
+
var PROJECTS_DIR = import_path.default.join(CLAUDE_DIR, "projects");
|
|
39
|
+
function readStatsCache() {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(import_fs.default.readFileSync(STATS_CACHE, "utf8"));
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`No stats cache found at ${STATS_CACHE}. Make sure Claude Code has been used on this machine.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var MODEL_PRICING = [
|
|
49
|
+
{ match: (id) => /opus-4/.test(id), inputPerMToken: 15, outputPerMToken: 75, cacheWritePerMToken: 18.75, cacheReadPerMToken: 1.5 },
|
|
50
|
+
{ match: (id) => /sonnet-4|3-5-sonnet|sonnet-3-5/.test(id), inputPerMToken: 3, outputPerMToken: 15, cacheWritePerMToken: 3.75, cacheReadPerMToken: 0.3 },
|
|
51
|
+
{ match: (id) => /haiku-4|haiku-3-5|3-5-haiku/.test(id), inputPerMToken: 0.8, outputPerMToken: 4, cacheWritePerMToken: 1, cacheReadPerMToken: 0.08 },
|
|
52
|
+
{ match: (id) => /opus-3|3-opus/.test(id), inputPerMToken: 15, outputPerMToken: 75, cacheWritePerMToken: 18.75, cacheReadPerMToken: 1.5 },
|
|
53
|
+
{ match: (id) => /sonnet-3|3-sonnet/.test(id), inputPerMToken: 3, outputPerMToken: 15, cacheWritePerMToken: 3.75, cacheReadPerMToken: 0.3 },
|
|
54
|
+
{ match: (id) => /haiku-3|3-haiku/.test(id), inputPerMToken: 0.25, outputPerMToken: 1.25, cacheWritePerMToken: 0.3, cacheReadPerMToken: 0.03 }
|
|
55
|
+
];
|
|
56
|
+
function estimateCost(id, inputTokens, outputTokens, cacheReadTokens, cacheCreateTokens) {
|
|
57
|
+
const p = MODEL_PRICING.find((m) => m.match(id));
|
|
58
|
+
if (!p) return 0;
|
|
59
|
+
return inputTokens * p.inputPerMToken / 1e6 + outputTokens * p.outputPerMToken / 1e6 + cacheCreateTokens * p.cacheWritePerMToken / 1e6 + cacheReadTokens * p.cacheReadPerMToken / 1e6;
|
|
60
|
+
}
|
|
61
|
+
function formatModelName(id) {
|
|
62
|
+
const stripped = id.startsWith("claude-") ? id.slice(7) : id;
|
|
63
|
+
const match = stripped.match(/^(sonnet|opus|haiku)-(\d+)-(\d+)/);
|
|
64
|
+
if (match) {
|
|
65
|
+
const [, family, major, minor] = match;
|
|
66
|
+
return `${family.charAt(0).toUpperCase() + family.slice(1)} ${major}.${minor}`;
|
|
67
|
+
}
|
|
68
|
+
return stripped.charAt(0).toUpperCase() + stripped.slice(1);
|
|
69
|
+
}
|
|
70
|
+
async function getCwdFromJsonl(filePath) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
let resolved = false;
|
|
73
|
+
const done = (val) => {
|
|
74
|
+
if (!resolved) {
|
|
75
|
+
resolved = true;
|
|
76
|
+
resolve(val);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
let stream;
|
|
80
|
+
try {
|
|
81
|
+
stream = import_fs.default.createReadStream(filePath, { encoding: "utf8" });
|
|
82
|
+
} catch {
|
|
83
|
+
return done(null);
|
|
84
|
+
}
|
|
85
|
+
const rl = import_readline.default.createInterface({ input: stream, crlfDelay: Infinity });
|
|
86
|
+
let lineCount = 0;
|
|
87
|
+
rl.on("line", (line) => {
|
|
88
|
+
lineCount++;
|
|
89
|
+
if (lineCount > 20) {
|
|
90
|
+
rl.close();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const obj = JSON.parse(line);
|
|
95
|
+
if (obj.cwd) {
|
|
96
|
+
done(obj.cwd);
|
|
97
|
+
rl.close();
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
rl.on("close", () => done(null));
|
|
103
|
+
stream.on("error", () => done(null));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function parseJSONLFiles() {
|
|
107
|
+
const result = {
|
|
108
|
+
modelMessages: {},
|
|
109
|
+
modelSessionCounts: {},
|
|
110
|
+
modelToolCalls: {},
|
|
111
|
+
modelToolNames: {},
|
|
112
|
+
modelThinkingTurns: {},
|
|
113
|
+
stopReasons: {},
|
|
114
|
+
stopReasonHourlyCounts: {},
|
|
115
|
+
stopReasonDailyCounts: {},
|
|
116
|
+
stopReasonMiniutelyCounts: {},
|
|
117
|
+
entrypoints: {},
|
|
118
|
+
entrypointHourlyCounts: {},
|
|
119
|
+
entrypointDailyCounts: {},
|
|
120
|
+
entrypointMiniutelyCounts: {},
|
|
121
|
+
branchCounts: {},
|
|
122
|
+
branchHourlyCounts: {},
|
|
123
|
+
branchDailyCounts: {},
|
|
124
|
+
branchMiniutelyCounts: {},
|
|
125
|
+
branchSessionCounts: {},
|
|
126
|
+
toolCounts: {},
|
|
127
|
+
toolHourlyCounts: {},
|
|
128
|
+
toolDailyCounts: {},
|
|
129
|
+
toolMiniutelyCounts: {},
|
|
130
|
+
hourlyMessages: {},
|
|
131
|
+
hourlyModelTokens: {},
|
|
132
|
+
hourlySessionCounts: {},
|
|
133
|
+
minutelyMessages: {},
|
|
134
|
+
minutelyModelTokens: {},
|
|
135
|
+
projectDailyMessages: {},
|
|
136
|
+
projectHourlyMessages: {},
|
|
137
|
+
modelStopReasons: {},
|
|
138
|
+
modelBranchSessions: {},
|
|
139
|
+
modelProjectSessions: {},
|
|
140
|
+
toolStopReasons: {},
|
|
141
|
+
toolBranchSessions: {},
|
|
142
|
+
toolProjectSessions: {},
|
|
143
|
+
toolSessionCounts: {},
|
|
144
|
+
stopReasonBranchSessions: {},
|
|
145
|
+
stopReasonProjectSessions: {},
|
|
146
|
+
stopReasonSessionCounts: {},
|
|
147
|
+
branchProjectSessions: {}
|
|
148
|
+
};
|
|
149
|
+
const inc = (map, key) => {
|
|
150
|
+
map[key] = (map[key] ?? 0) + 1;
|
|
151
|
+
};
|
|
152
|
+
let dirEntries;
|
|
153
|
+
try {
|
|
154
|
+
dirEntries = import_fs.default.readdirSync(PROJECTS_DIR, { withFileTypes: true });
|
|
155
|
+
} catch {
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
for (const dir of dirEntries) {
|
|
159
|
+
if (!dir.isDirectory()) continue;
|
|
160
|
+
const dirPath = import_path.default.join(PROJECTS_DIR, dir.name);
|
|
161
|
+
let files;
|
|
162
|
+
try {
|
|
163
|
+
files = import_fs.default.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
164
|
+
} catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
let content;
|
|
169
|
+
try {
|
|
170
|
+
content = import_fs.default.readFileSync(import_path.default.join(dirPath, file), "utf8");
|
|
171
|
+
} catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
let firstUserHour = null;
|
|
175
|
+
const branchesInSession = /* @__PURE__ */ new Set();
|
|
176
|
+
const cwdsInSession = /* @__PURE__ */ new Set();
|
|
177
|
+
const modelsInSession = /* @__PURE__ */ new Set();
|
|
178
|
+
const toolsInSession = /* @__PURE__ */ new Set();
|
|
179
|
+
const stopReasonsInSession = /* @__PURE__ */ new Set();
|
|
180
|
+
for (const line of content.split("\n")) {
|
|
181
|
+
if (!line.trim()) continue;
|
|
182
|
+
let obj;
|
|
183
|
+
try {
|
|
184
|
+
obj = JSON.parse(line);
|
|
185
|
+
} catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (obj.type === "assistant") {
|
|
189
|
+
const msg = obj.message;
|
|
190
|
+
const model = msg?.model;
|
|
191
|
+
const ts = obj.timestamp;
|
|
192
|
+
const toolsInMessage = /* @__PURE__ */ new Set();
|
|
193
|
+
if (model) {
|
|
194
|
+
inc(result.modelMessages, model);
|
|
195
|
+
modelsInSession.add(model);
|
|
196
|
+
let hasThinking = false;
|
|
197
|
+
for (const block of msg?.content ?? []) {
|
|
198
|
+
if (block.type === "tool_use") {
|
|
199
|
+
inc(result.modelToolCalls, model);
|
|
200
|
+
if (block.name) {
|
|
201
|
+
inc(result.toolCounts, block.name);
|
|
202
|
+
toolsInMessage.add(block.name);
|
|
203
|
+
toolsInSession.add(block.name);
|
|
204
|
+
if (!result.modelToolNames[model]) result.modelToolNames[model] = {};
|
|
205
|
+
inc(result.modelToolNames[model], block.name);
|
|
206
|
+
if (ts) {
|
|
207
|
+
if (!result.toolHourlyCounts[block.name]) result.toolHourlyCounts[block.name] = {};
|
|
208
|
+
inc(result.toolHourlyCounts[block.name], ts.slice(0, 13));
|
|
209
|
+
if (!result.toolDailyCounts[block.name]) result.toolDailyCounts[block.name] = {};
|
|
210
|
+
inc(result.toolDailyCounts[block.name], ts.slice(0, 10));
|
|
211
|
+
if (!result.toolMiniutelyCounts[block.name]) result.toolMiniutelyCounts[block.name] = {};
|
|
212
|
+
inc(result.toolMiniutelyCounts[block.name], ts.slice(0, 16));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (block.type === "thinking") hasThinking = true;
|
|
217
|
+
}
|
|
218
|
+
if (hasThinking) inc(result.modelThinkingTurns, model);
|
|
219
|
+
const usage = msg?.usage;
|
|
220
|
+
if (ts && usage) {
|
|
221
|
+
const tokens = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
|
|
222
|
+
const hour = ts.slice(0, 13);
|
|
223
|
+
if (!result.hourlyModelTokens[model]) result.hourlyModelTokens[model] = {};
|
|
224
|
+
result.hourlyModelTokens[model][hour] = (result.hourlyModelTokens[model][hour] ?? 0) + tokens;
|
|
225
|
+
const minute = ts.slice(0, 16);
|
|
226
|
+
if (!result.minutelyModelTokens[model]) result.minutelyModelTokens[model] = {};
|
|
227
|
+
result.minutelyModelTokens[model][minute] = (result.minutelyModelTokens[model][minute] ?? 0) + tokens;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (msg?.stop_reason) {
|
|
231
|
+
const reason = msg.stop_reason;
|
|
232
|
+
inc(result.stopReasons, reason);
|
|
233
|
+
stopReasonsInSession.add(reason);
|
|
234
|
+
if (model) {
|
|
235
|
+
if (!result.modelStopReasons[model]) result.modelStopReasons[model] = {};
|
|
236
|
+
inc(result.modelStopReasons[model], reason);
|
|
237
|
+
}
|
|
238
|
+
for (const toolName of toolsInMessage) {
|
|
239
|
+
if (!result.toolStopReasons[toolName]) result.toolStopReasons[toolName] = {};
|
|
240
|
+
inc(result.toolStopReasons[toolName], reason);
|
|
241
|
+
}
|
|
242
|
+
if (ts) {
|
|
243
|
+
const hour = ts.slice(0, 13);
|
|
244
|
+
const day = ts.slice(0, 10);
|
|
245
|
+
const minute = ts.slice(0, 16);
|
|
246
|
+
if (!result.stopReasonHourlyCounts[reason]) result.stopReasonHourlyCounts[reason] = {};
|
|
247
|
+
inc(result.stopReasonHourlyCounts[reason], hour);
|
|
248
|
+
if (!result.stopReasonDailyCounts[reason]) result.stopReasonDailyCounts[reason] = {};
|
|
249
|
+
inc(result.stopReasonDailyCounts[reason], day);
|
|
250
|
+
if (!result.stopReasonMiniutelyCounts[reason]) result.stopReasonMiniutelyCounts[reason] = {};
|
|
251
|
+
inc(result.stopReasonMiniutelyCounts[reason], minute);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} else if (obj.type === "user") {
|
|
255
|
+
if (obj.entrypoint) {
|
|
256
|
+
const ep = obj.entrypoint;
|
|
257
|
+
inc(result.entrypoints, ep);
|
|
258
|
+
const ts2 = obj.timestamp;
|
|
259
|
+
if (ts2) {
|
|
260
|
+
const hour = ts2.slice(0, 13);
|
|
261
|
+
const day = ts2.slice(0, 10);
|
|
262
|
+
const minute = ts2.slice(0, 16);
|
|
263
|
+
if (!result.entrypointHourlyCounts[ep]) result.entrypointHourlyCounts[ep] = {};
|
|
264
|
+
inc(result.entrypointHourlyCounts[ep], hour);
|
|
265
|
+
if (!result.entrypointDailyCounts[ep]) result.entrypointDailyCounts[ep] = {};
|
|
266
|
+
inc(result.entrypointDailyCounts[ep], day);
|
|
267
|
+
if (!result.entrypointMiniutelyCounts[ep]) result.entrypointMiniutelyCounts[ep] = {};
|
|
268
|
+
inc(result.entrypointMiniutelyCounts[ep], minute);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (obj.gitBranch) {
|
|
272
|
+
const branch = obj.gitBranch;
|
|
273
|
+
inc(result.branchCounts, branch);
|
|
274
|
+
branchesInSession.add(branch);
|
|
275
|
+
const bts = obj.timestamp;
|
|
276
|
+
if (bts) {
|
|
277
|
+
if (!result.branchHourlyCounts[branch]) result.branchHourlyCounts[branch] = {};
|
|
278
|
+
inc(result.branchHourlyCounts[branch], bts.slice(0, 13));
|
|
279
|
+
if (!result.branchDailyCounts[branch]) result.branchDailyCounts[branch] = {};
|
|
280
|
+
inc(result.branchDailyCounts[branch], bts.slice(0, 10));
|
|
281
|
+
if (!result.branchMiniutelyCounts[branch]) result.branchMiniutelyCounts[branch] = {};
|
|
282
|
+
inc(result.branchMiniutelyCounts[branch], bts.slice(0, 16));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const ts = obj.timestamp;
|
|
286
|
+
if (ts) {
|
|
287
|
+
if (!firstUserHour) firstUserHour = ts.slice(0, 13);
|
|
288
|
+
inc(result.hourlyMessages, ts.slice(0, 13));
|
|
289
|
+
inc(result.minutelyMessages, ts.slice(0, 16));
|
|
290
|
+
const cwd = obj.cwd;
|
|
291
|
+
if (cwd) {
|
|
292
|
+
cwdsInSession.add(cwd);
|
|
293
|
+
const date = ts.slice(0, 10);
|
|
294
|
+
const hour = ts.slice(0, 13);
|
|
295
|
+
if (!result.projectDailyMessages[cwd]) result.projectDailyMessages[cwd] = {};
|
|
296
|
+
inc(result.projectDailyMessages[cwd], date);
|
|
297
|
+
if (!result.projectHourlyMessages[cwd]) result.projectHourlyMessages[cwd] = {};
|
|
298
|
+
inc(result.projectHourlyMessages[cwd], hour);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (firstUserHour) {
|
|
304
|
+
result.hourlySessionCounts[firstUserHour] = (result.hourlySessionCounts[firstUserHour] ?? 0) + 1;
|
|
305
|
+
}
|
|
306
|
+
for (const branch of branchesInSession) {
|
|
307
|
+
result.branchSessionCounts[branch] = (result.branchSessionCounts[branch] ?? 0) + 1;
|
|
308
|
+
for (const cwd of cwdsInSession) {
|
|
309
|
+
if (!result.branchProjectSessions[branch]) result.branchProjectSessions[branch] = {};
|
|
310
|
+
result.branchProjectSessions[branch][cwd] = (result.branchProjectSessions[branch][cwd] ?? 0) + 1;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
for (const model of modelsInSession) {
|
|
314
|
+
result.modelSessionCounts[model] = (result.modelSessionCounts[model] ?? 0) + 1;
|
|
315
|
+
for (const branch of branchesInSession) {
|
|
316
|
+
if (!result.modelBranchSessions[model]) result.modelBranchSessions[model] = {};
|
|
317
|
+
result.modelBranchSessions[model][branch] = (result.modelBranchSessions[model][branch] ?? 0) + 1;
|
|
318
|
+
}
|
|
319
|
+
for (const cwd of cwdsInSession) {
|
|
320
|
+
if (!result.modelProjectSessions[model]) result.modelProjectSessions[model] = {};
|
|
321
|
+
result.modelProjectSessions[model][cwd] = (result.modelProjectSessions[model][cwd] ?? 0) + 1;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const tool of toolsInSession) {
|
|
325
|
+
result.toolSessionCounts[tool] = (result.toolSessionCounts[tool] ?? 0) + 1;
|
|
326
|
+
for (const branch of branchesInSession) {
|
|
327
|
+
if (!result.toolBranchSessions[tool]) result.toolBranchSessions[tool] = {};
|
|
328
|
+
result.toolBranchSessions[tool][branch] = (result.toolBranchSessions[tool][branch] ?? 0) + 1;
|
|
329
|
+
}
|
|
330
|
+
for (const cwd of cwdsInSession) {
|
|
331
|
+
if (!result.toolProjectSessions[tool]) result.toolProjectSessions[tool] = {};
|
|
332
|
+
result.toolProjectSessions[tool][cwd] = (result.toolProjectSessions[tool][cwd] ?? 0) + 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
for (const reason of stopReasonsInSession) {
|
|
336
|
+
result.stopReasonSessionCounts[reason] = (result.stopReasonSessionCounts[reason] ?? 0) + 1;
|
|
337
|
+
for (const branch of branchesInSession) {
|
|
338
|
+
if (!result.stopReasonBranchSessions[reason]) result.stopReasonBranchSessions[reason] = {};
|
|
339
|
+
result.stopReasonBranchSessions[reason][branch] = (result.stopReasonBranchSessions[reason][branch] ?? 0) + 1;
|
|
340
|
+
}
|
|
341
|
+
for (const cwd of cwdsInSession) {
|
|
342
|
+
if (!result.stopReasonProjectSessions[reason]) result.stopReasonProjectSessions[reason] = {};
|
|
343
|
+
result.stopReasonProjectSessions[reason][cwd] = (result.stopReasonProjectSessions[reason][cwd] ?? 0) + 1;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
async function parseProjects() {
|
|
351
|
+
let dirEntries;
|
|
352
|
+
try {
|
|
353
|
+
dirEntries = import_fs.default.readdirSync(PROJECTS_DIR, { withFileTypes: true });
|
|
354
|
+
} catch {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
const home = import_os.default.homedir();
|
|
358
|
+
const projectMap = /* @__PURE__ */ new Map();
|
|
359
|
+
const addProject = (projectPath, sessions, messages) => {
|
|
360
|
+
const displayName = projectPath.startsWith(home + "/") ? projectPath.slice(home.length + 1) : projectPath;
|
|
361
|
+
const existing = projectMap.get(projectPath);
|
|
362
|
+
if (existing) {
|
|
363
|
+
existing.sessions += sessions;
|
|
364
|
+
existing.messages += messages;
|
|
365
|
+
} else {
|
|
366
|
+
projectMap.set(projectPath, { name: displayName, path: projectPath, sessions, messages, costEstimate: 0 });
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
await Promise.all(
|
|
370
|
+
dirEntries.filter((e) => e.isDirectory()).map(async (dir) => {
|
|
371
|
+
const dirPath = import_path.default.join(PROJECTS_DIR, dir.name);
|
|
372
|
+
const indexPath = import_path.default.join(dirPath, "sessions-index.json");
|
|
373
|
+
try {
|
|
374
|
+
const index = JSON.parse(import_fs.default.readFileSync(indexPath, "utf8"));
|
|
375
|
+
for (const entry of index.entries ?? []) {
|
|
376
|
+
if (entry.projectPath) {
|
|
377
|
+
addProject(entry.projectPath, 1, entry.messageCount ?? 0);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
let files;
|
|
384
|
+
try {
|
|
385
|
+
files = import_fs.default.readdirSync(dirPath).filter((f) => f.endsWith(".jsonl"));
|
|
386
|
+
} catch {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (files.length === 0) return;
|
|
390
|
+
let projectPath = null;
|
|
391
|
+
for (const file of files.slice(0, 3)) {
|
|
392
|
+
projectPath = await getCwdFromJsonl(import_path.default.join(dirPath, file));
|
|
393
|
+
if (projectPath) break;
|
|
394
|
+
}
|
|
395
|
+
if (!projectPath) {
|
|
396
|
+
projectPath = "/" + dir.name.replace(/^-/, "").replace(/-/g, "/");
|
|
397
|
+
}
|
|
398
|
+
addProject(projectPath, files.length, 0);
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
return Array.from(projectMap.values()).filter((p) => p.sessions > 0).sort((a, b) => b.sessions - a.sessions).slice(0, 15);
|
|
402
|
+
}
|
|
403
|
+
async function parse() {
|
|
404
|
+
const stats = readStatsCache();
|
|
405
|
+
const [projects, jsonl] = await Promise.all([parseProjects(), Promise.resolve(parseJSONLFiles())]);
|
|
406
|
+
const jsonlTotal = Object.values(jsonl.hourlyMessages).reduce((s, n) => s + n, 0);
|
|
407
|
+
const statsCacheTotal = (stats.dailyActivity ?? []).reduce((s, d) => s + (d.messageCount ?? 0), 0);
|
|
408
|
+
const scale = statsCacheTotal > 0 ? jsonlTotal / statsCacheTotal : 1;
|
|
409
|
+
const dailyActivityMap = new Map((stats.dailyActivity ?? []).map((d) => [d.date, { messages: d.messageCount, sessions: d.sessionCount }]));
|
|
410
|
+
const estimatedModelMessages = {};
|
|
411
|
+
const estimatedModelSessions = {};
|
|
412
|
+
for (const dayTokens of stats.dailyModelTokens ?? []) {
|
|
413
|
+
const day = dailyActivityMap.get(dayTokens.date);
|
|
414
|
+
const dayMessages = (day?.messages ?? 0) * scale;
|
|
415
|
+
const daySessions = day?.sessions ?? 0;
|
|
416
|
+
if (dayMessages === 0 && daySessions === 0) continue;
|
|
417
|
+
const totalDayTokens = Object.values(dayTokens.tokensByModel).reduce((a, b) => a + b, 0);
|
|
418
|
+
if (totalDayTokens === 0) continue;
|
|
419
|
+
for (const [model, tokens] of Object.entries(dayTokens.tokensByModel)) {
|
|
420
|
+
const frac = tokens / totalDayTokens;
|
|
421
|
+
estimatedModelMessages[model] = (estimatedModelMessages[model] ?? 0) + dayMessages * frac;
|
|
422
|
+
estimatedModelSessions[model] = (estimatedModelSessions[model] ?? 0) + daySessions * frac;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const modelUsage = Object.entries(stats.modelUsage ?? {}).map(([id, usage]) => ({
|
|
426
|
+
id,
|
|
427
|
+
name: formatModelName(id),
|
|
428
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
429
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
430
|
+
cacheReadTokens: usage.cacheReadInputTokens ?? 0,
|
|
431
|
+
cacheCreateTokens: usage.cacheCreationInputTokens ?? 0,
|
|
432
|
+
costUSD: usage.costUSD > 0 ? usage.costUSD : estimateCost(id, usage.inputTokens ?? 0, usage.outputTokens ?? 0, usage.cacheReadInputTokens ?? 0, usage.cacheCreationInputTokens ?? 0),
|
|
433
|
+
messages: Math.round(estimatedModelMessages[id] ?? 0),
|
|
434
|
+
sessions: Math.round(estimatedModelSessions[id] ?? 0),
|
|
435
|
+
toolCalls: jsonl.modelToolCalls[id] ?? 0,
|
|
436
|
+
thinkingTurns: jsonl.modelThinkingTurns[id] ?? 0
|
|
437
|
+
})).sort((a, b) => b.inputTokens + b.outputTokens - (a.inputTokens + a.outputTokens));
|
|
438
|
+
const rawHours = stats.hourCounts ?? {};
|
|
439
|
+
const hourCounts = Array.from({ length: 24 }, (_, i) => rawHours[String(i)] ?? 0);
|
|
440
|
+
const totalToolCalls = (stats.dailyActivity ?? []).reduce(
|
|
441
|
+
(sum, d) => sum + (d.toolCallCount ?? 0),
|
|
442
|
+
0
|
|
443
|
+
);
|
|
444
|
+
const totalTokens = modelUsage.reduce(
|
|
445
|
+
(sum, m) => sum + m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheCreateTokens,
|
|
446
|
+
0
|
|
447
|
+
);
|
|
448
|
+
const allMinutes = /* @__PURE__ */ new Set([
|
|
449
|
+
...Object.keys(jsonl.minutelyMessages),
|
|
450
|
+
...Object.values(jsonl.minutelyModelTokens).flatMap((m) => Object.keys(m))
|
|
451
|
+
]);
|
|
452
|
+
const minutelyActivity = Array.from(allMinutes).sort().map((minute) => ({
|
|
453
|
+
minute,
|
|
454
|
+
messageCount: jsonl.minutelyMessages[minute] ?? 0,
|
|
455
|
+
tokensByModel: Object.fromEntries(
|
|
456
|
+
Object.entries(jsonl.minutelyModelTokens).map(([model, mins]) => [model, mins[minute] ?? 0]).filter(([, v]) => v > 0)
|
|
457
|
+
)
|
|
458
|
+
}));
|
|
459
|
+
const allHours = /* @__PURE__ */ new Set([
|
|
460
|
+
...Object.keys(jsonl.hourlyMessages),
|
|
461
|
+
...Object.values(jsonl.hourlyModelTokens).flatMap((m) => Object.keys(m))
|
|
462
|
+
]);
|
|
463
|
+
const hourlyActivity = Array.from(allHours).sort().map((hour) => ({
|
|
464
|
+
hour,
|
|
465
|
+
messageCount: jsonl.hourlyMessages[hour] ?? 0,
|
|
466
|
+
sessionCount: jsonl.hourlySessionCounts[hour] ?? 0,
|
|
467
|
+
tokensByModel: Object.fromEntries(
|
|
468
|
+
Object.entries(jsonl.hourlyModelTokens).map(([model, hours]) => [model, hours[hour] ?? 0]).filter(([, v]) => v > 0)
|
|
469
|
+
)
|
|
470
|
+
}));
|
|
471
|
+
const topBranches = Object.entries(jsonl.branchCounts).map(([branch, count]) => ({ branch, count })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
472
|
+
return {
|
|
473
|
+
totalSessions: stats.totalSessions ?? 0,
|
|
474
|
+
totalMessages: stats.totalMessages ?? 0,
|
|
475
|
+
totalToolCalls,
|
|
476
|
+
totalTokens,
|
|
477
|
+
firstSessionDate: stats.firstSessionDate ?? null,
|
|
478
|
+
longestSession: stats.longestSession ?? null,
|
|
479
|
+
dailyActivity: stats.dailyActivity ?? [],
|
|
480
|
+
dailyModelTokens: stats.dailyModelTokens ?? [],
|
|
481
|
+
modelUsage,
|
|
482
|
+
hourCounts,
|
|
483
|
+
hourlyActivity,
|
|
484
|
+
minutelyActivity,
|
|
485
|
+
projects,
|
|
486
|
+
stopReasons: jsonl.stopReasons,
|
|
487
|
+
stopReasonHourlyCounts: jsonl.stopReasonHourlyCounts,
|
|
488
|
+
stopReasonDailyCounts: jsonl.stopReasonDailyCounts,
|
|
489
|
+
stopReasonMiniutelyCounts: jsonl.stopReasonMiniutelyCounts,
|
|
490
|
+
entrypoints: jsonl.entrypoints,
|
|
491
|
+
entrypointHourlyCounts: jsonl.entrypointHourlyCounts,
|
|
492
|
+
entrypointDailyCounts: jsonl.entrypointDailyCounts,
|
|
493
|
+
entrypointMiniutelyCounts: jsonl.entrypointMiniutelyCounts,
|
|
494
|
+
topBranches,
|
|
495
|
+
branchCounts: jsonl.branchCounts,
|
|
496
|
+
branchHourlyCounts: jsonl.branchHourlyCounts,
|
|
497
|
+
branchDailyCounts: jsonl.branchDailyCounts,
|
|
498
|
+
branchMiniutelyCounts: jsonl.branchMiniutelyCounts,
|
|
499
|
+
branchSessionCounts: jsonl.branchSessionCounts,
|
|
500
|
+
toolCounts: jsonl.toolCounts,
|
|
501
|
+
modelToolNames: jsonl.modelToolNames,
|
|
502
|
+
toolHourlyCounts: jsonl.toolHourlyCounts,
|
|
503
|
+
toolDailyCounts: jsonl.toolDailyCounts,
|
|
504
|
+
toolMiniutelyCounts: jsonl.toolMiniutelyCounts,
|
|
505
|
+
projectDailyMessages: jsonl.projectDailyMessages,
|
|
506
|
+
projectHourlyMessages: jsonl.projectHourlyMessages,
|
|
507
|
+
modelStopReasons: jsonl.modelStopReasons,
|
|
508
|
+
modelBranchSessions: jsonl.modelBranchSessions,
|
|
509
|
+
modelProjectSessions: jsonl.modelProjectSessions,
|
|
510
|
+
toolStopReasons: jsonl.toolStopReasons,
|
|
511
|
+
toolBranchSessions: jsonl.toolBranchSessions,
|
|
512
|
+
toolProjectSessions: jsonl.toolProjectSessions,
|
|
513
|
+
toolSessionCounts: jsonl.toolSessionCounts,
|
|
514
|
+
stopReasonBranchSessions: jsonl.stopReasonBranchSessions,
|
|
515
|
+
stopReasonProjectSessions: jsonl.stopReasonProjectSessions,
|
|
516
|
+
stopReasonSessionCounts: jsonl.stopReasonSessionCounts,
|
|
517
|
+
branchProjectSessions: jsonl.branchProjectSessions
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/cli/index.ts
|
|
522
|
+
var PORT = Number(process.env.PORT) || 3131;
|
|
523
|
+
var DASHBOARD_DIR = import_path2.default.join(__dirname, "../dashboard");
|
|
524
|
+
var MIME = {
|
|
525
|
+
".html": "text/html; charset=utf-8",
|
|
526
|
+
".js": "text/javascript",
|
|
527
|
+
".css": "text/css",
|
|
528
|
+
".svg": "image/svg+xml",
|
|
529
|
+
".ico": "image/x-icon",
|
|
530
|
+
".woff2": "font/woff2"
|
|
531
|
+
};
|
|
532
|
+
function serveStatic(res, filePath) {
|
|
533
|
+
try {
|
|
534
|
+
const data = import_fs2.default.readFileSync(filePath);
|
|
535
|
+
const ext = import_path2.default.extname(filePath);
|
|
536
|
+
res.writeHead(200, { "Content-Type": MIME[ext] ?? "application/octet-stream" });
|
|
537
|
+
res.end(data);
|
|
538
|
+
return true;
|
|
539
|
+
} catch {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function openBrowser(url) {
|
|
544
|
+
const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
|
|
545
|
+
(0, import_child_process.exec)(cmd, (err) => {
|
|
546
|
+
if (err) console.log("Visit:", url);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
async function main() {
|
|
550
|
+
process.stdout.write("Parsing Claude usage data...");
|
|
551
|
+
let data;
|
|
552
|
+
try {
|
|
553
|
+
data = await parse();
|
|
554
|
+
} catch (err) {
|
|
555
|
+
console.error("\nError:", err.message);
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
const dataJson = JSON.stringify(data);
|
|
559
|
+
process.stdout.write(" done.\n");
|
|
560
|
+
const server = import_http.default.createServer((req, res) => {
|
|
561
|
+
const url = req.url ?? "/";
|
|
562
|
+
if (url === "/api/data") {
|
|
563
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
564
|
+
res.end(dataJson);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const urlPath = url === "/" ? "/index.html" : url.split("?")[0];
|
|
568
|
+
const filePath = import_path2.default.join(DASHBOARD_DIR, urlPath);
|
|
569
|
+
if (serveStatic(res, filePath)) return;
|
|
570
|
+
const indexPath = import_path2.default.join(DASHBOARD_DIR, "index.html");
|
|
571
|
+
if (!serveStatic(res, indexPath)) {
|
|
572
|
+
res.writeHead(404);
|
|
573
|
+
res.end("Not found");
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
server.listen(PORT, "127.0.0.1", () => {
|
|
577
|
+
const url = `http://localhost:${PORT}`;
|
|
578
|
+
console.log(`Dashboard running at ${url}`);
|
|
579
|
+
openBrowser(url);
|
|
580
|
+
console.log("Press Ctrl+C to stop.");
|
|
581
|
+
});
|
|
582
|
+
server.on("error", (err) => {
|
|
583
|
+
if (err.code === "EADDRINUSE") {
|
|
584
|
+
console.error(`Port ${PORT} is in use. Set PORT env var to use a different one.`);
|
|
585
|
+
} else {
|
|
586
|
+
console.error("Server error:", err.message);
|
|
587
|
+
}
|
|
588
|
+
process.exit(1);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
main();
|