claude-cli-analytics 0.0.1 → 0.0.4

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-CXwfzzf8.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-Cb1zGdUJ.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-BkOIudNK.css">
10
10
  </head>
11
11
  <body>
@@ -125,7 +125,7 @@ export function getSessionsList(projectsDir, projectPath, startDate, endDate) {
125
125
  let postSpecReadTotal = 0, postSpecReadErrors = 0;
126
126
  let recordIndex = 0;
127
127
  let toolResultCount = 0, toolErrorCount = 0;
128
- let humanTurns = 0, autoTurns = 0;
128
+ let humanTurns = 0, autoTurns = 0, commandTurns = 0;
129
129
  for (const record of records) {
130
130
  if (record.type === 'assistant' && record.message?.usage) {
131
131
  const usage = record.message.usage;
@@ -179,8 +179,7 @@ export function getSessionsList(projectsDir, projectPath, startDate, endDate) {
179
179
  if (fp.includes('.claude/') || fp.includes('CLAUDE.md'))
180
180
  specFilesRead.push(fp);
181
181
  }
182
- // Human vs auto turn detection
183
- // Human vs auto turn detection
182
+ // Human vs auto vs command turn detection
184
183
  if (record.type === 'user') {
185
184
  const hasTUR = !!record.toolUseResult;
186
185
  const isMeta = !!record.isMeta;
@@ -192,15 +191,29 @@ export function getSessionsList(projectsDir, projectPath, startDate, endDate) {
192
191
  // skip
193
192
  }
194
193
  else if (typeof msgContent === 'string' && msgContent.trim().length > 0) {
195
- humanTurns++;
194
+ const raw = msgContent.trim();
195
+ // Slash commands (/feature, /init, etc.) are skill triggers, not user interventions
196
+ if (raw.includes('<command-name>') || raw.startsWith('This session is being continued')) {
197
+ commandTurns++;
198
+ }
199
+ else if (raw.startsWith('<local-command-stdout>') || raw.startsWith('<local-command-caveat>')) {
200
+ autoTurns++;
201
+ }
202
+ else {
203
+ humanTurns++;
204
+ }
196
205
  }
197
206
  else if (Array.isArray(msgContent)) {
198
- const hasText = msgContent.some(b => b.type === 'text' && b.text?.trim().length);
199
- const hasOnlyToolResults = msgContent.every(b => b.type === 'tool_result');
200
- if (hasOnlyToolResults)
207
+ const hasToolResults = msgContent.some(b => b.type === 'tool_result');
208
+ // Tool results (even with contextual text) are automatic, not user interventions
209
+ if (hasToolResults) {
201
210
  autoTurns++;
202
- else if (hasText)
203
- humanTurns++;
211
+ }
212
+ else {
213
+ const hasText = msgContent.some(b => b.type === 'text' && b.text?.trim().length);
214
+ if (hasText)
215
+ humanTurns++;
216
+ }
204
217
  }
205
218
  }
206
219
  recordIndex++;
@@ -238,6 +251,47 @@ export function getSessionsList(projectsDir, projectPath, startDate, endDate) {
238
251
  durationMinutes = 0;
239
252
  }
240
253
  const specCount = new Set(specFilesRead).size;
254
+ const specTriggerRate = specCount > 0 ? 100 : 0;
255
+ // Command turns are skill triggers (automatic), include them in auto side for autonomy calculation
256
+ const effectiveAutoTurns = autoTurns + commandTurns;
257
+ const autonomyRate = (effectiveAutoTurns + humanTurns) > 0
258
+ ? Math.round((effectiveAutoTurns / (effectiveAutoTurns + humanTurns)) * 1000) / 10 : 0;
259
+ const totalToolCalls = readCount + editCount;
260
+ const anthropicMetrics = {
261
+ skill_trigger: {
262
+ cache_hit_rate: Math.round(cacheHitRate * 10) / 10,
263
+ spec_trigger_rate: specTriggerRate,
264
+ danger_level: dangerLevel,
265
+ limit_impact: limitImpact,
266
+ },
267
+ tool_efficiency: {
268
+ read_edit_ratio: readEditRatio,
269
+ tokens_per_edit: tokensPerEdit,
270
+ duplicate_read_rate: duplicateReadRate,
271
+ repeated_edit_rate: repeatedEditRate,
272
+ total_tool_calls: totalToolCalls,
273
+ },
274
+ api_reliability: {
275
+ tool_error_rate: toolErrorRate,
276
+ tool_error_count: toolErrorCount,
277
+ session_exit: sessionExit,
278
+ },
279
+ user_intervention: {
280
+ human_turns: humanTurns,
281
+ auto_turns: effectiveAutoTurns,
282
+ autonomy_rate: autonomyRate,
283
+ ht_per_edit: editCount > 0 ? Math.round((humanTurns / editCount) * 10) / 10 : 0,
284
+ },
285
+ workflow_autonomy: {
286
+ repeated_edit_rate: repeatedEditRate,
287
+ efficiency_score: efficiencyScore,
288
+ sei,
289
+ },
290
+ session_consistency: {
291
+ grade: grade.letter,
292
+ grade_breakdown: grade.breakdown,
293
+ },
294
+ };
241
295
  sessions.push({
242
296
  session_id: file.replace('.jsonl', ''),
243
297
  project: project.name,
@@ -262,10 +316,12 @@ export function getSessionsList(projectsDir, projectPath, startDate, endDate) {
262
316
  spec_files_count: specCount,
263
317
  has_spec_context: specCount > 0,
264
318
  human_turns: humanTurns,
265
- auto_turns: autoTurns,
319
+ auto_turns: effectiveAutoTurns,
320
+ command_turns: commandTurns,
266
321
  human_turns_per_edit: editCount > 0 ? Math.round((humanTurns / editCount) * 10) / 10 : 0,
267
322
  spec_efficiency: sei,
268
- grade_breakdown: grade.breakdown
323
+ grade_breakdown: grade.breakdown,
324
+ anthropic_metrics: anthropicMetrics,
269
325
  });
270
326
  }
271
327
  }
@@ -323,6 +379,8 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
323
379
  let recordIndex = 0;
324
380
  const toolIdMap = new Map();
325
381
  const errorMap = new Map();
382
+ // Track pending questions from interactive tools (AskFollowupQuestion etc.)
383
+ const pendingQuestions = new Map();
326
384
  // Track which files were read by the assistant via tool_use
327
385
  // so we can deduplicate them from user's "context load" display
328
386
  let lastAssistantReadFiles = new Set();
@@ -391,6 +449,18 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
391
449
  for (const block of msgContent) {
392
450
  if (block?.type === 'tool_result') {
393
451
  toolResultCount++;
452
+ // Link answer back to pending interactive tool question
453
+ const resultToolId = block.tool_use_id;
454
+ if (resultToolId && pendingQuestions.has(resultToolId)) {
455
+ let answerText = '';
456
+ if (typeof block.content === 'string')
457
+ answerText = block.content;
458
+ else if (Array.isArray(block.content))
459
+ answerText = block.content.map(c => c.text || '').join(' ');
460
+ const pending = pendingQuestions.get(resultToolId);
461
+ pending.answer = answerText || undefined;
462
+ pendingQuestions.delete(resultToolId);
463
+ }
394
464
  if (block.is_error) {
395
465
  toolErrorCount++;
396
466
  if (firstSpecReadIndex !== -1 && recordIndex > firstSpecReadIndex)
@@ -507,6 +577,36 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
507
577
  else if (nameLower === 'tasklist') {
508
578
  detail = '';
509
579
  }
580
+ else if (nameLower.includes('ask') || nameLower.includes('question') || nameLower.includes('followup') || nameLower === 'attempt_completion') {
581
+ // Interactive tools: capture the question/message
582
+ // AskUserQuestion format: input.questions[{question, header, options[{label, description}]}]
583
+ let question = '';
584
+ const questions = input.questions;
585
+ if (Array.isArray(questions) && questions.length > 0) {
586
+ const parts = [];
587
+ for (const q of questions) {
588
+ let qText = '';
589
+ if (q.header)
590
+ qText += `[${q.header}] `;
591
+ if (q.question)
592
+ qText += q.question;
593
+ if (q.options && q.options.length > 0) {
594
+ qText += '\n' + q.options.map((o, idx) => ` ${idx + 1}. ${o.label || ''}${o.description ? ` - ${o.description}` : ''}`).join('\n');
595
+ }
596
+ parts.push(qText);
597
+ }
598
+ question = parts.join('\n\n');
599
+ }
600
+ else {
601
+ question = input.question || input.message || input.result || input.text || '';
602
+ }
603
+ detail = question.length > 100 ? question.substring(0, 100) + '...' : question;
604
+ const toolEntry = { name: toolName, detail: detail || undefined, question: question || undefined };
605
+ toolUses.push(toolEntry);
606
+ if (toolId)
607
+ pendingQuestions.set(toolId, toolEntry);
608
+ continue; // skip the generic push below
609
+ }
510
610
  else {
511
611
  detail = input.file_path || input.filePath || input.command || input.description || '';
512
612
  if (nameLower.includes('read') || nameLower.includes('view') || nameLower.includes('grep') || nameLower.includes('glob') || nameLower.includes('list')) {
@@ -620,7 +720,53 @@ export function getSessionDetail(projectsDir, sessionId, projectPath) {
620
720
  },
621
721
  spec_efficiency: sei,
622
722
  grade_breakdown: grade.breakdown
623
- }
723
+ },
724
+ anthropic_metrics: (() => {
725
+ const humanTurns = messages.filter(m => m.type === 'user' && m.subtype === 'human').length;
726
+ const commandTurnsDetail = messages.filter(m => m.type === 'user' && (m.subtype === 'command' || m.subtype === 'continuation')).length;
727
+ const autoTurns = messages.filter(m => m.type === 'user' && (m.subtype === 'tool_result' || m.subtype === '')).length + commandTurnsDetail;
728
+ const autonomyRate = (autoTurns + humanTurns) > 0
729
+ ? Math.round((autoTurns / (autoTurns + humanTurns)) * 1000) / 10 : 0;
730
+ const specFilesInSession = allReadFiles.filter(f => f.includes('.claude/') || f.includes('CLAUDE.md'));
731
+ const specTriggerRate = specFilesInSession.length > 0 ? 100 : 0;
732
+ const totalToolCalls = readCount + editCount;
733
+ return {
734
+ skill_trigger: {
735
+ cache_hit_rate: Math.round(cacheHitRate * 10) / 10,
736
+ spec_trigger_rate: specTriggerRate,
737
+ danger_level: dangerLevel,
738
+ limit_impact: limitImpact,
739
+ },
740
+ tool_efficiency: {
741
+ read_edit_ratio: readEditRatio,
742
+ tokens_per_edit: tokensPerEdit,
743
+ duplicate_read_rate: duplicateReadRate,
744
+ repeated_edit_rate: repeatedEditRate,
745
+ total_tool_calls: totalToolCalls,
746
+ },
747
+ api_reliability: {
748
+ tool_error_rate: Math.round(toolErrorRate * 10) / 10,
749
+ tool_error_count: toolErrorCount,
750
+ session_exit: sessionExit,
751
+ error_details: errorDetails,
752
+ },
753
+ user_intervention: {
754
+ human_turns: humanTurns,
755
+ auto_turns: autoTurns,
756
+ autonomy_rate: autonomyRate,
757
+ ht_per_edit: editCount > 0 ? Math.round((humanTurns / editCount) * 10) / 10 : 0,
758
+ },
759
+ workflow_autonomy: {
760
+ repeated_edit_rate: repeatedEditRate,
761
+ efficiency_score: Math.round(efficiencyScore),
762
+ sei,
763
+ },
764
+ session_consistency: {
765
+ grade: grade.letter,
766
+ grade_breakdown: grade.breakdown,
767
+ },
768
+ };
769
+ })(),
624
770
  };
625
771
  }
626
772
  /**
@@ -703,6 +849,23 @@ export function getAnalytics(projectsDir, projectPath, startDate, endDate) {
703
849
  avg_duration_without_spec: avg(withoutSpec, s => s.duration_minutes),
704
850
  sessions_with_spec: withSpec.length,
705
851
  sessions_without_spec: withoutSpec.length
852
+ },
853
+ anthropic_aggregate: {
854
+ avg_skill_trigger_rate: sessions.length > 0
855
+ ? Math.round(sessions.reduce((sum, s) => sum + s.anthropic_metrics.skill_trigger.cache_hit_rate, 0) / sessions.length * 10) / 10 : 0,
856
+ avg_spec_trigger_rate: sessions.length > 0
857
+ ? Math.round(sessions.filter(s => s.has_spec_context).length / sessions.length * 1000) / 10 : 0,
858
+ avg_tool_calls_per_session: sessions.length > 0
859
+ ? Math.round(sessions.reduce((sum, s) => sum + s.anthropic_metrics.tool_efficiency.total_tool_calls, 0) / sessions.length * 10) / 10 : 0,
860
+ total_tool_errors: sessions.reduce((sum, s) => sum + s.anthropic_metrics.api_reliability.tool_error_count, 0),
861
+ avg_autonomy_rate: sessions.length > 0
862
+ ? 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
+ workflow_completion_rate: sessions.length > 0
866
+ ? Math.round(sessions.filter(s => s.anthropic_metrics.workflow_autonomy.efficiency_score >= 60).length / sessions.length * 1000) / 10 : 0,
867
+ grade_consistency: sessions.length > 0
868
+ ? Math.round(sessions.filter(s => s.anthropic_metrics.session_consistency.grade === 'S' || s.anthropic_metrics.session_consistency.grade === 'A').length / sessions.length * 1000) / 10 : 0,
706
869
  }
707
870
  },
708
871
  projects,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-cli-analytics",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "description": "Claude CLI Analytics Dashboard - Efficiency insights for Claude Pro users",
5
5
  "keywords": [
6
6
  "claude",
@@ -25,7 +25,7 @@
25
25
  "main": "dist/server/index.js",
26
26
  "type": "module",
27
27
  "bin": {
28
- "claude-cli-analytics": "./bin/cli.js"
28
+ "claude-cli-analytics": "./bin/cli.cjs"
29
29
  },
30
30
  "files": [
31
31
  "dist",
package/bin/cli.js DELETED
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // ── Claude Analytics CLI ──
4
- // Supports: --port <number>, --path <dir>, --help
5
-
6
- const args = process.argv.slice(2);
7
-
8
- // Handle --help
9
- if (args.includes('--help') || args.includes('-h')) {
10
- console.log(`
11
- Claude Analytics Dashboard
12
-
13
- Usage: claude-cli-analytics [options]
14
-
15
- Options:
16
- --port <number> Server port (default: 3001)
17
- --path <dir> Claude projects directory (overrides auto-detection)
18
- --help, -h Show this help message
19
-
20
- Auto-detection:
21
- Automatically finds Claude Code data at ~/.claude/projects
22
- Works with all installation methods (homebrew, npm, direct install)
23
-
24
- Environment variables:
25
- CLAUDE_PROJECTS_DIR Override projects directory path
26
- PORT Override server port
27
- `);
28
- process.exit(0);
29
- }
30
-
31
- // Parse --port
32
- const portIdx = args.indexOf('--port');
33
- if (portIdx !== -1 && args[portIdx + 1]) {
34
- process.env.PORT = args[portIdx + 1];
35
- }
36
-
37
- // Parse --path
38
- const pathIdx = args.indexOf('--path');
39
- if (pathIdx !== -1 && args[pathIdx + 1]) {
40
- process.env.CLAUDE_PROJECTS_DIR = args[pathIdx + 1];
41
- }
42
-
43
- // Start the server
44
- import('../dist/server/index.js');