claude-burn 1.0.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/src/server.js ADDED
@@ -0,0 +1,361 @@
1
+ const http = require('http');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { URL } = require('url');
6
+
7
+ const ACTIVE_THRESHOLD_SEC = 120;
8
+ const HOME_DIR = os.homedir();
9
+
10
+ function parseSession(sessionPath, windowStartISO) {
11
+ const sid = path.basename(sessionPath, '.jsonl');
12
+ const sessionDir = path.join(path.dirname(sessionPath), sid);
13
+
14
+ const files = [{ path: sessionPath, isSubagent: false, name: 'main' }];
15
+ const subagentDir = path.join(sessionDir, 'subagents');
16
+ try {
17
+ if (fs.statSync(subagentDir).isDirectory()) {
18
+ for (const f of fs.readdirSync(subagentDir)) {
19
+ if (f.endsWith('.jsonl')) {
20
+ files.push({
21
+ path: path.join(subagentDir, f),
22
+ isSubagent: true,
23
+ name: path.basename(f, '.jsonl'),
24
+ });
25
+ }
26
+ }
27
+ }
28
+ } catch {}
29
+
30
+ let totalIn = 0, totalOut = 0, cacheRead = 0, cacheCreate = 0, msgCount = 0;
31
+ let userMsgCount = 0, windowTokens = 0;
32
+ let firstTs = null, lastTs = null;
33
+ let model = '';
34
+ let firstUserMsg = '';
35
+ const subagentStats = {};
36
+ const modelStats = {}; // per-model token tracking
37
+ const timeline = [];
38
+
39
+ for (const file of files) {
40
+ const agentName = file.name;
41
+ if (!subagentStats[agentName]) {
42
+ subagentStats[agentName] = { input: 0, output: 0, cache_read: 0, cache_create: 0, total: 0, messages: 0, model: '' };
43
+ }
44
+
45
+ let data;
46
+ try { data = fs.readFileSync(file.path, 'utf8'); } catch { continue; }
47
+
48
+ for (const line of data.split('\n')) {
49
+ if (!line.trim()) continue;
50
+ let d;
51
+ try { d = JSON.parse(line); } catch { continue; }
52
+
53
+ const ts = d.timestamp || '';
54
+ if (ts) {
55
+ if (!firstTs || ts < firstTs) firstTs = ts;
56
+ if (!lastTs || ts > lastTs) lastTs = ts;
57
+ }
58
+
59
+ const msg = (d.message || {});
60
+
61
+ if (!file.isSubagent && msg.role === 'user') {
62
+ userMsgCount++;
63
+ if (!firstUserMsg) {
64
+ const content = msg.content;
65
+ if (Array.isArray(content)) {
66
+ for (const block of content) {
67
+ if (block && block.type === 'text' && block.text) {
68
+ firstUserMsg = block.text.slice(0, 80);
69
+ break;
70
+ }
71
+ }
72
+ } else if (typeof content === 'string') {
73
+ firstUserMsg = content.slice(0, 80);
74
+ }
75
+ }
76
+ }
77
+
78
+ const usage = msg.usage;
79
+ if (!usage) continue;
80
+
81
+ const inp = usage.input_tokens || 0;
82
+ const out = usage.output_tokens || 0;
83
+ const cr = usage.cache_read_input_tokens || 0;
84
+ const cc = usage.cache_creation_input_tokens || 0;
85
+ const entryModel = msg.model || '';
86
+ if (entryModel) {
87
+ model = entryModel;
88
+ if (!subagentStats[agentName].model) subagentStats[agentName].model = entryModel;
89
+ }
90
+
91
+ // Track per-model tokens
92
+ const mkey = entryModel || 'unknown';
93
+ if (!modelStats[mkey]) modelStats[mkey] = { input: 0, output: 0, cache_read: 0, cache_create: 0 };
94
+ modelStats[mkey].input += inp;
95
+ modelStats[mkey].output += out;
96
+ modelStats[mkey].cache_read += cr;
97
+ modelStats[mkey].cache_create += cc;
98
+
99
+ totalIn += inp;
100
+ totalOut += out;
101
+ cacheRead += cr;
102
+ cacheCreate += cc;
103
+ msgCount++;
104
+
105
+ if (windowStartISO && ts && ts >= windowStartISO) {
106
+ windowTokens += inp + out + cr + cc;
107
+ }
108
+
109
+ const sa = subagentStats[agentName];
110
+ sa.input += inp;
111
+ sa.output += out;
112
+ sa.cache_read += cr;
113
+ sa.cache_create += cc;
114
+ sa.total += inp + out + cr + cc;
115
+ sa.messages++;
116
+
117
+ if (ts) {
118
+ timeline.push({ ts, tokens: inp + out + cr + cc, agent: agentName });
119
+ }
120
+ }
121
+ }
122
+
123
+ const totalTokens = totalIn + totalOut + cacheRead + cacheCreate;
124
+
125
+ // Sort timeline for duration calculations
126
+ timeline.sort((a, b) => (a.ts > b.ts ? 1 : -1));
127
+
128
+ let durationSec = 0, activeSec = 0, burnRate = 0, recentBurnRate = 0;
129
+ const IDLE_GAP_SEC = 120; // gaps > 2 min count as idle
130
+
131
+ if (firstTs && lastTs) {
132
+ try {
133
+ const t1 = new Date(firstTs).getTime();
134
+ const t2 = new Date(lastTs).getTime();
135
+ durationSec = Math.max((t2 - t1) / 1000, 1);
136
+
137
+ // Calculate active time (exclude idle gaps > 2 min)
138
+ activeSec = 0;
139
+ for (let i = 1; i < timeline.length; i++) {
140
+ const prev = new Date(timeline[i - 1].ts).getTime();
141
+ const curr = new Date(timeline[i].ts).getTime();
142
+ const gap = (curr - prev) / 1000;
143
+ if (gap <= IDLE_GAP_SEC) {
144
+ activeSec += gap;
145
+ }
146
+ }
147
+ activeSec = Math.max(activeSec, 1);
148
+
149
+ burnRate = totalTokens / (activeSec / 60);
150
+
151
+ // Recent burn rate: tokens in last 3 minutes of activity
152
+ const recentCutoff = new Date(t2 - 3 * 60 * 1000).toISOString();
153
+ let recentTokens = 0;
154
+ let recentFirst = null;
155
+ for (const entry of timeline) {
156
+ if (entry.ts >= recentCutoff) {
157
+ recentTokens += entry.tokens;
158
+ if (!recentFirst) recentFirst = entry.ts;
159
+ }
160
+ }
161
+ if (recentFirst) {
162
+ const recentDuration = Math.max((t2 - new Date(recentFirst).getTime()) / 1000 / 60, 0.1);
163
+ recentBurnRate = recentTokens / recentDuration;
164
+ }
165
+ } catch {}
166
+ }
167
+
168
+ let isActive = false;
169
+ if (lastTs) {
170
+ try {
171
+ const t2 = new Date(lastTs).getTime();
172
+ isActive = (Date.now() - t2) / 1000 < ACTIVE_THRESHOLD_SEC;
173
+ } catch {}
174
+ }
175
+
176
+ // Clean project name from directory
177
+ const projectDirName = path.basename(path.dirname(sessionPath));
178
+ const homeEscaped = HOME_DIR.replace(/\//g, '-').replace(/^-/, '');
179
+ const project = projectDirName.replace(`-${homeEscaped}-`, '~/').replace(/-/g, '/');
180
+
181
+ // Sample timeline (already sorted above)
182
+ const cumulativeTimeline = [];
183
+ let cum = 0;
184
+ const step = Math.max(1, Math.floor(timeline.length / 200));
185
+ for (let i = 0; i < timeline.length; i++) {
186
+ cum += timeline[i].tokens;
187
+ if (i % step === 0 || i === timeline.length - 1) {
188
+ cumulativeTimeline.push({ ts: timeline[i].ts, cumulative: cum, agent: timeline[i].agent });
189
+ }
190
+ }
191
+
192
+ return {
193
+ id: sid,
194
+ project,
195
+ title: firstUserMsg || '(no title)',
196
+ model,
197
+ first_ts: firstTs,
198
+ last_ts: lastTs,
199
+ duration_sec: Math.floor(durationSec),
200
+ active_sec: Math.floor(activeSec),
201
+ is_active: isActive,
202
+ msg_count: msgCount,
203
+ user_msg_count: userMsgCount,
204
+ input_tokens: totalIn,
205
+ output_tokens: totalOut,
206
+ cache_read_tokens: cacheRead,
207
+ cache_create_tokens: cacheCreate,
208
+ total_tokens: totalTokens,
209
+ burn_rate_per_min: Math.floor(burnRate),
210
+ recent_burn_rate_per_min: Math.floor(recentBurnRate),
211
+ window_tokens: windowTokens,
212
+ subagent_count: Object.keys(subagentStats).filter(k => k !== 'main').length,
213
+ subagents: subagentStats,
214
+ model_stats: modelStats,
215
+ timeline: cumulativeTimeline,
216
+ };
217
+ }
218
+
219
+ function getAllSessions(dataDir, hoursBack, projectFilter, windowStartISO) {
220
+ const cutoff = Date.now() - hoursBack * 3600 * 1000;
221
+ const sessions = [];
222
+
223
+ let projectDirs;
224
+ try { projectDirs = fs.readdirSync(dataDir); } catch { return []; }
225
+
226
+ for (const dirName of projectDirs) {
227
+ const projectDir = path.join(dataDir, dirName);
228
+ try { if (!fs.statSync(projectDir).isDirectory()) continue; } catch { continue; }
229
+ if (projectFilter && !projectDir.includes(projectFilter)) continue;
230
+
231
+ let files;
232
+ try { files = fs.readdirSync(projectDir); } catch { continue; }
233
+
234
+ for (const f of files) {
235
+ if (!f.endsWith('.jsonl')) continue;
236
+ const fp = path.join(projectDir, f);
237
+ try {
238
+ const stat = fs.statSync(fp);
239
+ if (stat.mtimeMs < cutoff) continue;
240
+ sessions.push(parseSession(fp, windowStartISO));
241
+ } catch (e) {
242
+ console.error(`Error parsing ${fp}: ${e.message}`);
243
+ }
244
+ }
245
+ }
246
+
247
+ sessions.sort((a, b) => (b.last_ts || '').localeCompare(a.last_ts || ''));
248
+ return sessions;
249
+ }
250
+
251
+ function createServer({ port, dataDir }) {
252
+ const publicDir = path.join(__dirname, '..', 'public');
253
+
254
+ return http.createServer((req, res) => {
255
+ const parsed = new URL(req.url, `http://localhost:${port}`);
256
+ const pathname = parsed.pathname;
257
+ const params = parsed.searchParams;
258
+
259
+ if (pathname === '/api/sessions') {
260
+ const hours = parseInt(params.get('hours') || '24', 10);
261
+ const project = params.get('project') || null;
262
+ const windowMin = parseInt(params.get('window_minutes') || '0', 10);
263
+ const windowPct = parseFloat(params.get('window_pct') || '0');
264
+
265
+ let windowStartISO = null;
266
+ if (windowMin > 0) {
267
+ windowStartISO = new Date(Date.now() - windowMin * 60 * 1000).toISOString();
268
+ }
269
+
270
+ const data = getAllSessions(dataDir, hours, project, windowStartISO);
271
+
272
+ const totalTokens = data.reduce((s, d) => s + d.total_tokens, 0);
273
+ const activeCount = data.filter(d => d.is_active).length;
274
+
275
+ // Calculate API-equivalent cost per model
276
+ const PRICING = {
277
+ 'opus': { input: 5, output: 25, cache_read: 0.5, cache_create: 6.25 },
278
+ 'sonnet': { input: 3, output: 15, cache_read: 0.3, cache_create: 3.75 },
279
+ 'haiku': { input: 1, output: 5, cache_read: 0.1, cache_create: 1.25 },
280
+ };
281
+ function getPricing(modelName) {
282
+ if (modelName.includes('haiku')) return PRICING.haiku;
283
+ if (modelName.includes('sonnet')) return PRICING.sonnet;
284
+ return PRICING.opus;
285
+ }
286
+
287
+ let costInput = 0, costOutput = 0, costCR = 0, costCC = 0;
288
+ for (const session of data) {
289
+ for (const [mname, ms] of Object.entries(session.model_stats || {})) {
290
+ const p = getPricing(mname);
291
+ costInput += ms.input / 1e6 * p.input;
292
+ costOutput += ms.output / 1e6 * p.output;
293
+ costCR += ms.cache_read / 1e6 * p.cache_read;
294
+ costCC += ms.cache_create / 1e6 * p.cache_create;
295
+ }
296
+ }
297
+ const apiCost = {
298
+ input: Math.round(costInput * 100) / 100,
299
+ output: Math.round(costOutput * 100) / 100,
300
+ cache_read: Math.round(costCR * 100) / 100,
301
+ cache_create: Math.round(costCC * 100) / 100,
302
+ };
303
+ apiCost.total = Math.round((apiCost.input + apiCost.output + apiCost.cache_read + apiCost.cache_create) * 100) / 100;
304
+
305
+ jsonResponse(res, {
306
+ generated_at: new Date().toISOString(),
307
+ summary: {
308
+ total_sessions: data.length,
309
+ active_sessions: activeCount,
310
+ total_tokens: totalTokens,
311
+ api_cost: apiCost,
312
+ },
313
+ sessions: data,
314
+ });
315
+
316
+ } else if (pathname === '/api/session') {
317
+ const sid = params.get('id');
318
+ if (!sid) return jsonResponse(res, { error: 'missing id' }, 400);
319
+
320
+ let projectDirs;
321
+ try { projectDirs = fs.readdirSync(dataDir); } catch { return jsonResponse(res, { error: 'cannot read data dir' }, 500); }
322
+
323
+ for (const dirName of projectDirs) {
324
+ const fp = path.join(dataDir, dirName, `${sid}.jsonl`);
325
+ try {
326
+ if (fs.existsSync(fp)) {
327
+ return jsonResponse(res, parseSession(fp));
328
+ }
329
+ } catch {}
330
+ }
331
+ jsonResponse(res, { error: 'session not found' }, 404);
332
+
333
+ } else if (pathname === '/' || pathname === '/index.html') {
334
+ const htmlPath = path.join(publicDir, 'index.html');
335
+ try {
336
+ const html = fs.readFileSync(htmlPath, 'utf8');
337
+ res.writeHead(200, { 'Content-Type': 'text/html', 'Content-Length': Buffer.byteLength(html) });
338
+ res.end(html);
339
+ } catch {
340
+ res.writeHead(404);
341
+ res.end('index.html not found');
342
+ }
343
+
344
+ } else {
345
+ res.writeHead(404);
346
+ res.end('Not found');
347
+ }
348
+ });
349
+ }
350
+
351
+ function jsonResponse(res, data, code = 200) {
352
+ const body = JSON.stringify(data);
353
+ res.writeHead(code, {
354
+ 'Content-Type': 'application/json',
355
+ 'Content-Length': Buffer.byteLength(body),
356
+ 'Access-Control-Allow-Origin': '*',
357
+ });
358
+ res.end(body);
359
+ }
360
+
361
+ module.exports = { createServer, getAllSessions, parseSession };