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.
@@ -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-Cb1zGdUJ.js"></script>
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>
@@ -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
- score += tokensPerEdit > 0 && tokensPerEdit < 30000 ? 15
20
- : tokensPerEdit < 50000 ? 10
21
- : tokensPerEdit < 80000 ? 5 : 0;
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
- if (avgContextSize < 10000)
86
+ function getDangerLevel(avgContextSize, modelName = 'sonnet') {
87
+ const normalizedCost = avgContextSize * getModelCostMultiplier(modelName);
88
+ if (normalizedCost < 10000)
67
89
  return 'optimal';
68
- if (avgContextSize < 20000)
90
+ if (normalizedCost < 20000)
69
91
  return 'safe';
70
- if (avgContextSize > 50000)
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' && record.message?.usage) {
131
- const usage = record.message.usage;
132
- inputTokens += usage.input_tokens || 0;
133
- // outputTokens += usage.output_tokens || 0;
134
- cacheRead += usage.cache_read_input_tokens || 0;
135
- requests++;
136
- if (record.message?.content && Array.isArray(record.message.content)) {
137
- for (const block of record.message.content) {
138
- if (block.type === 'tool_use') {
139
- const toolName = block.name?.toLowerCase() || '';
140
- if (toolName.includes('read') || toolName.includes('view') || toolName.includes('grep') || toolName.includes('glob') || toolName.includes('list')) {
141
- readCount++;
142
- const input = block.input || {};
143
- const fp = input.file_path || input.path || '';
144
- if (firstSpecReadIndex === -1 && (fp.includes('.claude/') || fp.includes('CLAUDE.md'))) {
145
- firstSpecReadIndex = recordIndex;
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
- human_turns_per_edit: editCount > 0 ? Math.round((humanTurns / editCount) * 10) / 10 : 0,
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({ name: toolName, detail: detail || undefined });
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: tokensPerEdit > 0 && tokensPerEdit < 30000 ? 15 : tokensPerEdit < 50000 ? 10 : tokensPerEdit < 80000 ? 5 : 0
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, totalOutput = 0, totalCacheRead = 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
- const avg = (arr, fn) => arr.length > 0 ? Math.round(arr.reduce((sum, s) => sum + fn(s), 0) / arr.length * 10) / 10 : 0;
808
- const avgHumanTurnsWithSpec = avg(withSpec, s => s.human_turns);
809
- const avgHumanTurnsWithoutSpec = avg(withoutSpec, s => s.human_turns);
810
- const humanTurnImprovement = avgHumanTurnsWithoutSpec > 0
811
- ? Math.round((1 - avgHumanTurnsWithSpec / avgHumanTurnsWithoutSpec) * 1000) / 10 : 0;
812
- const specWithEdits = withSpec.filter(s => s.human_turns_per_edit > 0);
813
- const noSpecWithEdits = withoutSpec.filter(s => s.human_turns_per_edit > 0);
814
- const avgHtPerEditWithSpec = avg(specWithEdits, s => s.human_turns_per_edit);
815
- const avgHtPerEditWithoutSpec = avg(noSpecWithEdits, s => s.human_turns_per_edit);
816
- const normalizedImprovement = avgHtPerEditWithoutSpec > 0
817
- ? Math.round((1 - avgHtPerEditWithSpec / avgHtPerEditWithoutSpec) * 1000) / 10 : 0;
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
- avg_turns_with_spec: avg(withSpec, s => s.total_requests),
841
- avg_turns_without_spec: avg(withoutSpec, s => s.total_requests),
842
- avg_human_turns_with_spec: avgHumanTurnsWithSpec,
843
- avg_human_turns_without_spec: avgHumanTurnsWithoutSpec,
844
- human_turn_improvement: humanTurnImprovement,
845
- avg_ht_per_edit_with_spec: avgHtPerEditWithSpec,
846
- avg_ht_per_edit_without_spec: avgHtPerEditWithoutSpec,
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
@@ -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 indexHtmlPath = path.join(distPath, 'index.html');
98
- if (fs.existsSync(distPath)) {
99
- app.use(express.static(distPath));
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: ${distPath}`);
107
+ console.log(`📦 Serving frontend from: ${resolvedDistPath}`);
106
108
  }
107
109
  else {
108
- console.log(`⚠️ Frontend not found at: ${distPath}`);
110
+ console.log(`⚠️ Frontend not found at: ${resolvedDistPath}`);
109
111
  console.log(` Run in dev mode: npm run dev`);
110
112
  }
111
113
  // ════════════════════════════════════════════════════════════
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-cli-analytics",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Claude CLI Analytics Dashboard - Efficiency insights for Claude Pro users",
5
5
  "keywords": [
6
6
  "claude",