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.
- package/README.md +148 -60
- package/bin/cli.cjs +168 -0
- package/bin/sanity.mjs +3 -0
- package/dist/client/assets/index-Cb1zGdUJ.js +48 -0
- package/dist/client/index.html +1 -1
- package/dist/server/analyzer.js +175 -12
- package/package.json +2 -2
- package/bin/cli.js +0 -44
- package/dist/client/assets/index-CXwfzzf8.js +0 -48
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-Cb1zGdUJ.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
|
@@ -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
|
-
|
|
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
|
|
199
|
-
|
|
200
|
-
if (
|
|
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
|
-
|
|
203
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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');
|