tokentracker-cli 0.10.1 → 0.11.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/dashboard/dist/assets/main-CeKzyCR5.css +1 -0
- package/dashboard/dist/assets/{main-Cw4csGy9.js → main-DuLJEZRP.js} +240 -198
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/sync.js +110 -7
- package/src/lib/categorizer-utils.js +232 -0
- package/src/lib/claude-categorizer.js +541 -84
- package/src/lib/codex-context-breakdown.js +400 -0
- package/src/lib/codex-rollout-parser.js +623 -0
- package/src/lib/local-api.js +137 -14
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/dashboard/dist/assets/main-Bst6S3yM.css +0 -1
|
@@ -15,6 +15,18 @@ const os = require("node:os");
|
|
|
15
15
|
const path = require("node:path");
|
|
16
16
|
const readline = require("node:readline");
|
|
17
17
|
|
|
18
|
+
const {
|
|
19
|
+
categorizeTool,
|
|
20
|
+
inferExecCommandKind,
|
|
21
|
+
sanitizeCommandSignature,
|
|
22
|
+
getExecutableName,
|
|
23
|
+
emptyTotals,
|
|
24
|
+
addInto,
|
|
25
|
+
roundTotals,
|
|
26
|
+
buildExecStatsEntry,
|
|
27
|
+
allocateByLargestRemainder,
|
|
28
|
+
} = require("./categorizer-utils");
|
|
29
|
+
|
|
18
30
|
const CATEGORY_KEYS = [
|
|
19
31
|
"system_prefix",
|
|
20
32
|
"conversation_history",
|
|
@@ -27,14 +39,10 @@ const CATEGORY_KEYS = [
|
|
|
27
39
|
|
|
28
40
|
const SUBAGENT_TOOL_NAMES = new Set(["Agent", "Task"]);
|
|
29
41
|
|
|
30
|
-
function
|
|
42
|
+
function emptyToolBreakdown() {
|
|
31
43
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
cache_creation_input_tokens: 0,
|
|
35
|
-
output_tokens: 0,
|
|
36
|
-
reasoning_output_tokens: 0,
|
|
37
|
-
total_tokens: 0,
|
|
44
|
+
total_calls: 0,
|
|
45
|
+
tools: [],
|
|
38
46
|
};
|
|
39
47
|
}
|
|
40
48
|
|
|
@@ -44,113 +52,281 @@ function emptyCategoryMap() {
|
|
|
44
52
|
return out;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
function extractExecCommands(content) {
|
|
56
|
+
const commands = [];
|
|
57
|
+
for (const block of Array.isArray(content) ? content : []) {
|
|
58
|
+
if (!block || typeof block !== "object") continue;
|
|
59
|
+
if (block.type !== "tool_use") continue;
|
|
60
|
+
if (block.name !== "Bash" && block.name !== "exec_command") continue;
|
|
61
|
+
const input = block.input || {};
|
|
62
|
+
const command =
|
|
63
|
+
typeof input.command === "string" ? input.command
|
|
64
|
+
: typeof input.cmd === "string" ? input.cmd
|
|
65
|
+
: "";
|
|
66
|
+
if (command.trim()) commands.push(command.trim());
|
|
67
|
+
}
|
|
68
|
+
return commands;
|
|
54
69
|
}
|
|
55
70
|
|
|
56
|
-
function
|
|
57
|
-
|
|
71
|
+
function ensureExecRow(map, key) {
|
|
72
|
+
const safeKey = key || "unknown";
|
|
73
|
+
if (!map.has(safeKey)) map.set(safeKey, { name: safeKey, ...buildExecStatsEntry() });
|
|
74
|
+
return map.get(safeKey);
|
|
58
75
|
}
|
|
59
76
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
function recordExecCommandUsage(execLedger, commands, totals) {
|
|
78
|
+
if (!execLedger || !Array.isArray(commands) || commands.length === 0) return;
|
|
79
|
+
const perCommandRows = new Map();
|
|
80
|
+
for (const command of commands) {
|
|
81
|
+
if (!perCommandRows.has(command)) perCommandRows.set(command, { calls: 0 });
|
|
82
|
+
perCommandRows.get(command).calls += 1;
|
|
83
|
+
}
|
|
84
|
+
const totalsByCommand = allocateTotalsAcrossRows(totals, perCommandRows);
|
|
85
|
+
|
|
86
|
+
for (const [command, row] of perCommandRows.entries()) {
|
|
87
|
+
const commandTotals = totalsByCommand.get(command) || {};
|
|
88
|
+
const kind = inferExecCommandKind(command);
|
|
89
|
+
const executable = getExecutableName(command);
|
|
90
|
+
const signature = sanitizeCommandSignature(command);
|
|
91
|
+
const exitKey = "unknown:unknown";
|
|
92
|
+
|
|
93
|
+
const targets = [
|
|
94
|
+
[execLedger.by_type, kind],
|
|
95
|
+
[execLedger.by_executable, executable],
|
|
96
|
+
[execLedger.by_command, signature],
|
|
97
|
+
[execLedger.by_exit, exitKey],
|
|
98
|
+
];
|
|
99
|
+
for (const [map, key] of targets) {
|
|
100
|
+
const target = ensureExecRow(map, key);
|
|
101
|
+
target.calls += Math.max(1, Number(row.calls || 0));
|
|
102
|
+
addInto(target.totals, commandTotals);
|
|
70
103
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
104
|
+
execLedger.total_calls += Math.max(1, Number(row.calls || 0));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function allocateTotalsAcrossRows(totals, rows) {
|
|
109
|
+
const entries = Array.from(rows?.entries?.() || []);
|
|
110
|
+
if (entries.length === 0) return new Map();
|
|
111
|
+
const weights = {};
|
|
112
|
+
const order = entries.map(([name]) => name).sort();
|
|
113
|
+
for (const name of order) {
|
|
114
|
+
const row = rows.get(name) || {};
|
|
115
|
+
weights[name] = Math.max(0, Number(row.output_tokens || row.calls || 0));
|
|
116
|
+
}
|
|
117
|
+
if (order.every((name) => !weights[name])) {
|
|
118
|
+
for (const name of order) weights[name] = 1;
|
|
119
|
+
}
|
|
120
|
+
const out = new Map();
|
|
121
|
+
for (const name of order) out.set(name, emptyTotals());
|
|
122
|
+
for (const key of [
|
|
123
|
+
"input_tokens",
|
|
124
|
+
"cached_input_tokens",
|
|
125
|
+
"cache_creation_input_tokens",
|
|
126
|
+
"output_tokens",
|
|
127
|
+
"reasoning_output_tokens",
|
|
128
|
+
"total_tokens",
|
|
129
|
+
]) {
|
|
130
|
+
const alloc = allocateByLargestRemainder(Math.max(0, Number(totals?.[key] || 0)), weights, order);
|
|
131
|
+
for (const name of order) {
|
|
132
|
+
out.get(name)[key] = alloc[name] || 0;
|
|
78
133
|
}
|
|
79
134
|
}
|
|
80
135
|
return out;
|
|
81
136
|
}
|
|
82
137
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
138
|
+
function extractSkillNames(content) {
|
|
139
|
+
const names = [];
|
|
140
|
+
for (const block of Array.isArray(content) ? content : []) {
|
|
141
|
+
if (!block || typeof block !== "object") continue;
|
|
142
|
+
if (block.type !== "tool_use" || block.name !== "Skill") continue;
|
|
143
|
+
const skill = typeof block.input?.skill === "string" ? block.input.skill.trim() : "";
|
|
144
|
+
if (skill) names.push(skill);
|
|
145
|
+
}
|
|
146
|
+
return names;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function recordSkillUsage(skillLedger, skillNames, totals) {
|
|
150
|
+
if (!skillLedger || !Array.isArray(skillNames) || skillNames.length === 0) return;
|
|
151
|
+
const perMessageRows = new Map();
|
|
152
|
+
for (const name of skillNames) {
|
|
153
|
+
if (!perMessageRows.has(name)) perMessageRows.set(name, { calls: 0 });
|
|
154
|
+
perMessageRows.get(name).calls += 1;
|
|
155
|
+
}
|
|
156
|
+
const totalsByName = allocateTotalsAcrossRows(totals, perMessageRows);
|
|
157
|
+
for (const [name, row] of perMessageRows.entries()) {
|
|
158
|
+
if (!skillLedger.by_name.has(name)) {
|
|
159
|
+
skillLedger.by_name.set(name, { name, calls: 0, totals: emptyTotals() });
|
|
160
|
+
}
|
|
161
|
+
const target = skillLedger.by_name.get(name);
|
|
162
|
+
target.calls += row.calls || 0;
|
|
163
|
+
addInto(target.totals, totalsByName.get(name) || {});
|
|
164
|
+
}
|
|
165
|
+
skillLedger.total_calls += skillNames.length;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function computeOutputTokenBreakdown(usage, content) {
|
|
89
169
|
const total = Math.max(0, Number(usage.output_tokens || 0));
|
|
90
170
|
const reasoningExplicit = Math.max(0, Number(usage.reasoning_output_tokens || 0));
|
|
91
|
-
if (total === 0) return;
|
|
92
|
-
|
|
93
171
|
const blocks = Array.isArray(content) ? content : [];
|
|
94
|
-
const buckets = { reasoning: 0, tool_calls: 0, subagents: 0, assistant_response: 0 };
|
|
95
|
-
let totalChars = 0;
|
|
96
172
|
|
|
173
|
+
if (total === 0) {
|
|
174
|
+
return {
|
|
175
|
+
bucket_tokens: { reasoning: 0, tool_calls: 0, subagents: 0, assistant_response: 0 },
|
|
176
|
+
tool_calls: { total_calls: 0, by_name: new Map() },
|
|
177
|
+
subagents: { total_calls: 0, by_name: new Map() },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const bucketChars = { reasoning: 0, tool_calls: 0, subagents: 0, assistant_response: 0 };
|
|
182
|
+
const toolCallChars = new Map();
|
|
183
|
+
const subagentChars = new Map();
|
|
184
|
+
const toolCallCounts = new Map();
|
|
185
|
+
const subagentCounts = new Map();
|
|
186
|
+
|
|
187
|
+
let totalChars = 0;
|
|
97
188
|
for (const block of blocks) {
|
|
98
189
|
if (!block || typeof block !== "object") continue;
|
|
99
190
|
const type = block.type;
|
|
100
191
|
let chars = 0;
|
|
192
|
+
|
|
101
193
|
if (type === "thinking") {
|
|
102
194
|
chars = String(block.thinking || block.text || "").length || 1;
|
|
103
|
-
|
|
195
|
+
bucketChars.reasoning += chars;
|
|
104
196
|
} else if (type === "text") {
|
|
105
197
|
chars = String(block.text || "").length || 1;
|
|
106
|
-
|
|
198
|
+
bucketChars.assistant_response += chars;
|
|
107
199
|
} else if (type === "tool_use") {
|
|
108
200
|
const inputJson = block.input ? JSON.stringify(block.input) : "";
|
|
109
201
|
chars = (block.name || "").length + inputJson.length + 1;
|
|
110
|
-
if (SUBAGENT_TOOL_NAMES.has(block.name))
|
|
111
|
-
|
|
202
|
+
if (SUBAGENT_TOOL_NAMES.has(block.name)) {
|
|
203
|
+
bucketChars.subagents += chars;
|
|
204
|
+
subagentChars.set(block.name, (subagentChars.get(block.name) || 0) + chars);
|
|
205
|
+
subagentCounts.set(block.name, (subagentCounts.get(block.name) || 0) + 1);
|
|
206
|
+
} else {
|
|
207
|
+
bucketChars.tool_calls += chars;
|
|
208
|
+
toolCallChars.set(block.name, (toolCallChars.get(block.name) || 0) + chars);
|
|
209
|
+
toolCallCounts.set(block.name, (toolCallCounts.get(block.name) || 0) + 1);
|
|
210
|
+
}
|
|
112
211
|
} else {
|
|
113
212
|
continue;
|
|
114
213
|
}
|
|
214
|
+
|
|
115
215
|
totalChars += chars;
|
|
116
216
|
}
|
|
117
217
|
|
|
118
218
|
if (totalChars === 0) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
219
|
+
return {
|
|
220
|
+
bucket_tokens: { reasoning: 0, tool_calls: 0, subagents: 0, assistant_response: total },
|
|
221
|
+
tool_calls: { total_calls: 0, by_name: new Map() },
|
|
222
|
+
subagents: { total_calls: 0, by_name: new Map() },
|
|
223
|
+
};
|
|
122
224
|
}
|
|
123
225
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let nonReasoningOutput = total;
|
|
127
|
-
if (reasoningExplicit > 0) {
|
|
128
|
-
const reasoningShare = Math.min(reasoningExplicit, total);
|
|
129
|
-
breakdown.reasoning.output_tokens += reasoningShare;
|
|
130
|
-
breakdown.reasoning.reasoning_output_tokens += reasoningShare;
|
|
131
|
-
breakdown.reasoning.total_tokens += reasoningShare;
|
|
132
|
-
nonReasoningOutput = total - reasoningShare;
|
|
133
|
-
// Drop the thinking-char contribution; it was just paid for.
|
|
134
|
-
totalChars -= buckets.reasoning;
|
|
135
|
-
buckets.reasoning = 0;
|
|
136
|
-
}
|
|
226
|
+
const explicitReasoning = reasoningExplicit > 0 ? Math.min(reasoningExplicit, total) : 0;
|
|
227
|
+
const nonReasoningOutput = total - explicitReasoning;
|
|
137
228
|
|
|
138
|
-
|
|
229
|
+
const allocChars = { ...bucketChars };
|
|
230
|
+
let allocTotalChars = totalChars;
|
|
231
|
+
if (explicitReasoning > 0) {
|
|
232
|
+
allocTotalChars -= allocChars.reasoning;
|
|
233
|
+
allocChars.reasoning = 0;
|
|
234
|
+
}
|
|
139
235
|
|
|
140
|
-
// Largest-remainder rounding so the four sub-buckets sum exactly to
|
|
141
|
-
// nonReasoningOutput (no off-by-one drift across thousands of messages).
|
|
142
236
|
const order = ["reasoning", "tool_calls", "subagents", "assistant_response"];
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
237
|
+
const prorated = allocateByLargestRemainder(Math.max(0, nonReasoningOutput), allocChars, order);
|
|
238
|
+
|
|
239
|
+
const bucketTokens = {
|
|
240
|
+
reasoning: explicitReasoning + (prorated.reasoning || 0),
|
|
241
|
+
tool_calls: prorated.tool_calls || 0,
|
|
242
|
+
subagents: prorated.subagents || 0,
|
|
243
|
+
assistant_response: prorated.assistant_response || 0,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
if (allocTotalChars <= 0 && nonReasoningOutput > 0) {
|
|
247
|
+
bucketTokens.reasoning = explicitReasoning;
|
|
248
|
+
bucketTokens.tool_calls = 0;
|
|
249
|
+
bucketTokens.subagents = 0;
|
|
250
|
+
bucketTokens.assistant_response = nonReasoningOutput;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const toolTokensByName = new Map();
|
|
254
|
+
if (bucketTokens.tool_calls > 0 && toolCallChars.size > 0) {
|
|
255
|
+
const keys = [...toolCallChars.keys()].sort();
|
|
256
|
+
const weights = {};
|
|
257
|
+
for (const k of keys) weights[k] = toolCallChars.get(k) || 0;
|
|
258
|
+
const alloc = allocateByLargestRemainder(bucketTokens.tool_calls, weights, keys);
|
|
259
|
+
for (const k of keys) toolTokensByName.set(k, alloc[k] || 0);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const subagentTokensByName = new Map();
|
|
263
|
+
if (bucketTokens.subagents > 0 && subagentChars.size > 0) {
|
|
264
|
+
const keys = [...subagentChars.keys()].sort();
|
|
265
|
+
const weights = {};
|
|
266
|
+
for (const k of keys) weights[k] = subagentChars.get(k) || 0;
|
|
267
|
+
const alloc = allocateByLargestRemainder(bucketTokens.subagents, weights, keys);
|
|
268
|
+
for (const k of keys) subagentTokensByName.set(k, alloc[k] || 0);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
bucket_tokens: bucketTokens,
|
|
273
|
+
tool_calls: {
|
|
274
|
+
total_calls: [...toolCallCounts.values()].reduce((a, b) => a + b, 0),
|
|
275
|
+
by_name: new Map(
|
|
276
|
+
[...toolCallCounts.entries()].map(([name, calls]) => [
|
|
277
|
+
name,
|
|
278
|
+
{ name, calls, output_tokens: toolTokensByName.get(name) || 0 },
|
|
279
|
+
]),
|
|
280
|
+
),
|
|
281
|
+
},
|
|
282
|
+
subagents: {
|
|
283
|
+
total_calls: [...subagentCounts.values()].reduce((a, b) => a + b, 0),
|
|
284
|
+
by_name: new Map(
|
|
285
|
+
[...subagentCounts.entries()].map(([name, calls]) => [
|
|
286
|
+
name,
|
|
287
|
+
{ name, calls, output_tokens: subagentTokensByName.get(name) || 0 },
|
|
288
|
+
]),
|
|
289
|
+
),
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function defaultClaudeProjectsDir() {
|
|
295
|
+
return path.join(os.homedir(), ".claude", "projects");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function listSessionFiles(rootDir) {
|
|
299
|
+
const out = [];
|
|
300
|
+
const stack = [rootDir];
|
|
301
|
+
while (stack.length > 0) {
|
|
302
|
+
const dir = stack.pop();
|
|
303
|
+
let entries;
|
|
304
|
+
try {
|
|
305
|
+
entries = fssync.readdirSync(dir, { withFileTypes: true });
|
|
306
|
+
} catch (_e) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
for (const entry of entries) {
|
|
310
|
+
const fp = path.join(dir, entry.name);
|
|
311
|
+
if (entry.isDirectory()) {
|
|
312
|
+
stack.push(fp);
|
|
313
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
314
|
+
out.push(fp);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Distribute one assistant message's output tokens across categories by the
|
|
322
|
+
// character-length ratio of each content block. Thinking goes to reasoning,
|
|
323
|
+
// tool_use(Agent|Task) → subagents, tool_use(other) → tool_calls, text →
|
|
324
|
+
// assistant_response. If reasoning_output_tokens is reported separately, use
|
|
325
|
+
// that exact figure for reasoning instead of pro-rating.
|
|
326
|
+
function splitOutputByContent(usage, content, breakdown) {
|
|
327
|
+
const out = computeOutputTokenBreakdown(usage, content);
|
|
328
|
+
for (const key of ["reasoning", "tool_calls", "subagents", "assistant_response"]) {
|
|
329
|
+
const tok = out.bucket_tokens[key] || 0;
|
|
154
330
|
if (tok === 0) continue;
|
|
155
331
|
breakdown[key].output_tokens += tok;
|
|
156
332
|
breakdown[key].total_tokens += tok;
|
|
@@ -161,7 +337,7 @@ function splitOutputByContent(usage, content, breakdown) {
|
|
|
161
337
|
// Per-session state lets us pick out the *first* meaningful cache_creation
|
|
162
338
|
// chunk and call that the system_prefix. Subsequent cache_creations are
|
|
163
339
|
// incremental — we attribute them to conversation_history.
|
|
164
|
-
function classifyOneMessage(obj, sessionState, breakdown) {
|
|
340
|
+
function classifyOneMessage(obj, sessionState, breakdown, toolLedger = null, skillLedger = null, execLedger = null) {
|
|
165
341
|
const usage = obj?.message?.usage;
|
|
166
342
|
if (!usage || typeof usage !== "object") return;
|
|
167
343
|
|
|
@@ -195,18 +371,90 @@ function classifyOneMessage(obj, sessionState, breakdown) {
|
|
|
195
371
|
}
|
|
196
372
|
}
|
|
197
373
|
|
|
374
|
+
recordSkillUsage(
|
|
375
|
+
skillLedger,
|
|
376
|
+
extractSkillNames(obj?.message?.content),
|
|
377
|
+
{
|
|
378
|
+
input_tokens: inputNonCached,
|
|
379
|
+
cached_input_tokens: cacheRead,
|
|
380
|
+
cache_creation_input_tokens: cacheCreate,
|
|
381
|
+
output_tokens: output,
|
|
382
|
+
reasoning_output_tokens: 0,
|
|
383
|
+
total_tokens: inputNonCached + cacheRead + cacheCreate + output,
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
|
|
198
387
|
// Split output across reasoning / tool_calls / subagents / assistant_response.
|
|
199
388
|
if (output > 0) {
|
|
200
|
-
|
|
389
|
+
const out = computeOutputTokenBreakdown(
|
|
201
390
|
{ output_tokens: output, reasoning_output_tokens: usage.reasoning_output_tokens },
|
|
202
391
|
obj?.message?.content,
|
|
203
|
-
breakdown,
|
|
204
392
|
);
|
|
393
|
+
for (const key of ["reasoning", "tool_calls", "subagents", "assistant_response"]) {
|
|
394
|
+
const tok = out.bucket_tokens[key] || 0;
|
|
395
|
+
if (tok === 0) continue;
|
|
396
|
+
breakdown[key].output_tokens += tok;
|
|
397
|
+
breakdown[key].total_tokens += tok;
|
|
398
|
+
if (key === "reasoning") breakdown[key].reasoning_output_tokens += tok;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
recordExecCommandUsage(
|
|
402
|
+
execLedger,
|
|
403
|
+
extractExecCommands(obj?.message?.content),
|
|
404
|
+
{
|
|
405
|
+
input_tokens: inputNonCached,
|
|
406
|
+
cached_input_tokens: cacheRead,
|
|
407
|
+
cache_creation_input_tokens: cacheCreate,
|
|
408
|
+
output_tokens: out.bucket_tokens.tool_calls || 0,
|
|
409
|
+
reasoning_output_tokens: 0,
|
|
410
|
+
total_tokens: inputNonCached + cacheRead + cacheCreate + (out.bucket_tokens.tool_calls || 0),
|
|
411
|
+
},
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
if (toolLedger) {
|
|
415
|
+
const ledgerTotals = {
|
|
416
|
+
input_tokens: inputNonCached,
|
|
417
|
+
cached_input_tokens: cacheRead,
|
|
418
|
+
cache_creation_input_tokens: cacheCreate,
|
|
419
|
+
output_tokens: out.bucket_tokens.tool_calls || 0,
|
|
420
|
+
reasoning_output_tokens: 0,
|
|
421
|
+
total_tokens: inputNonCached + cacheRead + cacheCreate + (out.bucket_tokens.tool_calls || 0),
|
|
422
|
+
};
|
|
423
|
+
const toolTotalsByName = allocateTotalsAcrossRows(ledgerTotals, out.tool_calls.by_name);
|
|
424
|
+
for (const [name, row] of out.tool_calls.by_name.entries()) {
|
|
425
|
+
if (!toolLedger.tool_calls.by_name.has(name)) {
|
|
426
|
+
toolLedger.tool_calls.by_name.set(name, { name, calls: 0, totals: emptyTotals() });
|
|
427
|
+
}
|
|
428
|
+
const target = toolLedger.tool_calls.by_name.get(name);
|
|
429
|
+
target.calls += row.calls || 0;
|
|
430
|
+
addInto(target.totals, toolTotalsByName.get(name) || {});
|
|
431
|
+
}
|
|
432
|
+
toolLedger.tool_calls.total_calls += out.tool_calls.total_calls || 0;
|
|
433
|
+
|
|
434
|
+
const subagentTotals = {
|
|
435
|
+
input_tokens: inputNonCached,
|
|
436
|
+
cached_input_tokens: cacheRead,
|
|
437
|
+
cache_creation_input_tokens: cacheCreate,
|
|
438
|
+
output_tokens: out.bucket_tokens.subagents || 0,
|
|
439
|
+
reasoning_output_tokens: 0,
|
|
440
|
+
total_tokens: inputNonCached + cacheRead + cacheCreate + (out.bucket_tokens.subagents || 0),
|
|
441
|
+
};
|
|
442
|
+
const subagentTotalsByName = allocateTotalsAcrossRows(subagentTotals, out.subagents.by_name);
|
|
443
|
+
for (const [name, row] of out.subagents.by_name.entries()) {
|
|
444
|
+
if (!toolLedger.subagents.by_name.has(name)) {
|
|
445
|
+
toolLedger.subagents.by_name.set(name, { name, calls: 0, totals: emptyTotals() });
|
|
446
|
+
}
|
|
447
|
+
const target = toolLedger.subagents.by_name.get(name);
|
|
448
|
+
target.calls += row.calls || 0;
|
|
449
|
+
addInto(target.totals, subagentTotalsByName.get(name) || {});
|
|
450
|
+
}
|
|
451
|
+
toolLedger.subagents.total_calls += out.subagents.total_calls || 0;
|
|
452
|
+
}
|
|
205
453
|
}
|
|
206
454
|
}
|
|
207
455
|
|
|
208
456
|
// Read one session jsonl streaming, in timestamp range, dedup by msgId+reqId.
|
|
209
|
-
async function categorizeSessionFile(filePath, { fromIso, toIso, seenHashes }, breakdown) {
|
|
457
|
+
async function categorizeSessionFile(filePath, { fromIso, toIso, seenHashes }, breakdown, toolLedger = null, skillLedger = null, execLedger = null) {
|
|
210
458
|
let stream;
|
|
211
459
|
try {
|
|
212
460
|
stream = fssync.createReadStream(filePath, { encoding: "utf8" });
|
|
@@ -238,7 +486,7 @@ async function categorizeSessionFile(filePath, { fromIso, toIso, seenHashes }, b
|
|
|
238
486
|
seenHashes.add(hash);
|
|
239
487
|
}
|
|
240
488
|
|
|
241
|
-
classifyOneMessage(obj, sessionState, breakdown);
|
|
489
|
+
classifyOneMessage(obj, sessionState, breakdown, toolLedger, skillLedger, execLedger);
|
|
242
490
|
counted += 1;
|
|
243
491
|
}
|
|
244
492
|
rl.close();
|
|
@@ -271,6 +519,7 @@ function dayKeyToIsoBounds(from, to) {
|
|
|
271
519
|
// case the watcher misses something.
|
|
272
520
|
const CACHE = new Map();
|
|
273
521
|
const CACHE_TTL_MS = 60_000;
|
|
522
|
+
const CACHE_SCHEMA_VERSION = "skills-exec-v2";
|
|
274
523
|
|
|
275
524
|
function maxMtimeMs(files) {
|
|
276
525
|
let max = 0;
|
|
@@ -303,7 +552,7 @@ async function computeClaudeCategoryBreakdown({ from = null, to = null, rootDir
|
|
|
303
552
|
};
|
|
304
553
|
}
|
|
305
554
|
|
|
306
|
-
const cacheKey = `${root}|${from || ""}|${to || ""}|${files.length}|${maxMtimeMs(files)}`;
|
|
555
|
+
const cacheKey = `${CACHE_SCHEMA_VERSION}|${root}|${from || ""}|${to || ""}|${files.length}|${maxMtimeMs(files)}`;
|
|
307
556
|
const cached = CACHE.get(cacheKey);
|
|
308
557
|
if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
309
558
|
return cached.value;
|
|
@@ -314,12 +563,27 @@ async function computeClaudeCategoryBreakdown({ from = null, to = null, rootDir
|
|
|
314
563
|
const seenHashes = new Set();
|
|
315
564
|
let messageCount = 0;
|
|
316
565
|
let sessionCount = 0;
|
|
566
|
+
const toolLedger = {
|
|
567
|
+
tool_calls: { total_calls: 0, by_name: new Map() },
|
|
568
|
+
subagents: { total_calls: 0, by_name: new Map() },
|
|
569
|
+
};
|
|
570
|
+
const skillLedger = { total_calls: 0, by_name: new Map() };
|
|
571
|
+
const execLedger = {
|
|
572
|
+
total_calls: 0,
|
|
573
|
+
by_type: new Map(),
|
|
574
|
+
by_executable: new Map(),
|
|
575
|
+
by_command: new Map(),
|
|
576
|
+
by_exit: new Map(),
|
|
577
|
+
};
|
|
317
578
|
|
|
318
579
|
for (const fp of files) {
|
|
319
580
|
const counted = await categorizeSessionFile(
|
|
320
581
|
fp,
|
|
321
582
|
{ fromIso, toIso, seenHashes },
|
|
322
583
|
breakdown,
|
|
584
|
+
toolLedger,
|
|
585
|
+
skillLedger,
|
|
586
|
+
execLedger,
|
|
323
587
|
);
|
|
324
588
|
if (counted > 0) sessionCount += 1;
|
|
325
589
|
messageCount += counted;
|
|
@@ -341,17 +605,210 @@ async function computeClaudeCategoryBreakdown({ from = null, to = null, rootDir
|
|
|
341
605
|
}),
|
|
342
606
|
session_count: sessionCount,
|
|
343
607
|
message_count: messageCount,
|
|
608
|
+
message_breakdown: buildMessageBreakdown(breakdown),
|
|
609
|
+
tool_calls_breakdown: buildToolCallsBreakdown(toolLedger),
|
|
610
|
+
skills_breakdown: buildSkillsBreakdown(skillLedger),
|
|
611
|
+
exec_command_breakdown: buildExecCommandBreakdown(execLedger),
|
|
612
|
+
configured_resources: getConfiguredResources({ projectDir }),
|
|
344
613
|
};
|
|
345
614
|
|
|
346
615
|
CACHE.set(cacheKey, { at: Date.now(), value: result });
|
|
347
616
|
// Bound cache size — categorizer is cheap to recompute, no point hoarding.
|
|
348
|
-
|
|
617
|
+
while (CACHE.size > 32) {
|
|
349
618
|
const oldest = [...CACHE.entries()].sort((a, b) => a[1].at - b[1].at)[0];
|
|
350
619
|
if (oldest) CACHE.delete(oldest[0]);
|
|
351
620
|
}
|
|
352
621
|
return result;
|
|
353
622
|
}
|
|
354
623
|
|
|
624
|
+
function buildToolCallsBreakdown(toolLedger) {
|
|
625
|
+
if (!toolLedger || !toolLedger.tool_calls || !toolLedger.subagents) {
|
|
626
|
+
return {
|
|
627
|
+
tool_calls: emptyToolBreakdown(),
|
|
628
|
+
subagents: emptyToolBreakdown(),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function mapToSortedRows(map) {
|
|
633
|
+
const rows = Array.from(map.values()).map((row) => ({
|
|
634
|
+
name: row.name,
|
|
635
|
+
calls: row.calls,
|
|
636
|
+
totals: row.totals || {
|
|
637
|
+
...emptyTotals(),
|
|
638
|
+
output_tokens: row.output_tokens || 0,
|
|
639
|
+
total_tokens: row.total_tokens || row.output_tokens || 0,
|
|
640
|
+
},
|
|
641
|
+
}));
|
|
642
|
+
rows.sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
|
|
643
|
+
return rows;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const toolCallsRows = mapToSortedRows(toolLedger.tool_calls.by_name || new Map());
|
|
647
|
+
const subagentRows = mapToSortedRows(toolLedger.subagents.by_name || new Map());
|
|
648
|
+
|
|
649
|
+
function groupIntoCategories(rows) {
|
|
650
|
+
const byCategory = new Map(); // name -> {name,calls,totals,tools:[]}
|
|
651
|
+
for (const row of rows) {
|
|
652
|
+
const toolName = String(row?.name || "");
|
|
653
|
+
if (!toolName) continue;
|
|
654
|
+
const cat = categorizeTool(toolName);
|
|
655
|
+
if (!byCategory.has(cat)) {
|
|
656
|
+
byCategory.set(cat, {
|
|
657
|
+
name: cat,
|
|
658
|
+
calls: 0,
|
|
659
|
+
totals: emptyTotals(),
|
|
660
|
+
tools: [],
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
const bucket = byCategory.get(cat);
|
|
664
|
+
bucket.calls += Number(row.calls || 0);
|
|
665
|
+
addInto(bucket.totals, row.totals || {});
|
|
666
|
+
bucket.tools.push({
|
|
667
|
+
name: toolName,
|
|
668
|
+
calls: Number(row.calls || 0),
|
|
669
|
+
totals: row.totals || emptyTotals(),
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
const categories = Array.from(byCategory.values())
|
|
673
|
+
.map((c) => ({
|
|
674
|
+
name: c.name,
|
|
675
|
+
calls: Math.round(c.calls || 0),
|
|
676
|
+
totals: {
|
|
677
|
+
input_tokens: Math.round(c.totals.input_tokens || 0),
|
|
678
|
+
cached_input_tokens: Math.round(c.totals.cached_input_tokens || 0),
|
|
679
|
+
cache_creation_input_tokens: Math.round(c.totals.cache_creation_input_tokens || 0),
|
|
680
|
+
output_tokens: Math.round(c.totals.output_tokens || 0),
|
|
681
|
+
reasoning_output_tokens: Math.round(c.totals.reasoning_output_tokens || 0),
|
|
682
|
+
total_tokens: Math.round(c.totals.total_tokens || 0),
|
|
683
|
+
},
|
|
684
|
+
tools: c.tools
|
|
685
|
+
.sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0))
|
|
686
|
+
.map((t) => ({
|
|
687
|
+
name: t.name,
|
|
688
|
+
calls: Math.round(t.calls || 0),
|
|
689
|
+
totals: {
|
|
690
|
+
input_tokens: Math.round(t.totals.input_tokens || 0),
|
|
691
|
+
cached_input_tokens: Math.round(t.totals.cached_input_tokens || 0),
|
|
692
|
+
cache_creation_input_tokens: Math.round(t.totals.cache_creation_input_tokens || 0),
|
|
693
|
+
output_tokens: Math.round(t.totals.output_tokens || 0),
|
|
694
|
+
reasoning_output_tokens: Math.round(t.totals.reasoning_output_tokens || 0),
|
|
695
|
+
total_tokens: Math.round(t.totals.total_tokens || 0),
|
|
696
|
+
},
|
|
697
|
+
})),
|
|
698
|
+
}))
|
|
699
|
+
.sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
|
|
700
|
+
return categories;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return {
|
|
704
|
+
tool_calls: {
|
|
705
|
+
total_calls: toolLedger.tool_calls.total_calls || 0,
|
|
706
|
+
tools: toolCallsRows,
|
|
707
|
+
categories: groupIntoCategories(toolCallsRows),
|
|
708
|
+
},
|
|
709
|
+
subagents: {
|
|
710
|
+
total_calls: toolLedger.subagents.total_calls || 0,
|
|
711
|
+
tools: subagentRows,
|
|
712
|
+
categories: groupIntoCategories(subagentRows),
|
|
713
|
+
},
|
|
714
|
+
privacy: {
|
|
715
|
+
includes_inputs: false,
|
|
716
|
+
note: "Aggregated tool names only; tool inputs are never recorded.",
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function buildSkillsBreakdown(skillLedger) {
|
|
722
|
+
const rows = Array.from(skillLedger?.by_name?.values?.() || [])
|
|
723
|
+
.map((row) => ({
|
|
724
|
+
name: row.name,
|
|
725
|
+
calls: Math.round(row.calls || 0),
|
|
726
|
+
totals: roundTotals(row.totals || emptyTotals()),
|
|
727
|
+
}))
|
|
728
|
+
.sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
total_calls: Math.round(skillLedger?.total_calls || 0),
|
|
732
|
+
skills: rows,
|
|
733
|
+
privacy: {
|
|
734
|
+
includes_inputs: false,
|
|
735
|
+
note: "Aggregated skill names only; skill inputs are never returned.",
|
|
736
|
+
},
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function serializeExecRows(map) {
|
|
741
|
+
return Array.from(map?.values?.() || [])
|
|
742
|
+
.map((row) => ({
|
|
743
|
+
name: row.name,
|
|
744
|
+
calls: Math.round(row.calls || 0),
|
|
745
|
+
failures: Math.round(row.failures || 0),
|
|
746
|
+
duration_ms: Math.round(row.duration_ms || 0),
|
|
747
|
+
max_duration_ms: Math.round(row.max_duration_ms || 0),
|
|
748
|
+
output_chars: Math.round(row.output_chars || 0),
|
|
749
|
+
output_lines: Math.round(row.output_lines || 0),
|
|
750
|
+
totals: roundTotals(row.totals || emptyTotals()),
|
|
751
|
+
}))
|
|
752
|
+
.sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function buildExecCommandBreakdown(execLedger) {
|
|
756
|
+
return {
|
|
757
|
+
total_calls: Math.round(execLedger?.total_calls || 0),
|
|
758
|
+
by_type: serializeExecRows(execLedger?.by_type),
|
|
759
|
+
by_executable: serializeExecRows(execLedger?.by_executable),
|
|
760
|
+
by_command: serializeExecRows(execLedger?.by_command),
|
|
761
|
+
by_duration: [],
|
|
762
|
+
by_output: [],
|
|
763
|
+
by_exit: serializeExecRows(execLedger?.by_exit),
|
|
764
|
+
privacy: {
|
|
765
|
+
includes_commands: false,
|
|
766
|
+
note: "Claude Bash commands are grouped into sanitized signatures; raw commands are not returned.",
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function buildMessageBreakdown(breakdown) {
|
|
772
|
+
const rows = [
|
|
773
|
+
{
|
|
774
|
+
key: "user_input",
|
|
775
|
+
name: "User input",
|
|
776
|
+
totals: breakdown.user_input || emptyTotals(),
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
key: "conversation_history",
|
|
780
|
+
name: "Conversation history",
|
|
781
|
+
totals: breakdown.conversation_history || emptyTotals(),
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
key: "assistant_response",
|
|
785
|
+
name: "Assistant response",
|
|
786
|
+
totals: breakdown.assistant_response || emptyTotals(),
|
|
787
|
+
},
|
|
788
|
+
];
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
categories: rows
|
|
792
|
+
.map((row) => ({
|
|
793
|
+
key: row.key,
|
|
794
|
+
name: row.name,
|
|
795
|
+
totals: {
|
|
796
|
+
input_tokens: Math.round(row.totals.input_tokens || 0),
|
|
797
|
+
cached_input_tokens: Math.round(row.totals.cached_input_tokens || 0),
|
|
798
|
+
cache_creation_input_tokens: Math.round(row.totals.cache_creation_input_tokens || 0),
|
|
799
|
+
output_tokens: Math.round(row.totals.output_tokens || 0),
|
|
800
|
+
reasoning_output_tokens: Math.round(row.totals.reasoning_output_tokens || 0),
|
|
801
|
+
total_tokens: Math.round(row.totals.total_tokens || 0),
|
|
802
|
+
},
|
|
803
|
+
}))
|
|
804
|
+
.sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0)),
|
|
805
|
+
privacy: {
|
|
806
|
+
includes_content: false,
|
|
807
|
+
note: "Aggregated message token categories only; prompt and assistant text are never returned.",
|
|
808
|
+
},
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
355
812
|
// Lightweight on-disk count of static resources Claude Code's /context UI
|
|
356
813
|
// also surfaces (Skills, MCP servers, Memory files, Custom agents). These are
|
|
357
814
|
// counts of what's *installed*, not historical token usage — the same way
|