burner-cc 0.1.2 → 0.3.1
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 +79 -19
- package/bin/burner.js +2 -0
- package/package.json +11 -8
- package/src/analyze.js +241 -0
- package/src/build-receipt.js +118 -11
- package/src/classifier.js +140 -0
- package/src/cli.js +181 -84
- package/src/config.js +222 -0
- package/src/discover.js +10 -7
- package/src/install.js +215 -0
- package/src/model/prior.json +1 -0
- package/src/model/prior.meta.json +320 -0
- package/src/parse.js +234 -42
- package/src/pricing-fallback.json +199 -6
- package/src/pricing.js +176 -98
- package/src/receipt.js +351 -0
- package/src/report.js +873 -0
- package/src/server.js +261 -0
- package/src/settings-page.js +752 -0
- package/src/tag-end.js +94 -0
- package/src/tagging.js +122 -0
- package/src/tags.js +79 -0
- package/src/themes.js +122 -0
- package/src/yaml.js +163 -0
- package/web/receipt.css +0 -298
- package/web/receipt.html +0 -32
- package/web/receipt.js +0 -315
- /package/{web → assets}/logo-mark.png +0 -0
- /package/{web → assets}/logo-with-text.png +0 -0
- /package/{web/vendor → vendor}/html2canvas.min.js +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
{
|
|
2
|
+
"trainedAt": "2026-10-25T10:33:58.921Z",
|
|
3
|
+
"recommended": false,
|
|
4
|
+
"datasets": [
|
|
5
|
+
{
|
|
6
|
+
"id": "dolly",
|
|
7
|
+
"name": "databricks/databricks-dolly-15k",
|
|
8
|
+
"license": "CC-BY-SA-3.0",
|
|
9
|
+
"url": "https://huggingface.co/datasets/databricks/databricks-dolly-15k",
|
|
10
|
+
"examplesUsed": 2809
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": "no_robots",
|
|
14
|
+
"name": "HuggingFaceH4/no_robots",
|
|
15
|
+
"license": "CC-BY-NC-4.0",
|
|
16
|
+
"url": "https://huggingface.co/datasets/HuggingFaceH4/no_robots",
|
|
17
|
+
"examplesUsed": 2253
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "code_instructions",
|
|
21
|
+
"name": "iamtarun/python_code_instructions_18k_alpaca",
|
|
22
|
+
"license": "See dataset card (instruction-tuning; widely redistributed)",
|
|
23
|
+
"url": "https://huggingface.co/datasets/iamtarun/python_code_instructions_18k_alpaca",
|
|
24
|
+
"examplesUsed": 971
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "swe_bench",
|
|
28
|
+
"name": "princeton-nlp/SWE-bench",
|
|
29
|
+
"license": "MIT (per SWE-bench repository)",
|
|
30
|
+
"url": "https://huggingface.co/datasets/princeton-nlp/SWE-bench",
|
|
31
|
+
"examplesUsed": 1000
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "sql_context",
|
|
35
|
+
"name": "b-mc2/sql-create-context",
|
|
36
|
+
"license": "See dataset card (Spider-derived text-to-SQL context)",
|
|
37
|
+
"url": "https://huggingface.co/datasets/b-mc2/sql-create-context",
|
|
38
|
+
"examplesUsed": 762
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "github_codereview",
|
|
42
|
+
"name": "ronantakizawa/github-codereview",
|
|
43
|
+
"license": "Permissive (source repos MIT/Apache/BSD per dataset card)",
|
|
44
|
+
"url": "https://huggingface.co/datasets/ronantakizawa/github-codereview",
|
|
45
|
+
"examplesUsed": 1000
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"rejectedDatasets": [
|
|
49
|
+
{
|
|
50
|
+
"name": "lmsys/lmsys-chat-1m",
|
|
51
|
+
"reason": "Not accessible via public datasets-server (gated/private)."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "irfanalee/github-code-review-full",
|
|
55
|
+
"reason": "No parquet config on datasets-server; raw JSONL paths not available without LFS pull."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "allenai/WildChat",
|
|
59
|
+
"reason": "Ungated download works but has no intent/task labels suitable for honest mapping."
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"labelMappings": {
|
|
63
|
+
"dolly": {
|
|
64
|
+
"brainstorming": "planning",
|
|
65
|
+
"summarization": "writing",
|
|
66
|
+
"creative_writing": "writing",
|
|
67
|
+
"classification": "analysis",
|
|
68
|
+
"information_extraction": "research",
|
|
69
|
+
"open_qa": "learning",
|
|
70
|
+
"closed_qa": "learning",
|
|
71
|
+
"general_qa": "learning"
|
|
72
|
+
},
|
|
73
|
+
"no_robots": {
|
|
74
|
+
"Coding": "coding",
|
|
75
|
+
"Brainstorm": "planning",
|
|
76
|
+
"Summarize": "writing",
|
|
77
|
+
"Generation": "writing",
|
|
78
|
+
"Rewrite": "writing",
|
|
79
|
+
"Open QA": "learning",
|
|
80
|
+
"Closed QA": "learning",
|
|
81
|
+
"Extract": "research",
|
|
82
|
+
"Classify": "analysis",
|
|
83
|
+
"Chat": "other"
|
|
84
|
+
},
|
|
85
|
+
"code_instructions": {
|
|
86
|
+
"code_instruction": "coding"
|
|
87
|
+
},
|
|
88
|
+
"swe_bench": {
|
|
89
|
+
"bug_report": "debugging"
|
|
90
|
+
},
|
|
91
|
+
"sql_context": {
|
|
92
|
+
"text_to_sql": "analysis"
|
|
93
|
+
},
|
|
94
|
+
"github_codereview": {
|
|
95
|
+
"code_review": "review"
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"training": {
|
|
99
|
+
"seed": 42,
|
|
100
|
+
"capPerClass": 1000,
|
|
101
|
+
"trainExamples": 7036,
|
|
102
|
+
"testExamples": 1759,
|
|
103
|
+
"minFeatureCount": 2,
|
|
104
|
+
"topNPerClass": 600
|
|
105
|
+
},
|
|
106
|
+
"examplesPerClass": {
|
|
107
|
+
"other": 795,
|
|
108
|
+
"coding": 1000,
|
|
109
|
+
"debugging": 1000,
|
|
110
|
+
"planning": 1000,
|
|
111
|
+
"writing": 1000,
|
|
112
|
+
"review": 1000,
|
|
113
|
+
"research": 1000,
|
|
114
|
+
"learning": 1000,
|
|
115
|
+
"analysis": 1000
|
|
116
|
+
},
|
|
117
|
+
"evaluation": {
|
|
118
|
+
"holdout": {
|
|
119
|
+
"accuracy": 0.6646,
|
|
120
|
+
"macroF1": 0.6315,
|
|
121
|
+
"perClass": {
|
|
122
|
+
"analysis": {
|
|
123
|
+
"precision": 0.9551,
|
|
124
|
+
"recall": 0.85,
|
|
125
|
+
"f1": 0.8995,
|
|
126
|
+
"support": 200
|
|
127
|
+
},
|
|
128
|
+
"coding": {
|
|
129
|
+
"precision": 0.8117,
|
|
130
|
+
"recall": 0.97,
|
|
131
|
+
"f1": 0.8838,
|
|
132
|
+
"support": 200
|
|
133
|
+
},
|
|
134
|
+
"debugging": {
|
|
135
|
+
"precision": 0.9879,
|
|
136
|
+
"recall": 0.815,
|
|
137
|
+
"f1": 0.8932,
|
|
138
|
+
"support": 200
|
|
139
|
+
},
|
|
140
|
+
"learning": {
|
|
141
|
+
"precision": 0.2984,
|
|
142
|
+
"recall": 0.37,
|
|
143
|
+
"f1": 0.3304,
|
|
144
|
+
"support": 200
|
|
145
|
+
},
|
|
146
|
+
"other": {
|
|
147
|
+
"precision": 0.5502,
|
|
148
|
+
"recall": 1,
|
|
149
|
+
"f1": 0.7098,
|
|
150
|
+
"support": 159
|
|
151
|
+
},
|
|
152
|
+
"planning": {
|
|
153
|
+
"precision": 0.4663,
|
|
154
|
+
"recall": 0.9,
|
|
155
|
+
"f1": 0.6143,
|
|
156
|
+
"support": 200
|
|
157
|
+
},
|
|
158
|
+
"research": {
|
|
159
|
+
"precision": 0.3333,
|
|
160
|
+
"recall": 0.01,
|
|
161
|
+
"f1": 0.0194,
|
|
162
|
+
"support": 200
|
|
163
|
+
},
|
|
164
|
+
"review": {
|
|
165
|
+
"precision": 0.9766,
|
|
166
|
+
"recall": 0.835,
|
|
167
|
+
"f1": 0.9003,
|
|
168
|
+
"support": 200
|
|
169
|
+
},
|
|
170
|
+
"writing": {
|
|
171
|
+
"precision": 0.7792,
|
|
172
|
+
"recall": 0.3,
|
|
173
|
+
"f1": 0.4332,
|
|
174
|
+
"support": 200
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"majorityBaseline": 0.1137,
|
|
178
|
+
"majorityClass": "debugging",
|
|
179
|
+
"evaluated": 1759,
|
|
180
|
+
"confidentAccuracy": 0.708,
|
|
181
|
+
"confidentCoverage": 0.8977,
|
|
182
|
+
"confidentThreshold": 0.6
|
|
183
|
+
},
|
|
184
|
+
"crossDataset": {
|
|
185
|
+
"perDataset": {
|
|
186
|
+
"dolly": {
|
|
187
|
+
"skipped": false,
|
|
188
|
+
"testExamples": 2809,
|
|
189
|
+
"overlappingCategories": [
|
|
190
|
+
"writing",
|
|
191
|
+
"research",
|
|
192
|
+
"learning",
|
|
193
|
+
"planning",
|
|
194
|
+
"analysis"
|
|
195
|
+
],
|
|
196
|
+
"accuracy": 0.3407,
|
|
197
|
+
"macroF1": 0.2643,
|
|
198
|
+
"majorityBaseline": 0.3158
|
|
199
|
+
},
|
|
200
|
+
"no_robots": {
|
|
201
|
+
"skipped": false,
|
|
202
|
+
"testExamples": 2253,
|
|
203
|
+
"overlappingCategories": [
|
|
204
|
+
"planning",
|
|
205
|
+
"writing",
|
|
206
|
+
"learning",
|
|
207
|
+
"research",
|
|
208
|
+
"coding",
|
|
209
|
+
"analysis"
|
|
210
|
+
],
|
|
211
|
+
"accuracy": 0.2166,
|
|
212
|
+
"macroF1": 0.1463,
|
|
213
|
+
"majorityBaseline": 0.3529
|
|
214
|
+
},
|
|
215
|
+
"code_instructions": {
|
|
216
|
+
"skipped": true,
|
|
217
|
+
"reason": "fewer than 2 overlapping categories"
|
|
218
|
+
},
|
|
219
|
+
"swe_bench": {
|
|
220
|
+
"skipped": true,
|
|
221
|
+
"reason": "fewer than 2 overlapping categories"
|
|
222
|
+
},
|
|
223
|
+
"sql_context": {
|
|
224
|
+
"skipped": true,
|
|
225
|
+
"reason": "fewer than 2 overlapping categories"
|
|
226
|
+
},
|
|
227
|
+
"github_codereview": {
|
|
228
|
+
"skipped": true,
|
|
229
|
+
"reason": "fewer than 2 overlapping categories"
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
"avgMacroF1": 0.2053,
|
|
233
|
+
"avgAccuracy": 0.2787,
|
|
234
|
+
"datasetsScored": 2
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
"sanity": [
|
|
238
|
+
{
|
|
239
|
+
"prompt": "the login page crashes when I click submit",
|
|
240
|
+
"category": "coding",
|
|
241
|
+
"confidence": 0.738
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"prompt": "write a PRD for onboarding new users",
|
|
245
|
+
"category": "coding",
|
|
246
|
+
"confidence": 0.924
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"prompt": "summarize this Q3 sales spreadsheet into bullet points for leadership",
|
|
250
|
+
"category": "planning",
|
|
251
|
+
"confidence": 0.586
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"prompt": "what does this function do? explain like I am new to the codebase",
|
|
255
|
+
"category": "planning",
|
|
256
|
+
"confidence": 0.902
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"prompt": "review my PR for security issues before I merge",
|
|
260
|
+
"category": "review",
|
|
261
|
+
"confidence": 0.99
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"prompt": "add a dark mode toggle to the settings page using our existing theme tokens",
|
|
265
|
+
"category": "review",
|
|
266
|
+
"confidence": 0.844
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
"prompt": "why is my Jest test failing with \"cannot read property of undefined\"",
|
|
270
|
+
"category": "review",
|
|
271
|
+
"confidence": 0.349
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
"prompt": "compare Postgres vs SQLite for our side project and recommend one",
|
|
275
|
+
"category": "learning",
|
|
276
|
+
"confidence": 0.472
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
"prompt": "draft an email to the customer apologizing for the outage",
|
|
280
|
+
"category": "coding",
|
|
281
|
+
"confidence": 0.605
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"prompt": "plot month-over-month revenue from this CSV and call out anomalies",
|
|
285
|
+
"category": "coding",
|
|
286
|
+
"confidence": 0.704
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"prompt": "break the epic into user stories with acceptance criteria",
|
|
290
|
+
"category": "other",
|
|
291
|
+
"confidence": 0.992
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"prompt": "find where we authenticate API keys in this repo",
|
|
295
|
+
"category": "coding",
|
|
296
|
+
"confidence": 0.98
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"prompt": "help me understand what a binary search tree is",
|
|
300
|
+
"category": "writing",
|
|
301
|
+
"confidence": 0.853
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
"prompt": "refactor this React component to use hooks instead of a class",
|
|
305
|
+
"category": "review",
|
|
306
|
+
"confidence": 0.9
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"prompt": "check my migration script for data loss risks",
|
|
310
|
+
"category": "coding",
|
|
311
|
+
"confidence": 0.995
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"prompt": "write release notes for version 2.4 from these commit messages",
|
|
315
|
+
"category": "debugging",
|
|
316
|
+
"confidence": 0.78
|
|
317
|
+
}
|
|
318
|
+
],
|
|
319
|
+
"limitations": "Labels come from general instruction-tuning corpora (Q&A, summarization, coding exercises, bug reports, SQL questions, code-review diffs), not from Claude Code session logs. Categories such as planning and debugging are represented by narrower proxies (brainstorming prompts, SWE-bench issue text). Expect confusion between writing vs research, coding vs debugging, and analysis vs research on short prompts. The model should be treated as a weak default prior until a project trains on its own tagged sessions."
|
|
320
|
+
}
|
package/src/parse.js
CHANGED
|
@@ -1,20 +1,108 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
3
|
+
// Gateways and local servers are not always tidy: counts can arrive as
|
|
4
|
+
// strings or null, and some use OpenAI-style names. Anything that is not a
|
|
5
|
+
// finite non-negative number counts as 0 so one odd value can never turn a
|
|
6
|
+
// total into NaN or a concatenated string.
|
|
7
|
+
function tok(v) {
|
|
8
|
+
const n = Number(v);
|
|
9
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeUsage(u) {
|
|
13
|
+
const cc = u.cache_creation ?? {};
|
|
14
|
+
const hasSplit = cc.ephemeral_5m_input_tokens != null || cc.ephemeral_1h_input_tokens != null;
|
|
15
|
+
const createdTotal = tok(u.cache_creation_input_tokens);
|
|
16
|
+
return {
|
|
17
|
+
input: tok(u.input_tokens ?? u.prompt_tokens),
|
|
18
|
+
output: tok(u.output_tokens ?? u.completion_tokens),
|
|
19
|
+
cacheWrite5m: hasSplit ? tok(cc.ephemeral_5m_input_tokens) : createdTotal,
|
|
20
|
+
cacheWrite1h: hasSplit ? tok(cc.ephemeral_1h_input_tokens) : 0,
|
|
21
|
+
cacheRead: tok(u.cache_read_input_tokens ?? u.prompt_tokens_details?.cached_tokens),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const EMPTY_ACTIVITY = () => ({ mcp: {}, skills: {}, web: {} });
|
|
26
|
+
|
|
27
|
+
function serverDisplayName(slug) {
|
|
28
|
+
return slug.replace(/_/g, ' ').trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function newBucket() {
|
|
32
|
+
return {
|
|
33
|
+
calls: 0,
|
|
34
|
+
outputTokens: 0,
|
|
35
|
+
contextTokens: 0,
|
|
36
|
+
contextMissing: 0,
|
|
37
|
+
outputByModel: {},
|
|
38
|
+
contextByModel: {},
|
|
39
|
+
via: {},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function userText(message) {
|
|
44
|
+
if (typeof message?.content === 'string') return message.content;
|
|
45
|
+
if (!Array.isArray(message?.content)) return '';
|
|
46
|
+
return message.content
|
|
47
|
+
.filter((b) => b.type === 'text' && typeof b.text === 'string')
|
|
48
|
+
.map((b) => b.text)
|
|
49
|
+
.join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Prompts the human actually typed — not tool results, slash-command
|
|
53
|
+
// scaffolding, or the "session continued" summaries Claude Code injects.
|
|
54
|
+
function isHumanPrompt(entry, text) {
|
|
55
|
+
if (entry.isMeta || entry.isCompactSummary || entry.isSidechain) return false;
|
|
56
|
+
if (!text.trim()) return false;
|
|
57
|
+
if (/^<(command-|local-command|system-reminder|bash-|user-memory)/.test(text.trim())) return false;
|
|
58
|
+
if (text.startsWith('This session is being continued')) return false;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toolStep(block) {
|
|
63
|
+
const name = block.name ?? 'unknown';
|
|
64
|
+
const input = block.input ?? {};
|
|
65
|
+
if (name === 'Bash' && typeof input.command === 'string') {
|
|
66
|
+
return `Bash(${input.command.trim().split(/\s+/).slice(0, 2).join(' ')})`;
|
|
67
|
+
}
|
|
68
|
+
const file = input.file_path ?? input.notebook_path ?? input.path;
|
|
69
|
+
if (typeof file === 'string') {
|
|
70
|
+
const ext = file.match(/\.([a-z0-9]+)$/i)?.[1];
|
|
71
|
+
return ext ? `${name}(.${ext.toLowerCase()})` : name;
|
|
72
|
+
}
|
|
73
|
+
if (name === 'Skill' && input.skill) return `Skill(${input.skill})`;
|
|
74
|
+
if (name === 'Task' || name === 'Agent') return `${name}(${input.subagent_type ?? 'agent'})`;
|
|
75
|
+
return name.startsWith('mcp__') ? `mcp:${name.split('__')[1]}` : name;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Reads one transcript and returns:
|
|
79
|
+
// turns — one entry per billed API request:
|
|
80
|
+
// { requestId, model, timestamp, input, output, cacheWrite5m, cacheWrite1h, cacheRead }
|
|
81
|
+
// activity — call counts { mcp: {server: n}, skills: {name: n}, web: {tool: n} }
|
|
82
|
+
// mcp/skills/web — per-request cost attribution buckets (see build-receipt.js)
|
|
83
|
+
// digest — what the session was about: cwd, prompts, tool sequence
|
|
84
|
+
// reported — Claude Code's own "cost-state" totals when the transcript has one
|
|
85
|
+
//
|
|
86
|
+
// Claude Code writes one API response as several lines (one per content
|
|
87
|
+
// block, streamed). They share a requestId/message.id; output_tokens grows
|
|
88
|
+
// until the last one, so the final line is the billed usage.
|
|
89
|
+
export function parseTranscript(path) {
|
|
90
|
+
const file = String(path);
|
|
7
91
|
let text;
|
|
8
92
|
try {
|
|
9
|
-
text = readFileSync(
|
|
93
|
+
text = readFileSync(path, 'utf-8');
|
|
10
94
|
} catch {
|
|
11
|
-
return { turns: [], activity:
|
|
95
|
+
return { turns: [], activity: EMPTY_ACTIVITY(), mcp: {}, skills: {}, web: {}, digest: emptyDigest(), reported: null };
|
|
12
96
|
}
|
|
13
97
|
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
98
|
+
const requests = new Map();
|
|
99
|
+
const latest = new Map(); // base id -> newest request carrying it
|
|
100
|
+
const order = [];
|
|
101
|
+
let userCount = 0;
|
|
102
|
+
let splitN = 0;
|
|
103
|
+
const digest = emptyDigest();
|
|
104
|
+
let reported = null;
|
|
105
|
+
let anon = 0;
|
|
18
106
|
|
|
19
107
|
for (const line of text.split('\n')) {
|
|
20
108
|
if (!line.trim()) continue;
|
|
@@ -25,43 +113,119 @@ export function parseTranscript(file) {
|
|
|
25
113
|
continue;
|
|
26
114
|
}
|
|
27
115
|
|
|
116
|
+
if (!digest.cwd && typeof entry.cwd === 'string') digest.cwd = entry.cwd;
|
|
117
|
+
if (!digest.gitBranch && entry.gitBranch) digest.gitBranch = entry.gitBranch;
|
|
118
|
+
if (entry.type === 'ai-title' && entry.aiTitle) digest.aiTitle = entry.aiTitle;
|
|
119
|
+
if (entry.type === 'cost-state' && entry.modelUsage) reported = entry;
|
|
120
|
+
|
|
121
|
+
if (entry.type === 'user') userCount += 1;
|
|
122
|
+
if (entry.type === 'user' && entry.message?.role === 'user') {
|
|
123
|
+
const t = userText(entry.message);
|
|
124
|
+
if (isHumanPrompt(entry, t)) {
|
|
125
|
+
digest.prompts.push({ text: t.slice(0, 2000), timestamp: entry.timestamp ?? null });
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
28
130
|
const msg = entry.message;
|
|
29
|
-
if (!msg || msg.
|
|
131
|
+
if (!msg || msg.role !== 'assistant') continue;
|
|
132
|
+
if (msg.model === '<synthetic>') continue;
|
|
30
133
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
134
|
+
// One API response is streamed as several lines sharing an id. Claude
|
|
135
|
+
// always supplies a unique requestId. A gateway may not: then message.id is
|
|
136
|
+
// all we have, and some servers reuse one constant id for every response,
|
|
137
|
+
// so a tool result in between means the id belongs to a new request.
|
|
138
|
+
const rid = entry.requestId || null;
|
|
139
|
+
const base = rid ?? (msg.id || null);
|
|
140
|
+
let req = base ? latest.get(base) : null;
|
|
141
|
+
if (req && !rid && req.lastUser !== userCount) req = null;
|
|
142
|
+
if (!req) {
|
|
143
|
+
const key = !base ? `${file}#anon${anon++}` : latest.has(base) ? `${file}#${base}#${++splitN}` : base;
|
|
144
|
+
req = { key, model: msg.model ?? 'unknown', timestamp: entry.timestamp ?? null, usage: null, tools: new Map(), skill: null, lastUser: userCount };
|
|
145
|
+
requests.set(key, req);
|
|
146
|
+
order.push(req);
|
|
147
|
+
if (base) latest.set(base, req);
|
|
148
|
+
}
|
|
149
|
+
req.lastUser = userCount;
|
|
150
|
+
if (entry.attributionSkill) req.skill = entry.attributionSkill;
|
|
151
|
+
if (msg.usage && typeof msg.usage === 'object' && (!req.usage || tok(msg.usage.output_tokens ?? msg.usage.completion_tokens) >= tok(req.usage.output_tokens ?? req.usage.completion_tokens))) {
|
|
152
|
+
req.usage = msg.usage;
|
|
153
|
+
req.model = msg.model ?? req.model;
|
|
154
|
+
if (entry.timestamp) req.timestamp = entry.timestamp;
|
|
155
|
+
}
|
|
33
156
|
if (Array.isArray(msg.content)) {
|
|
34
157
|
for (const block of msg.content) {
|
|
35
158
|
if (block.type !== 'tool_use') continue;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} else if (block.name.startsWith('mcp__')) {
|
|
39
|
-
const server = block.name.split('__')[1] ?? 'unknown';
|
|
40
|
-
mcp[server] = (mcp[server] ?? 0) + 1;
|
|
41
|
-
} else if (block.name === 'WebFetch' || block.name === 'WebSearch') {
|
|
42
|
-
web[block.name] = (web[block.name] ?? 0) + 1;
|
|
43
|
-
}
|
|
159
|
+
const id = block.id ?? `${req.key}-${req.tools.size}`;
|
|
160
|
+
if (!req.tools.has(id)) req.tools.set(id, block);
|
|
44
161
|
}
|
|
45
162
|
}
|
|
163
|
+
}
|
|
46
164
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
output: u.output_tokens ?? 0,
|
|
59
|
-
cacheWrite5m: cacheCreation.ephemeral_5m_input_tokens ?? u.cache_creation_input_tokens ?? 0,
|
|
60
|
-
cacheWrite1h: cacheCreation.ephemeral_1h_input_tokens ?? 0,
|
|
61
|
-
cacheRead: u.cache_read_input_tokens ?? 0,
|
|
62
|
-
});
|
|
165
|
+
const turns = [];
|
|
166
|
+
const billed = [];
|
|
167
|
+
for (const req of order) {
|
|
168
|
+
// Some local servers report no usage at all. Keep the request (so the
|
|
169
|
+
// model shows up and can be priced) but mark that its tokens are unknown.
|
|
170
|
+
const n = req.usage ? normalizeUsage(req.usage) : { input: 0, output: 0, cacheWrite5m: 0, cacheWrite1h: 0, cacheRead: 0 };
|
|
171
|
+
const unreported = !(n.input || n.output || n.cacheWrite5m || n.cacheWrite1h || n.cacheRead);
|
|
172
|
+
if (unreported && !req.model) continue;
|
|
173
|
+
const turn = { requestId: req.key, model: req.model, timestamp: req.timestamp, ...n, unreported };
|
|
174
|
+
turns.push(turn);
|
|
175
|
+
billed.push({ req, turn });
|
|
63
176
|
}
|
|
64
|
-
|
|
177
|
+
|
|
178
|
+
const activity = EMPTY_ACTIVITY();
|
|
179
|
+
const mcp = {};
|
|
180
|
+
const skills = {};
|
|
181
|
+
const web = {};
|
|
182
|
+
|
|
183
|
+
billed.forEach(({ req, turn }, i) => {
|
|
184
|
+
const next = billed[i + 1]?.turn ?? null;
|
|
185
|
+
const charge = (bucket) => {
|
|
186
|
+
bucket.calls += 1;
|
|
187
|
+
bucket.outputTokens += turn.output;
|
|
188
|
+
bucket.outputByModel[turn.model] = (bucket.outputByModel[turn.model] ?? 0) + turn.output;
|
|
189
|
+
if (!next) {
|
|
190
|
+
bucket.contextMissing += 1;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
bucket.contextTokens += next.input + next.cacheWrite5m + next.cacheWrite1h;
|
|
194
|
+
const ctx = (bucket.contextByModel[next.model] ??= { input: 0, cacheWrite5m: 0, cacheWrite1h: 0 });
|
|
195
|
+
ctx.input += next.input;
|
|
196
|
+
ctx.cacheWrite5m += next.cacheWrite5m;
|
|
197
|
+
ctx.cacheWrite1h += next.cacheWrite1h;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const blocks = [...req.tools.values()];
|
|
201
|
+
let mcpCharged = false;
|
|
202
|
+
for (const block of blocks) {
|
|
203
|
+
const name = block.name ?? '';
|
|
204
|
+
digest.tools.push(toolStep(block));
|
|
205
|
+
if (name.startsWith('mcp__')) {
|
|
206
|
+
const [, slug = 'unknown', ...rest] = name.split('__');
|
|
207
|
+
const tool = rest.join('__') || 'unknown';
|
|
208
|
+
activity.mcp[slug] = (activity.mcp[slug] ?? 0) + 1;
|
|
209
|
+
const server = (mcp[slug] ??= { displayName: serverDisplayName(slug), tools: {} });
|
|
210
|
+
const bucket = (server.tools[tool] ??= newBucket());
|
|
211
|
+
charge(bucket);
|
|
212
|
+
if (req.skill) bucket.via[req.skill] = (bucket.via[req.skill] ?? 0) + 1;
|
|
213
|
+
mcpCharged = true;
|
|
214
|
+
} else if (name === 'Skill' && block.input?.skill) {
|
|
215
|
+
activity.skills[block.input.skill] = (activity.skills[block.input.skill] ?? 0) + 1;
|
|
216
|
+
} else if (name === 'WebFetch' || name === 'WebSearch') {
|
|
217
|
+
activity.web[name] = (activity.web[name] ?? 0) + 1;
|
|
218
|
+
web[name] = (web[name] ?? 0) + 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (req.skill && !mcpCharged) charge((skills[req.skill] ??= newBucket()));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return { turns, activity, mcp, skills, web, digest, reported };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function emptyDigest() {
|
|
228
|
+
return { cwd: null, gitBranch: null, aiTitle: null, prompts: [], tools: [] };
|
|
65
229
|
}
|
|
66
230
|
|
|
67
231
|
export function timeRange(turns) {
|
|
@@ -70,11 +234,39 @@ export function timeRange(turns) {
|
|
|
70
234
|
}
|
|
71
235
|
|
|
72
236
|
export function mergeActivity(activities) {
|
|
73
|
-
const merged =
|
|
237
|
+
const merged = EMPTY_ACTIVITY();
|
|
74
238
|
for (const a of activities) {
|
|
75
|
-
for (const
|
|
76
|
-
|
|
77
|
-
|
|
239
|
+
for (const kind of ['mcp', 'skills', 'web']) {
|
|
240
|
+
for (const [k, v] of Object.entries(a[kind] ?? {})) merged[kind][k] = (merged[kind][k] ?? 0) + v;
|
|
241
|
+
}
|
|
78
242
|
}
|
|
79
243
|
return merged;
|
|
80
244
|
}
|
|
245
|
+
|
|
246
|
+
function addBucket(into, from) {
|
|
247
|
+
into.calls += from.calls;
|
|
248
|
+
into.outputTokens += from.outputTokens;
|
|
249
|
+
into.contextTokens += from.contextTokens;
|
|
250
|
+
into.contextMissing += from.contextMissing;
|
|
251
|
+
for (const [m, n] of Object.entries(from.outputByModel)) into.outputByModel[m] = (into.outputByModel[m] ?? 0) + n;
|
|
252
|
+
for (const [m, u] of Object.entries(from.contextByModel)) {
|
|
253
|
+
const c = (into.contextByModel[m] ??= { input: 0, cacheWrite5m: 0, cacheWrite1h: 0 });
|
|
254
|
+
c.input += u.input;
|
|
255
|
+
c.cacheWrite5m += u.cacheWrite5m;
|
|
256
|
+
c.cacheWrite1h += u.cacheWrite1h;
|
|
257
|
+
}
|
|
258
|
+
for (const [s, n] of Object.entries(from.via)) into.via[s] = (into.via[s] ?? 0) + n;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function mergeParsed(parsedList) {
|
|
262
|
+
const out = { mcp: {}, skills: {}, web: {} };
|
|
263
|
+
for (const p of parsedList) {
|
|
264
|
+
for (const [slug, server] of Object.entries(p.mcp ?? {})) {
|
|
265
|
+
const into = (out.mcp[slug] ??= { displayName: server.displayName, tools: {} });
|
|
266
|
+
for (const [tool, bucket] of Object.entries(server.tools)) addBucket((into.tools[tool] ??= newBucket()), bucket);
|
|
267
|
+
}
|
|
268
|
+
for (const [name, bucket] of Object.entries(p.skills ?? {})) addBucket((out.skills[name] ??= newBucket()), bucket);
|
|
269
|
+
for (const [k, v] of Object.entries(p.web ?? {})) out.web[k] = (out.web[k] ?? 0) + v;
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|