claude-cli-analytics 0.0.4 → 0.0.5
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/dist/client/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>claude-analytics</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-B3X-FepG.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-BkOIudNK.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/dist/server/analyzer.js
CHANGED
|
@@ -2,8 +2,28 @@
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { parseJsonlFile, getProjectsList } from './parser.js';
|
|
5
|
+
// ── Model cost multiplier (Sonnet = 1.0 baseline) ──
|
|
6
|
+
const MODEL_COST_MULTIPLIER = {
|
|
7
|
+
haiku: 0.25,
|
|
8
|
+
sonnet: 1.0,
|
|
9
|
+
opus: 5.0,
|
|
10
|
+
};
|
|
11
|
+
function getModelCostMultiplier(modelName) {
|
|
12
|
+
const lower = modelName.toLowerCase();
|
|
13
|
+
for (const [key, value] of Object.entries(MODEL_COST_MULTIPLIER)) {
|
|
14
|
+
if (lower.includes(key))
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
return 1.0; // default to sonnet
|
|
18
|
+
}
|
|
19
|
+
function extractModelName(record) {
|
|
20
|
+
const model = record.message?.model;
|
|
21
|
+
if (model && typeof model === 'string')
|
|
22
|
+
return model;
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
5
25
|
// ── Helper: compute efficiency score ──
|
|
6
|
-
function computeEfficiencyScore(cacheHitRate, toolErrorRate, toolErrorCount, duplicateReadRate, readEditRatio, repeatedEditRate, sessionExit, tokensPerEdit) {
|
|
26
|
+
function computeEfficiencyScore(cacheHitRate, toolErrorRate, toolErrorCount, duplicateReadRate, readEditRatio, repeatedEditRate, sessionExit, tokensPerEdit, modelName = 'sonnet') {
|
|
7
27
|
let score = 0;
|
|
8
28
|
// 1. Cache efficiency (30 pts)
|
|
9
29
|
score += cacheHitRate >= 95 ? 30 : cacheHitRate >= 90 ? 25 : cacheHitRate >= 80 ? 20 : cacheHitRate >= 60 ? 10 : 0;
|
|
@@ -15,10 +35,11 @@ function computeEfficiencyScore(cacheHitRate, toolErrorRate, toolErrorCount, dup
|
|
|
15
35
|
+ (repeatedEditRate < 10 ? 5 : repeatedEditRate < 20 ? 3 : 0);
|
|
16
36
|
// 4. Session termination (10 pts)
|
|
17
37
|
score += sessionExit === 'clean' ? 10 : sessionExit === 'unknown' ? 5 : 0;
|
|
18
|
-
// 5. Cost efficiency (15 pts)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
38
|
+
// 5. Cost efficiency (15 pts) — normalized by model cost multiplier
|
|
39
|
+
const costPerEdit = tokensPerEdit * getModelCostMultiplier(modelName);
|
|
40
|
+
score += costPerEdit > 0 && costPerEdit < 30000 ? 15
|
|
41
|
+
: costPerEdit < 50000 ? 10
|
|
42
|
+
: costPerEdit < 80000 ? 5 : 0;
|
|
22
43
|
// 6. Error penalty
|
|
23
44
|
score = Math.max(0, score - (toolErrorCount * 5));
|
|
24
45
|
return score;
|
|
@@ -62,12 +83,13 @@ function computeGrade(cacheHitRate, toolErrorCount, requests, readEditRatio) {
|
|
|
62
83
|
};
|
|
63
84
|
}
|
|
64
85
|
// ── Helper: compute danger level ──
|
|
65
|
-
function getDangerLevel(avgContextSize) {
|
|
66
|
-
|
|
86
|
+
function getDangerLevel(avgContextSize, modelName = 'sonnet') {
|
|
87
|
+
const normalizedCost = avgContextSize * getModelCostMultiplier(modelName);
|
|
88
|
+
if (normalizedCost < 10000)
|
|
67
89
|
return 'optimal';
|
|
68
|
-
if (
|
|
90
|
+
if (normalizedCost < 20000)
|
|
69
91
|
return 'safe';
|
|
70
|
-
if (
|
|
92
|
+
if (normalizedCost > 50000)
|
|
71
93
|
return 'critical';
|
|
72
94
|
return 'caution';
|
|
73
95
|
}
|
|
@@ -126,31 +148,38 @@ export function getSessionsList(projectsDir, projectPath, startDate, endDate) {
|
|
|
126
148
|
let recordIndex = 0;
|
|
127
149
|
let toolResultCount = 0, toolErrorCount = 0;
|
|
128
150
|
let humanTurns = 0, autoTurns = 0, commandTurns = 0;
|
|
151
|
+
let sessionModel = 'sonnet';
|
|
129
152
|
for (const record of records) {
|
|
130
|
-
if (record.type === 'assistant'
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
153
|
+
if (record.type === 'assistant') {
|
|
154
|
+
// Extract model name from first assistant message
|
|
155
|
+
const model = extractModelName(record);
|
|
156
|
+
if (model && sessionModel === 'sonnet')
|
|
157
|
+
sessionModel = model;
|
|
158
|
+
if (record.message?.usage) {
|
|
159
|
+
const usage = record.message.usage;
|
|
160
|
+
inputTokens += usage.input_tokens || 0;
|
|
161
|
+
// outputTokens += usage.output_tokens || 0;
|
|
162
|
+
cacheRead += usage.cache_read_input_tokens || 0;
|
|
163
|
+
requests++;
|
|
164
|
+
if (record.message?.content && Array.isArray(record.message.content)) {
|
|
165
|
+
for (const block of record.message.content) {
|
|
166
|
+
if (block.type === 'tool_use') {
|
|
167
|
+
const toolName = block.name?.toLowerCase() || '';
|
|
168
|
+
if (toolName.includes('read') || toolName.includes('view') || toolName.includes('grep') || toolName.includes('glob') || toolName.includes('list')) {
|
|
169
|
+
readCount++;
|
|
170
|
+
const input = block.input || {};
|
|
171
|
+
const fp = input.file_path || input.path || '';
|
|
172
|
+
if (firstSpecReadIndex === -1 && (fp.includes('.claude/') || fp.includes('CLAUDE.md'))) {
|
|
173
|
+
firstSpecReadIndex = recordIndex;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (toolName.includes('edit') || toolName.includes('write') || toolName.includes('replace')) {
|
|
177
|
+
editCount++;
|
|
178
|
+
const input = block.input;
|
|
179
|
+
const filePath = input?.file_path || input?.filePath;
|
|
180
|
+
if (filePath)
|
|
181
|
+
filesEdited.push(filePath);
|
|
146
182
|
}
|
|
147
|
-
}
|
|
148
|
-
else if (toolName.includes('edit') || toolName.includes('write') || toolName.includes('replace')) {
|
|
149
|
-
editCount++;
|
|
150
|
-
const input = block.input;
|
|
151
|
-
const filePath = input?.file_path || input?.filePath;
|
|
152
|
-
if (filePath)
|
|
153
|
-
filesEdited.push(filePath);
|
|
154
183
|
}
|
|
155
184
|
}
|
|
156
185
|
}
|
|
@@ -224,8 +253,7 @@ export function getSessionsList(projectsDir, projectPath, startDate, endDate) {
|
|
|
224
253
|
lastRecordRaw?.type === 'system' ? 'forced' : 'unknown';
|
|
225
254
|
const totalContextSent = inputTokens + cacheRead;
|
|
226
255
|
const avgContextSize = requests > 0 ? Math.round(totalContextSent / requests) : 0;
|
|
227
|
-
const dangerLevel = getDangerLevel(avgContextSize);
|
|
228
|
-
const limitImpact = Math.round((totalContextSent / 44000) * 100);
|
|
256
|
+
const dangerLevel = getDangerLevel(avgContextSize, sessionModel);
|
|
229
257
|
const uniqueFilesRead = new Set(filesReadList).size;
|
|
230
258
|
const duplicateReads = filesReadList.length - uniqueFilesRead;
|
|
231
259
|
const duplicateReadRate = filesReadList.length > 0 ? Math.round((duplicateReads / filesReadList.length) * 100) : 0;
|
|
@@ -236,7 +264,7 @@ export function getSessionsList(projectsDir, projectPath, startDate, endDate) {
|
|
|
236
264
|
const tokensPerEdit = editCount > 0 ? Math.round(totalContextSent / editCount) : 0;
|
|
237
265
|
const toolErrorRate = toolResultCount > 0 ? Math.round((toolErrorCount / toolResultCount) * 1000) / 10 : 0;
|
|
238
266
|
const cacheHitRate = (inputTokens + cacheRead) > 0 ? (cacheRead / (inputTokens + cacheRead)) * 100 : 0;
|
|
239
|
-
const efficiencyScore = computeEfficiencyScore(cacheHitRate, toolErrorRate, toolErrorCount, duplicateReadRate, readEditRatio, repeatedEditRate, sessionExit, tokensPerEdit);
|
|
267
|
+
const efficiencyScore = computeEfficiencyScore(cacheHitRate, toolErrorRate, toolErrorCount, duplicateReadRate, readEditRatio, repeatedEditRate, sessionExit, tokensPerEdit, sessionModel);
|
|
240
268
|
const sei = computeSEI(postSpecReadTotal, postSpecReadErrors, cacheRead, new Set(specFilesRead).size);
|
|
241
269
|
const grade = computeGrade(cacheHitRate, toolErrorCount, requests, readEditRatio);
|
|
242
270
|
// Timestamps & duration
|
|
@@ -262,7 +290,6 @@ export function getSessionsList(projectsDir, projectPath, startDate, endDate) {
|
|
|
262
290
|
cache_hit_rate: Math.round(cacheHitRate * 10) / 10,
|
|
263
291
|
spec_trigger_rate: specTriggerRate,
|
|
264
292
|
danger_level: dangerLevel,
|
|
265
|
-
limit_impact: limitImpact,
|
|
266
293
|
},
|
|
267
294
|
tool_efficiency: {
|
|
268
295
|
read_edit_ratio: readEditRatio,
|
|
@@ -280,10 +307,8 @@ export function getSessionsList(projectsDir, projectPath, startDate, endDate) {
|
|
|
280
307
|
human_turns: humanTurns,
|
|
281
308
|
auto_turns: effectiveAutoTurns,
|
|
282
309
|
autonomy_rate: autonomyRate,
|
|
283
|
-
ht_per_edit: editCount > 0 ? Math.round((humanTurns / editCount) * 10) / 10 : 0,
|
|
284
310
|
},
|
|
285
311
|
workflow_autonomy: {
|
|
286
|
-
repeated_edit_rate: repeatedEditRate,
|
|
287
312
|
efficiency_score: efficiencyScore,
|
|
288
313
|
sei,
|
|
289
314
|
},
|
|
@@ -295,15 +320,14 @@ export function getSessionsList(projectsDir, projectPath, startDate, endDate) {
|
|
|
295
320
|
sessions.push({
|
|
296
321
|
session_id: file.replace('.jsonl', ''),
|
|
297
322
|
project: project.name,
|
|
323
|
+
model: sessionModel,
|
|
298
324
|
start_time: startTs,
|
|
299
325
|
end_time: endTs,
|
|
300
326
|
git_branch: firstTimestampRecord?.gitBranch || '',
|
|
301
327
|
total_requests: requests,
|
|
302
328
|
avg_context_size: avgContextSize,
|
|
303
329
|
danger_level: dangerLevel,
|
|
304
|
-
limit_impact: limitImpact,
|
|
305
330
|
total_context_tokens: totalContextSent,
|
|
306
|
-
total_output_tokens: 0,
|
|
307
331
|
duplicate_read_rate: duplicateReadRate,
|
|
308
332
|
read_edit_ratio: readEditRatio,
|
|
309
333
|
repeated_edit_rate: repeatedEditRate,
|
|
@@ -318,7 +342,7 @@ export function getSessionsList(projectsDir, projectPath, startDate, endDate) {
|
|
|
318
342
|
human_turns: humanTurns,
|
|
319
343
|
auto_turns: effectiveAutoTurns,
|
|
320
344
|
command_turns: commandTurns,
|
|
321
|
-
|
|
345
|
+
edit_count: editCount,
|
|
322
346
|
spec_efficiency: sei,
|
|
323
347
|
grade_breakdown: grade.breakdown,
|
|
324
348
|
anthropic_metrics: anthropicMetrics,
|
|
@@ -377,6 +401,7 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
377
401
|
let firstSpecReadIndex = -1;
|
|
378
402
|
let postSpecReadTotal = 0, postSpecReadErrors = 0;
|
|
379
403
|
let recordIndex = 0;
|
|
404
|
+
let sessionModel = 'sonnet';
|
|
380
405
|
const toolIdMap = new Map();
|
|
381
406
|
const errorMap = new Map();
|
|
382
407
|
// Track pending questions from interactive tools (AskFollowupQuestion etc.)
|
|
@@ -384,6 +409,8 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
384
409
|
// Track which files were read by the assistant via tool_use
|
|
385
410
|
// so we can deduplicate them from user's "context load" display
|
|
386
411
|
let lastAssistantReadFiles = new Set();
|
|
412
|
+
// ── Skill tracking: detect slash commands and associate subsequent work ──
|
|
413
|
+
let activeSkill = null;
|
|
387
414
|
let messageId = 0;
|
|
388
415
|
for (const record of records) {
|
|
389
416
|
if (record.type === 'user') {
|
|
@@ -405,6 +432,8 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
405
432
|
const cmdArgs = argsMatch ? argsMatch[1].trim() : '';
|
|
406
433
|
content = cmdArgs ? `${cmdName} ${cmdArgs}` : cmdName;
|
|
407
434
|
messageSubtype = 'command';
|
|
435
|
+
// Set active skill from slash command
|
|
436
|
+
activeSkill = cmdMatch ? cmdMatch[1].replace(/^\//, '') : null;
|
|
408
437
|
}
|
|
409
438
|
else if (raw.startsWith('This session is being continued')) {
|
|
410
439
|
content = '(세션 이어하기 — 이전 대화 요약 자동 삽입)';
|
|
@@ -413,6 +442,8 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
413
442
|
else {
|
|
414
443
|
content = raw;
|
|
415
444
|
messageSubtype = 'human';
|
|
445
|
+
// User typed a message → reset active skill
|
|
446
|
+
activeSkill = null;
|
|
416
447
|
}
|
|
417
448
|
}
|
|
418
449
|
else if (Array.isArray(msgContent)) {
|
|
@@ -495,11 +526,16 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
495
526
|
timestamp: record.timestamp || '',
|
|
496
527
|
content,
|
|
497
528
|
files_read: fileList,
|
|
498
|
-
tool_uses: toolUses
|
|
529
|
+
tool_uses: toolUses,
|
|
530
|
+
skill_name: messageSubtype === 'command' ? activeSkill || undefined : undefined,
|
|
499
531
|
});
|
|
500
532
|
}
|
|
501
533
|
}
|
|
502
534
|
else if (record.type === 'assistant') {
|
|
535
|
+
// Extract model name from first assistant message
|
|
536
|
+
const model = extractModelName(record);
|
|
537
|
+
if (model && sessionModel === 'sonnet')
|
|
538
|
+
sessionModel = model;
|
|
503
539
|
const usage = record.message?.usage;
|
|
504
540
|
const content = [];
|
|
505
541
|
const toolUses = [];
|
|
@@ -520,6 +556,7 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
520
556
|
const nameLower = toolName.toLowerCase();
|
|
521
557
|
if (nameLower === 'read' || nameLower === 'view') {
|
|
522
558
|
const fp = input.file_path || input.filePath || '';
|
|
559
|
+
const isSpec = fp.includes('.claude/') || fp.includes('CLAUDE.md');
|
|
523
560
|
if (fp) {
|
|
524
561
|
detail = fp;
|
|
525
562
|
// Track that this file was read by assistant tool
|
|
@@ -543,18 +580,24 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
543
580
|
firstSpecReadIndex = recordIndex;
|
|
544
581
|
}
|
|
545
582
|
}
|
|
583
|
+
toolUses.push({ name: toolName, detail: detail || undefined, category: isSpec ? 'read_spec' : 'read_code' });
|
|
584
|
+
continue;
|
|
546
585
|
}
|
|
547
586
|
else if (nameLower === 'edit' || nameLower === 'replace') {
|
|
548
587
|
detail = input.file_path || input.filePath || '';
|
|
549
588
|
editCount++;
|
|
550
589
|
if (detail)
|
|
551
590
|
filesEdited.push(detail);
|
|
591
|
+
toolUses.push({ name: toolName, detail: detail || undefined, category: 'edit' });
|
|
592
|
+
continue;
|
|
552
593
|
}
|
|
553
594
|
else if (nameLower === 'write') {
|
|
554
595
|
detail = input.file_path || input.filePath || '';
|
|
555
596
|
editCount++;
|
|
556
597
|
if (detail)
|
|
557
598
|
filesEdited.push(detail);
|
|
599
|
+
toolUses.push({ name: toolName, detail: detail || undefined, category: 'edit' });
|
|
600
|
+
continue;
|
|
558
601
|
}
|
|
559
602
|
else if (nameLower === 'bash') {
|
|
560
603
|
detail = input.command || '';
|
|
@@ -625,7 +668,15 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
625
668
|
filesEdited.push(detail);
|
|
626
669
|
}
|
|
627
670
|
}
|
|
628
|
-
toolUses.push({
|
|
671
|
+
toolUses.push({
|
|
672
|
+
name: toolName, detail: detail || undefined,
|
|
673
|
+
category: nameLower.includes('read') || nameLower.includes('view') || nameLower.includes('list')
|
|
674
|
+
? (detail && (detail.includes('.claude/') || detail.includes('CLAUDE.md')) ? 'read_spec' : 'read_code')
|
|
675
|
+
: nameLower.includes('grep') || nameLower.includes('glob') || nameLower.includes('search')
|
|
676
|
+
? 'search'
|
|
677
|
+
: nameLower.includes('edit') || nameLower.includes('write') || nameLower.includes('replace')
|
|
678
|
+
? 'edit' : 'other'
|
|
679
|
+
});
|
|
629
680
|
}
|
|
630
681
|
}
|
|
631
682
|
}
|
|
@@ -642,7 +693,8 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
642
693
|
content: content.join('\n'),
|
|
643
694
|
usage,
|
|
644
695
|
files_read: [],
|
|
645
|
-
tool_uses: toolUses
|
|
696
|
+
tool_uses: toolUses,
|
|
697
|
+
skill_name: activeSkill || undefined,
|
|
646
698
|
});
|
|
647
699
|
}
|
|
648
700
|
recordIndex++;
|
|
@@ -655,8 +707,7 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
655
707
|
messages.length > 0 && messages[messages.length - 1].type === 'assistant' ? 'clean' : 'unknown';
|
|
656
708
|
const totalContextSent = inputTokens + cacheRead;
|
|
657
709
|
const avgContextSize = requests > 0 ? Math.round(totalContextSent / requests) : 0;
|
|
658
|
-
const dangerLevel = getDangerLevel(avgContextSize);
|
|
659
|
-
const limitImpact = Math.round((totalContextSent / 44000) * 100);
|
|
710
|
+
const dangerLevel = getDangerLevel(avgContextSize, sessionModel);
|
|
660
711
|
const readEditRatio = editCount > 0 ? Math.round((readCount / editCount) * 10) / 10 : readCount;
|
|
661
712
|
const uniqueFilesRead = new Set(allReadFiles).size;
|
|
662
713
|
const duplicateReads = allReadFiles.length - uniqueFilesRead;
|
|
@@ -675,13 +726,14 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
675
726
|
const repeatedEditRate = filesEdited.length > 0 ? Math.round((repeatedEdits / filesEdited.length) * 100) : 0;
|
|
676
727
|
const tokensPerEdit = editCount > 0 ? Math.round(totalContextSent / editCount) : 0;
|
|
677
728
|
const cacheHitRate = (inputTokens + cacheRead) > 0 ? (cacheRead / (inputTokens + cacheRead)) * 100 : 0;
|
|
678
|
-
const efficiencyScore = computeEfficiencyScore(cacheHitRate, toolErrorRate, toolErrorCount, duplicateReadRate, readEditRatio, repeatedEditRate, sessionExit, tokensPerEdit);
|
|
729
|
+
const efficiencyScore = computeEfficiencyScore(cacheHitRate, toolErrorRate, toolErrorCount, duplicateReadRate, readEditRatio, repeatedEditRate, sessionExit, tokensPerEdit, sessionModel);
|
|
679
730
|
const specFilesReadList = allReadFiles.filter(f => f.includes('.claude/') || f.includes('CLAUDE.md'));
|
|
680
731
|
const sei = computeSEI(postSpecReadTotal, postSpecReadErrors, cacheRead, specFilesReadList.length);
|
|
681
732
|
const grade = computeGrade(cacheHitRate, toolErrorCount, requests, readEditRatio);
|
|
682
733
|
return {
|
|
683
734
|
session_id: sessionId,
|
|
684
735
|
project: foundProject,
|
|
736
|
+
model: sessionModel,
|
|
685
737
|
start_time: firstRecord.timestamp || '',
|
|
686
738
|
end_time: lastRecord.timestamp || '',
|
|
687
739
|
git_branch: firstRecord?.gitBranch || '',
|
|
@@ -690,7 +742,6 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
690
742
|
total_requests: requests,
|
|
691
743
|
avg_context_size: avgContextSize,
|
|
692
744
|
danger_level: dangerLevel,
|
|
693
|
-
limit_impact: limitImpact,
|
|
694
745
|
total_context_tokens: totalContextSent,
|
|
695
746
|
files_read: [...new Set(filesRead)],
|
|
696
747
|
spec_files_read: [...new Set(specFilesRead)]
|
|
@@ -716,7 +767,10 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
716
767
|
+ (readEditRatio >= 5 && readEditRatio <= 20 ? 10 : readEditRatio >= 3 ? 5 : 0)
|
|
717
768
|
+ (repeatedEditRate < 10 ? 5 : repeatedEditRate < 20 ? 3 : 0),
|
|
718
769
|
termination: sessionExit === 'clean' ? 10 : sessionExit === 'unknown' ? 5 : 0,
|
|
719
|
-
cost_efficiency:
|
|
770
|
+
cost_efficiency: (() => {
|
|
771
|
+
const costPerEdit = tokensPerEdit * getModelCostMultiplier(sessionModel);
|
|
772
|
+
return costPerEdit > 0 && costPerEdit < 30000 ? 15 : costPerEdit < 50000 ? 10 : costPerEdit < 80000 ? 5 : 0;
|
|
773
|
+
})()
|
|
720
774
|
},
|
|
721
775
|
spec_efficiency: sei,
|
|
722
776
|
grade_breakdown: grade.breakdown
|
|
@@ -735,7 +789,6 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
735
789
|
cache_hit_rate: Math.round(cacheHitRate * 10) / 10,
|
|
736
790
|
spec_trigger_rate: specTriggerRate,
|
|
737
791
|
danger_level: dangerLevel,
|
|
738
|
-
limit_impact: limitImpact,
|
|
739
792
|
},
|
|
740
793
|
tool_efficiency: {
|
|
741
794
|
read_edit_ratio: readEditRatio,
|
|
@@ -754,10 +807,8 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
754
807
|
human_turns: humanTurns,
|
|
755
808
|
auto_turns: autoTurns,
|
|
756
809
|
autonomy_rate: autonomyRate,
|
|
757
|
-
ht_per_edit: editCount > 0 ? Math.round((humanTurns / editCount) * 10) / 10 : 0,
|
|
758
810
|
},
|
|
759
811
|
workflow_autonomy: {
|
|
760
|
-
repeated_edit_rate: repeatedEditRate,
|
|
761
812
|
efficiency_score: Math.round(efficiencyScore),
|
|
762
813
|
sei,
|
|
763
814
|
},
|
|
@@ -775,7 +826,7 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
|
|
|
775
826
|
export function getAnalytics(projectsDir, projectPath, startDate, endDate) {
|
|
776
827
|
const sessions = getSessionsList(projectsDir, projectPath, startDate, endDate);
|
|
777
828
|
const projects = getProjectsList(projectsDir);
|
|
778
|
-
let totalInput = 0,
|
|
829
|
+
let totalInput = 0, totalCacheRead = 0;
|
|
779
830
|
const projectsToScan = getProjectsToScan(projectsDir, projectPath);
|
|
780
831
|
for (const projectDir of projectsToScan) {
|
|
781
832
|
if (!fs.existsSync(projectDir.path))
|
|
@@ -788,7 +839,6 @@ export function getAnalytics(projectsDir, projectPath, startDate, endDate) {
|
|
|
788
839
|
if (record.type === 'assistant' && record.message?.usage) {
|
|
789
840
|
const usage = record.message.usage;
|
|
790
841
|
totalInput += usage.input_tokens || 0;
|
|
791
|
-
totalOutput += usage.output_tokens || 0;
|
|
792
842
|
totalCacheRead += usage.cache_read_input_tokens || 0;
|
|
793
843
|
}
|
|
794
844
|
}
|
|
@@ -798,32 +848,44 @@ export function getAnalytics(projectsDir, projectPath, startDate, endDate) {
|
|
|
798
848
|
const totalRequests = sessions.reduce((sum, s) => sum + s.total_requests, 0);
|
|
799
849
|
const avgContextSize = totalRequests > 0 ? Math.round(totalContextSent / totalRequests) : 0;
|
|
800
850
|
const dangerLevel = getDangerLevel(avgContextSize);
|
|
801
|
-
const limitImpact = Math.round((totalContextSent / 44000) * 100);
|
|
802
851
|
const avgEfficiency = sessions.length > 0 ? Math.round(sessions.reduce((sum, s) => sum + s.efficiency_score, 0) / sessions.length) : 0;
|
|
803
852
|
const avgToolErrorRate = sessions.length > 0 ? Math.round(sessions.reduce((sum, s) => sum + s.tool_error_rate, 0) / sessions.length * 10) / 10 : 0;
|
|
804
|
-
// Hypothesis Check
|
|
853
|
+
// Hypothesis Check — Anthropic-aligned metrics (HT/E, SEI, P99)
|
|
805
854
|
const withSpec = sessions.filter(s => s.has_spec_context && s.total_requests > 0);
|
|
806
855
|
const withoutSpec = sessions.filter(s => !s.has_spec_context && s.total_requests > 0);
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
const
|
|
815
|
-
const
|
|
816
|
-
const
|
|
817
|
-
? Math.round((1 -
|
|
856
|
+
// HT/E: Human Turns per Edit (lower = more autonomous)
|
|
857
|
+
const computeAvgHTE = (arr) => {
|
|
858
|
+
const valid = arr.filter(s => s.edit_count > 0);
|
|
859
|
+
if (valid.length === 0)
|
|
860
|
+
return 0;
|
|
861
|
+
return Math.round(valid.reduce((sum, s) => sum + (s.human_turns / s.edit_count), 0) / valid.length * 100) / 100;
|
|
862
|
+
};
|
|
863
|
+
const hteWithSpec = computeAvgHTE(withSpec);
|
|
864
|
+
const hteWithoutSpec = computeAvgHTE(withoutSpec);
|
|
865
|
+
const hteImprovement = hteWithoutSpec > 0
|
|
866
|
+
? Math.round((1 - hteWithSpec / hteWithoutSpec) * 1000) / 10 : 0;
|
|
867
|
+
// SEI: Spec Efficiency Index comparison
|
|
868
|
+
const computeAvgSEI = (arr) => {
|
|
869
|
+
const valid = arr.filter(s => s.spec_efficiency && s.spec_efficiency.sei_score > 0);
|
|
870
|
+
if (valid.length === 0)
|
|
871
|
+
return 0;
|
|
872
|
+
return Math.round(valid.reduce((sum, s) => sum + (s.spec_efficiency?.sei_score || 0), 0) / valid.length * 10) / 10;
|
|
873
|
+
};
|
|
874
|
+
// P99 duration: 99th percentile tail-end (minutes)
|
|
875
|
+
const computeP99Duration = (arr) => {
|
|
876
|
+
if (arr.length === 0)
|
|
877
|
+
return 0;
|
|
878
|
+
const sorted = [...arr].map(s => s.duration_minutes).sort((a, b) => a - b);
|
|
879
|
+
const idx = Math.min(Math.floor(sorted.length * 0.99), sorted.length - 1);
|
|
880
|
+
return sorted[idx];
|
|
881
|
+
};
|
|
818
882
|
return {
|
|
819
883
|
summary: {
|
|
820
884
|
total_sessions: sessions.length,
|
|
821
885
|
total_projects: projects.length,
|
|
822
886
|
total_context_tokens: totalContextSent,
|
|
823
|
-
total_output_tokens: totalOutput,
|
|
824
887
|
avg_context_size: avgContextSize,
|
|
825
888
|
danger_level: dangerLevel,
|
|
826
|
-
limit_impact: limitImpact,
|
|
827
889
|
optimal_sessions: sessions.filter(s => s.danger_level === 'optimal').length,
|
|
828
890
|
safe_sessions: sessions.filter(s => s.danger_level === 'safe').length,
|
|
829
891
|
caution_sessions: sessions.filter(s => s.danger_level === 'caution').length,
|
|
@@ -837,16 +899,13 @@ export function getAnalytics(projectsDir, projectPath, startDate, endDate) {
|
|
|
837
899
|
clean_exits: sessions.filter(s => s.session_exit === 'clean').length,
|
|
838
900
|
forced_exits: sessions.filter(s => s.session_exit === 'forced').length,
|
|
839
901
|
hypothesis_check: {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
normalized_improvement: normalizedImprovement,
|
|
848
|
-
avg_duration_with_spec: avg(withSpec, s => s.duration_minutes),
|
|
849
|
-
avg_duration_without_spec: avg(withoutSpec, s => s.duration_minutes),
|
|
902
|
+
hte_with_spec: hteWithSpec,
|
|
903
|
+
hte_without_spec: hteWithoutSpec,
|
|
904
|
+
hte_improvement: hteImprovement,
|
|
905
|
+
avg_sei_with_spec: computeAvgSEI(withSpec),
|
|
906
|
+
avg_sei_without_spec: computeAvgSEI(withoutSpec),
|
|
907
|
+
p99_duration_with_spec: computeP99Duration(withSpec),
|
|
908
|
+
p99_duration_without_spec: computeP99Duration(withoutSpec),
|
|
850
909
|
sessions_with_spec: withSpec.length,
|
|
851
910
|
sessions_without_spec: withoutSpec.length
|
|
852
911
|
},
|
|
@@ -860,8 +919,6 @@ export function getAnalytics(projectsDir, projectPath, startDate, endDate) {
|
|
|
860
919
|
total_tool_errors: sessions.reduce((sum, s) => sum + s.anthropic_metrics.api_reliability.tool_error_count, 0),
|
|
861
920
|
avg_autonomy_rate: sessions.length > 0
|
|
862
921
|
? Math.round(sessions.reduce((sum, s) => sum + s.anthropic_metrics.user_intervention.autonomy_rate, 0) / sessions.length * 10) / 10 : 0,
|
|
863
|
-
avg_ht_per_edit: sessions.length > 0
|
|
864
|
-
? Math.round(sessions.reduce((sum, s) => sum + s.anthropic_metrics.user_intervention.ht_per_edit, 0) / sessions.length * 10) / 10 : 0,
|
|
865
922
|
workflow_completion_rate: sessions.length > 0
|
|
866
923
|
? Math.round(sessions.filter(s => s.anthropic_metrics.workflow_autonomy.efficiency_score >= 60).length / sessions.length * 1000) / 10 : 0,
|
|
867
924
|
grade_consistency: sessions.length > 0
|
package/dist/server/index.js
CHANGED
|
@@ -94,18 +94,20 @@ app.post('/api/refresh', (req, res) => {
|
|
|
94
94
|
// Static file serving (SPA)
|
|
95
95
|
// ════════════════════════════════════════════════════════════
|
|
96
96
|
const distPath = path.resolve(__dirname, '../client');
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
const distPathAlt = path.resolve(process.cwd(), 'dist/client');
|
|
98
|
+
const resolvedDistPath = fs.existsSync(distPath) ? distPath : distPathAlt;
|
|
99
|
+
const indexHtmlPath = path.join(resolvedDistPath, 'index.html');
|
|
100
|
+
if (fs.existsSync(resolvedDistPath)) {
|
|
101
|
+
app.use(express.static(resolvedDistPath));
|
|
100
102
|
app.use((req, res, next) => {
|
|
101
103
|
if (req.path.startsWith('/api'))
|
|
102
104
|
return next();
|
|
103
105
|
res.sendFile(indexHtmlPath);
|
|
104
106
|
});
|
|
105
|
-
console.log(`📦 Serving frontend from: ${
|
|
107
|
+
console.log(`📦 Serving frontend from: ${resolvedDistPath}`);
|
|
106
108
|
}
|
|
107
109
|
else {
|
|
108
|
-
console.log(`⚠️ Frontend not found at: ${
|
|
110
|
+
console.log(`⚠️ Frontend not found at: ${resolvedDistPath}`);
|
|
109
111
|
console.log(` Run in dev mode: npm run dev`);
|
|
110
112
|
}
|
|
111
113
|
// ════════════════════════════════════════════════════════════
|