claude-code-tracker 1.2.3 → 1.4.0-beta.3
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 +38 -2
- package/bin/claude-tracker-cost.js +20 -0
- package/bin/claude-tracker-setup +10 -0
- package/install.js +21 -0
- package/install.sh +36 -2
- package/package.json +7 -3
- package/skills/view-tracking/SKILL.md +54 -0
- package/src/__pycache__/cost.cpython-312.pyc +0 -0
- package/src/__pycache__/parse_friction.cpython-312.pyc +0 -0
- package/src/__pycache__/parse_skills.cpython-312.pyc +0 -0
- package/src/__pycache__/platform_utils.cpython-312.pyc +0 -0
- package/src/__pycache__/storage.cpython-312.pyc +0 -0
- package/src/__pycache__/write-agent.cpython-312.pyc +0 -0
- package/src/__pycache__/write-turns.cpython-312.pyc +0 -0
- package/src/backfill.py +32 -52
- package/src/cost-summary.py +48 -11
- package/src/cost.py +7 -0
- package/src/export-json.py +27 -0
- package/src/generate-charts.py +567 -12
- package/src/init-templates.py +26 -0
- package/src/init-templates.sh +3 -3
- package/src/parse_friction.py +286 -0
- package/src/parse_skills.py +133 -0
- package/src/patch-durations.py +14 -114
- package/src/platform_utils.py +36 -0
- package/src/stop-hook.js +26 -0
- package/src/stop-hook.sh +21 -153
- package/src/storage.py +397 -0
- package/src/subagent-stop-hook.sh +37 -0
- package/src/update-prompts-index.py +177 -20
- package/src/write-agent.py +113 -0
- package/src/write-turns.py +130 -0
- package/uninstall.js +20 -0
- package/uninstall.sh +17 -0
package/src/generate-charts.py
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Generates tracking/charts.html from
|
|
3
|
+
Generates tracking/charts.html from SQLite storage + key-prompts/ folder.
|
|
4
4
|
Called by stop-hook.sh after each session update.
|
|
5
5
|
|
|
6
|
-
Usage: python3 generate-charts.py <
|
|
6
|
+
Usage: python3 generate-charts.py <tracking_dir_or_tokens.json> <output.html>
|
|
7
7
|
"""
|
|
8
8
|
import sys, json, os, re, glob
|
|
9
9
|
from collections import defaultdict
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
12
|
+
sys.path.insert(0, SCRIPT_DIR)
|
|
13
|
+
import storage
|
|
14
|
+
|
|
15
|
+
arg1 = sys.argv[1]
|
|
12
16
|
output_file = sys.argv[2]
|
|
13
17
|
|
|
18
|
+
# Backward compat: accept either tokens.json path or tracking directory
|
|
19
|
+
if arg1.endswith('.json'):
|
|
20
|
+
tracking_dir = os.path.dirname(os.path.abspath(arg1))
|
|
21
|
+
else:
|
|
22
|
+
tracking_dir = os.path.abspath(arg1)
|
|
23
|
+
|
|
24
|
+
data = storage.get_all_turns(tracking_dir)
|
|
25
|
+
agent_data = storage.get_all_agents(tracking_dir)
|
|
26
|
+
skill_data = storage.get_all_skills(tracking_dir)
|
|
27
|
+
|
|
28
|
+
# Load friction data (optional — file may not exist on older installs)
|
|
29
|
+
friction_file = os.path.join(tracking_dir, 'friction.json')
|
|
30
|
+
friction_data = []
|
|
31
|
+
if os.path.exists(friction_file):
|
|
32
|
+
try:
|
|
33
|
+
with open(friction_file, encoding='utf-8') as f:
|
|
34
|
+
friction_data = json.load(f)
|
|
35
|
+
except:
|
|
36
|
+
pass
|
|
37
|
+
|
|
14
38
|
def format_duration(seconds):
|
|
15
39
|
if seconds <= 0:
|
|
16
40
|
return "0m"
|
|
@@ -21,9 +45,6 @@ def format_duration(seconds):
|
|
|
21
45
|
return f"{h}h {m}m"
|
|
22
46
|
return f"{m}m {s}s"
|
|
23
47
|
|
|
24
|
-
with open(tokens_file) as f:
|
|
25
|
-
data = json.load(f)
|
|
26
|
-
|
|
27
48
|
if not data:
|
|
28
49
|
sys.exit(0)
|
|
29
50
|
|
|
@@ -72,6 +93,7 @@ total_turns = len(data)
|
|
|
72
93
|
total_sessions = len({e.get("session_id") for e in data})
|
|
73
94
|
sessions_with_data = len({e.get("session_id") for e in data if e.get("total_tokens", 0) > 0})
|
|
74
95
|
total_output = sum(e.get("output_tokens", 0) for e in data)
|
|
96
|
+
total_input = sum(e.get("input_tokens", 0) for e in data)
|
|
75
97
|
total_cache_read = sum(e.get("cache_read_tokens", 0) for e in data)
|
|
76
98
|
total_all_tokens = sum(e.get("total_tokens", 0) for e in data)
|
|
77
99
|
cache_pct = round(total_cache_read / total_all_tokens * 100, 1) if total_all_tokens > 0 else 0
|
|
@@ -81,7 +103,7 @@ avg_duration = total_duration // total_turns if total_turns > 0 else 0
|
|
|
81
103
|
project_name = data[0].get("project", "Project") if data else "Project"
|
|
82
104
|
|
|
83
105
|
# --- Count total human messages per date from JSONL transcripts ---
|
|
84
|
-
project_dir = os.path.dirname(os.path.dirname(
|
|
106
|
+
project_dir = os.path.dirname(os.path.dirname(tracking_dir)) # project root
|
|
85
107
|
# Claude Code slugifies paths as: replace every "/" with "-" (keeping leading slash → leading dash)
|
|
86
108
|
transcripts_dir = os.path.expanduser(
|
|
87
109
|
"~/.claude/projects/" + project_dir.replace("/", "-")
|
|
@@ -107,7 +129,7 @@ if os.path.isdir(transcripts_dir):
|
|
|
107
129
|
os.path.getmtime(jf)).strftime("%Y-%m-%d")
|
|
108
130
|
|
|
109
131
|
try:
|
|
110
|
-
with open(jf) as f:
|
|
132
|
+
with open(jf, encoding='utf-8') as f:
|
|
111
133
|
for line in f:
|
|
112
134
|
try:
|
|
113
135
|
obj = json.loads(line)
|
|
@@ -147,7 +169,7 @@ total_human_msgs = sum(human_by_date.values())
|
|
|
147
169
|
total_trivial_msgs = sum(trivial_by_date.values())
|
|
148
170
|
|
|
149
171
|
# --- Aggregate prompt data from key-prompts/ folder ---
|
|
150
|
-
prompts_dir = os.path.join(
|
|
172
|
+
prompts_dir = os.path.join(tracking_dir, "key-prompts")
|
|
151
173
|
prompt_files = sorted(glob.glob(os.path.join(prompts_dir, "????-??-??.md")))
|
|
152
174
|
|
|
153
175
|
prompt_by_date = {} # date -> {total, by_category}
|
|
@@ -155,7 +177,7 @@ all_categories = set()
|
|
|
155
177
|
|
|
156
178
|
for f in prompt_files:
|
|
157
179
|
date = os.path.splitext(os.path.basename(f))[0]
|
|
158
|
-
content = open(f).read()
|
|
180
|
+
content = open(f, encoding='utf-8').read()
|
|
159
181
|
cats = re.findall(r'^\*\*Category\*\*: (\S+)', content, re.MULTILINE)
|
|
160
182
|
by_cat = defaultdict(int)
|
|
161
183
|
for c in cats:
|
|
@@ -176,6 +198,8 @@ cache_read_by_date_js = json.dumps([by_date[d]["cache_read"] for d in dates])
|
|
|
176
198
|
opus_by_date_js = json.dumps([round(by_date[d]["opus_cost"], 4) for d in dates])
|
|
177
199
|
sonnet_by_date_js = json.dumps([round(by_date[d]["sonnet_cost"], 4) for d in dates])
|
|
178
200
|
duration_by_date_js = json.dumps([by_date[d]["duration"] for d in dates])
|
|
201
|
+
input_by_date_js = json.dumps([by_date[d]["input"] for d in dates])
|
|
202
|
+
cache_create_by_date_js = json.dumps([by_date[d]["cache_create"] for d in dates])
|
|
179
203
|
|
|
180
204
|
cumul_labels_js = json.dumps([f"{c['date']} {c['session_id']}#{c['turn_index']}" for c in cumulative])
|
|
181
205
|
cumul_values_js = json.dumps([c["cumulative_cost"] for c in cumulative])
|
|
@@ -285,6 +309,490 @@ donut_labels_js = json.dumps(list(cat_totals.keys()))
|
|
|
285
309
|
donut_values_js = json.dumps(list(cat_totals.values()))
|
|
286
310
|
donut_colors_js = json.dumps([CAT_COLORS.get(c, DEFAULT_COLOR) for c in cat_totals])
|
|
287
311
|
|
|
312
|
+
# --- Agent data aggregation ---
|
|
313
|
+
by_agent_type = defaultdict(lambda: {"cost": 0, "count": 0, "turns": 0})
|
|
314
|
+
for a in agent_data:
|
|
315
|
+
t = a.get('agent_type', 'unknown')
|
|
316
|
+
by_agent_type[t]["cost"] += a.get('estimated_cost_usd', 0)
|
|
317
|
+
by_agent_type[t]["count"] += 1
|
|
318
|
+
by_agent_type[t]["turns"] += a.get('turns', 0)
|
|
319
|
+
|
|
320
|
+
agent_types_sorted = sorted(by_agent_type.keys(), key=lambda t: -by_agent_type[t]["cost"])
|
|
321
|
+
total_agent_cost = sum(a.get('estimated_cost_usd', 0) for a in agent_data)
|
|
322
|
+
total_agent_invocations = len(agent_data)
|
|
323
|
+
|
|
324
|
+
agent_labels_js = json.dumps(agent_types_sorted)
|
|
325
|
+
agent_costs_js = json.dumps([round(by_agent_type[t]["cost"], 4) for t in agent_types_sorted])
|
|
326
|
+
agent_counts_js = json.dumps([by_agent_type[t]["count"] for t in agent_types_sorted])
|
|
327
|
+
agent_cpt_js = json.dumps([
|
|
328
|
+
round(by_agent_type[t]["cost"] / by_agent_type[t]["turns"], 4)
|
|
329
|
+
if by_agent_type[t]["turns"] > 0 else 0
|
|
330
|
+
for t in agent_types_sorted
|
|
331
|
+
])
|
|
332
|
+
|
|
333
|
+
# Conditional HTML blocks
|
|
334
|
+
if agent_data:
|
|
335
|
+
agents_stat_html = f''' <div class="stat">
|
|
336
|
+
<div class="stat-label">Agent cost</div>
|
|
337
|
+
<div class="stat-value">${total_agent_cost:.2f}</div>
|
|
338
|
+
<div class="stat-sub">{total_agent_invocations} invocations</div>
|
|
339
|
+
</div>'''
|
|
340
|
+
agents_section_html = f'''<div class="section">
|
|
341
|
+
<div class="section-header agents">Agents</div>
|
|
342
|
+
<div class="grid">
|
|
343
|
+
<div class="card">
|
|
344
|
+
<h2>Cost by agent type</h2>
|
|
345
|
+
<canvas id="agentCost"></canvas>
|
|
346
|
+
</div>
|
|
347
|
+
<div class="card">
|
|
348
|
+
<h2>Invocations by agent type</h2>
|
|
349
|
+
<canvas id="agentCount"></canvas>
|
|
350
|
+
</div>
|
|
351
|
+
<div class="card">
|
|
352
|
+
<h2>Cost per turn by agent type</h2>
|
|
353
|
+
<canvas id="agentCPT"></canvas>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
'''
|
|
359
|
+
agents_js_constants = f'''const AGENT_LABELS = {agent_labels_js};
|
|
360
|
+
const AGENT_COSTS = {agent_costs_js};
|
|
361
|
+
const AGENT_COUNTS = {agent_counts_js};
|
|
362
|
+
const AGENT_CPT = {agent_cpt_js};'''
|
|
363
|
+
agents_js_charts = '''
|
|
364
|
+
new Chart(document.getElementById('agentCost'), {
|
|
365
|
+
type: 'bar',
|
|
366
|
+
data: { labels: AGENT_LABELS,
|
|
367
|
+
datasets: [{ label: 'Cost ($)', data: AGENT_COSTS,
|
|
368
|
+
backgroundColor: '#f97316', borderRadius: 4 }] },
|
|
369
|
+
options: { ...baseOpts, plugins: { ...baseOpts.plugins,
|
|
370
|
+
tooltip: { callbacks: { label: ctx => ' $' + ctx.parsed.y.toFixed(4) } } } }
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
new Chart(document.getElementById('agentCount'), {
|
|
374
|
+
type: 'bar',
|
|
375
|
+
data: { labels: AGENT_LABELS,
|
|
376
|
+
datasets: [{ label: 'Invocations', data: AGENT_COUNTS,
|
|
377
|
+
backgroundColor: '#fb923c', borderRadius: 4 }] },
|
|
378
|
+
options: baseOpts
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
new Chart(document.getElementById('agentCPT'), {
|
|
382
|
+
type: 'bar',
|
|
383
|
+
data: { labels: AGENT_LABELS,
|
|
384
|
+
datasets: [{ label: 'Cost/turn', data: AGENT_CPT,
|
|
385
|
+
backgroundColor: '#fbbf24', borderRadius: 4 }] },
|
|
386
|
+
options: { ...baseOpts, plugins: { ...baseOpts.plugins,
|
|
387
|
+
tooltip: { callbacks: { label: ctx => ' $' + ctx.parsed.y.toFixed(4) + '/turn' } } } }
|
|
388
|
+
});'''
|
|
389
|
+
else:
|
|
390
|
+
agents_stat_html = ''
|
|
391
|
+
agents_section_html = ''
|
|
392
|
+
agents_js_constants = ''
|
|
393
|
+
agents_js_charts = ''
|
|
394
|
+
|
|
395
|
+
# --- Skill data aggregation ---
|
|
396
|
+
by_skill_name = defaultdict(lambda: {"count": 0, "success": 0, "fail": 0, "duration": 0})
|
|
397
|
+
skill_by_date = defaultdict(int)
|
|
398
|
+
for sk in skill_data:
|
|
399
|
+
name = sk.get('skill_name', 'unknown')
|
|
400
|
+
by_skill_name[name]["count"] += 1
|
|
401
|
+
if sk.get('success', 1):
|
|
402
|
+
by_skill_name[name]["success"] += 1
|
|
403
|
+
else:
|
|
404
|
+
by_skill_name[name]["fail"] += 1
|
|
405
|
+
by_skill_name[name]["duration"] += sk.get('duration_seconds', 0)
|
|
406
|
+
skill_by_date[sk.get('date', 'unknown')] += 1
|
|
407
|
+
|
|
408
|
+
skill_names_sorted = sorted(by_skill_name.keys(), key=lambda n: -by_skill_name[n]["count"])
|
|
409
|
+
total_skill_invocations = sum(by_skill_name[n]["count"] for n in by_skill_name)
|
|
410
|
+
total_skill_success = sum(by_skill_name[n]["success"] for n in by_skill_name)
|
|
411
|
+
skill_success_rate = round(total_skill_success / total_skill_invocations * 100, 1) if total_skill_invocations > 0 else 0
|
|
412
|
+
|
|
413
|
+
skill_labels_js = json.dumps(skill_names_sorted)
|
|
414
|
+
skill_counts_js = json.dumps([by_skill_name[n]["count"] for n in skill_names_sorted])
|
|
415
|
+
skill_success_js = json.dumps([by_skill_name[n]["success"] for n in skill_names_sorted])
|
|
416
|
+
skill_fail_js = json.dumps([by_skill_name[n]["fail"] for n in skill_names_sorted])
|
|
417
|
+
|
|
418
|
+
skill_timeline_dates = sorted(skill_by_date.keys())
|
|
419
|
+
skill_timeline_dates_js = json.dumps(skill_timeline_dates)
|
|
420
|
+
skill_timeline_values_js = json.dumps([skill_by_date[d] for d in skill_timeline_dates])
|
|
421
|
+
|
|
422
|
+
if skill_data:
|
|
423
|
+
skills_stat_html = f''' <div class="stat">
|
|
424
|
+
<div class="stat-label">Skill invocations</div>
|
|
425
|
+
<div class="stat-value">{total_skill_invocations}</div>
|
|
426
|
+
<div class="stat-sub">{skill_success_rate}% success rate</div>
|
|
427
|
+
</div>'''
|
|
428
|
+
skills_section_html = f'''<div class="section">
|
|
429
|
+
<div class="section-header skills">Skills</div>
|
|
430
|
+
<div class="grid">
|
|
431
|
+
<div class="card">
|
|
432
|
+
<h2>Invocations by skill</h2>
|
|
433
|
+
<canvas id="skillCount"></canvas>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="card">
|
|
436
|
+
<h2>Success / fail by skill</h2>
|
|
437
|
+
<canvas id="skillSuccess"></canvas>
|
|
438
|
+
</div>
|
|
439
|
+
<div class="card wide">
|
|
440
|
+
<h2>Skill invocations over time</h2>
|
|
441
|
+
<canvas id="skillTimeline"></canvas>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
'''
|
|
447
|
+
skills_js_constants = f'''const SKILL_LABELS = {skill_labels_js};
|
|
448
|
+
const SKILL_COUNTS = {skill_counts_js};
|
|
449
|
+
const SKILL_SUCCESS = {skill_success_js};
|
|
450
|
+
const SKILL_FAIL = {skill_fail_js};
|
|
451
|
+
const SKILL_TIMELINE_DATES = {skill_timeline_dates_js};
|
|
452
|
+
const SKILL_TIMELINE_VALUES = {skill_timeline_values_js};'''
|
|
453
|
+
skills_js_charts = '''
|
|
454
|
+
new Chart(document.getElementById('skillCount'), {
|
|
455
|
+
type: 'bar',
|
|
456
|
+
data: { labels: SKILL_LABELS,
|
|
457
|
+
datasets: [{ label: 'Invocations', data: SKILL_COUNTS,
|
|
458
|
+
backgroundColor: '#f59e0b', borderRadius: 4 }] },
|
|
459
|
+
options: { ...baseOpts, indexAxis: 'y' }
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
new Chart(document.getElementById('skillSuccess'), {
|
|
463
|
+
type: 'bar',
|
|
464
|
+
data: { labels: SKILL_LABELS,
|
|
465
|
+
datasets: [
|
|
466
|
+
{ label: 'Success', data: SKILL_SUCCESS, backgroundColor: '#f59e0b', borderRadius: 2 },
|
|
467
|
+
{ label: 'Fail', data: SKILL_FAIL, backgroundColor: '#92400e', borderRadius: 2 }
|
|
468
|
+
] },
|
|
469
|
+
options: { ...baseOpts, scales: { ...baseOpts.scales,
|
|
470
|
+
x: { ...baseOpts.scales.x, stacked: true },
|
|
471
|
+
y: { ...baseOpts.scales.y, stacked: true } } }
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
new Chart(document.getElementById('skillTimeline'), {
|
|
475
|
+
type: 'line',
|
|
476
|
+
data: {
|
|
477
|
+
labels: SKILL_TIMELINE_DATES,
|
|
478
|
+
datasets: [{ label: 'Invocations', data: SKILL_TIMELINE_VALUES,
|
|
479
|
+
borderColor: '#f59e0b', backgroundColor: 'rgba(245,158,11,0.15)',
|
|
480
|
+
fill: true, tension: 0.3, pointRadius: 3 }]
|
|
481
|
+
},
|
|
482
|
+
options: baseOpts
|
|
483
|
+
});'''
|
|
484
|
+
else:
|
|
485
|
+
skills_stat_html = ''
|
|
486
|
+
skills_section_html = ''
|
|
487
|
+
skills_js_constants = ''
|
|
488
|
+
skills_js_charts = ''
|
|
489
|
+
|
|
490
|
+
# --- Friction data aggregation ---
|
|
491
|
+
FRICTION_CAT_COLORS = {
|
|
492
|
+
"permission_denied": "#dc2626",
|
|
493
|
+
"hook_blocked": "#b91c1c",
|
|
494
|
+
"cascade_error": "#f97316",
|
|
495
|
+
"command_failed": "#eab308",
|
|
496
|
+
"tool_error": "#ef4444",
|
|
497
|
+
"correction": "#8b5cf6",
|
|
498
|
+
"retry": "#06b6d4",
|
|
499
|
+
}
|
|
500
|
+
FRICTION_DEFAULT_COLOR = "#94a3b8"
|
|
501
|
+
|
|
502
|
+
friction_by_date = defaultdict(lambda: {"main": 0, "subagent": 0})
|
|
503
|
+
friction_by_category = defaultdict(int)
|
|
504
|
+
friction_by_tool = defaultdict(int)
|
|
505
|
+
for fe in friction_data:
|
|
506
|
+
d = fe.get('date', 'unknown')
|
|
507
|
+
src = fe.get('source', 'main')
|
|
508
|
+
if src == 'subagent':
|
|
509
|
+
friction_by_date[d]["subagent"] += 1
|
|
510
|
+
else:
|
|
511
|
+
friction_by_date[d]["main"] += 1
|
|
512
|
+
friction_by_category[fe.get('category', 'unknown')] += 1
|
|
513
|
+
tn = fe.get('tool_name')
|
|
514
|
+
if tn:
|
|
515
|
+
friction_by_tool[tn] += 1
|
|
516
|
+
|
|
517
|
+
friction_by_skill = defaultdict(int)
|
|
518
|
+
for fe in friction_data:
|
|
519
|
+
sk = fe.get('skill')
|
|
520
|
+
if sk:
|
|
521
|
+
friction_by_skill[sk] += 1
|
|
522
|
+
has_skill_data = bool(friction_by_skill)
|
|
523
|
+
|
|
524
|
+
retry_events = [fe for fe in friction_data if fe.get('category') == 'retry']
|
|
525
|
+
retry_total = len(retry_events)
|
|
526
|
+
retry_resolved = sum(1 for fe in retry_events if fe.get('resolved') is True)
|
|
527
|
+
retry_rate = round(retry_resolved / retry_total * 100, 1) if retry_total > 0 else 0
|
|
528
|
+
|
|
529
|
+
total_friction = len(friction_data)
|
|
530
|
+
friction_rate = round(total_friction / total_turns * 100, 1) if total_turns > 0 else 0
|
|
531
|
+
|
|
532
|
+
friction_dates_sorted = sorted(friction_by_date.keys())
|
|
533
|
+
friction_dates_js = json.dumps(friction_dates_sorted)
|
|
534
|
+
friction_main_by_date_js = json.dumps([friction_by_date[d]["main"] for d in friction_dates_sorted])
|
|
535
|
+
friction_sub_by_date_js = json.dumps([friction_by_date[d]["subagent"] for d in friction_dates_sorted])
|
|
536
|
+
|
|
537
|
+
friction_cats_sorted = sorted(friction_by_category.keys(), key=lambda c: -friction_by_category[c])
|
|
538
|
+
friction_cat_labels_js = json.dumps(friction_cats_sorted)
|
|
539
|
+
friction_cat_values_js = json.dumps([friction_by_category[c] for c in friction_cats_sorted])
|
|
540
|
+
friction_cat_colors_js = json.dumps([FRICTION_CAT_COLORS.get(c, FRICTION_DEFAULT_COLOR) for c in friction_cats_sorted])
|
|
541
|
+
|
|
542
|
+
friction_tools_sorted = sorted(friction_by_tool.keys(), key=lambda t: -friction_by_tool[t])
|
|
543
|
+
friction_tool_labels_js = json.dumps(friction_tools_sorted)
|
|
544
|
+
friction_tool_values_js = json.dumps([friction_by_tool[t] for t in friction_tools_sorted])
|
|
545
|
+
|
|
546
|
+
friction_skills_sorted = sorted(friction_by_skill.keys(), key=lambda s: -friction_by_skill[s])
|
|
547
|
+
friction_skill_labels_js = json.dumps(friction_skills_sorted)
|
|
548
|
+
friction_skill_values_js = json.dumps([friction_by_skill[s] for s in friction_skills_sorted])
|
|
549
|
+
|
|
550
|
+
# Friction rate trend per day (events per 100 prompts)
|
|
551
|
+
friction_rate_dates = sorted(set(friction_dates_sorted) & set(dates))
|
|
552
|
+
friction_rate_values = []
|
|
553
|
+
for d in friction_rate_dates:
|
|
554
|
+
day_friction = friction_by_date[d]["main"] + friction_by_date[d]["subagent"]
|
|
555
|
+
day_turns = by_date[d]["turns"] if d in by_date else 0
|
|
556
|
+
friction_rate_values.append(round(day_friction / day_turns * 100, 1) if day_turns > 0 else 0)
|
|
557
|
+
friction_rate_dates_js = json.dumps(friction_rate_dates)
|
|
558
|
+
friction_rate_values_js = json.dumps(friction_rate_values)
|
|
559
|
+
|
|
560
|
+
if friction_data:
|
|
561
|
+
friction_stat_html = f''' <div class="stat">
|
|
562
|
+
<div class="stat-label">Friction events</div>
|
|
563
|
+
<div class="stat-value">{total_friction}</div>
|
|
564
|
+
<div class="stat-sub">{friction_rate} per 100 prompts</div>
|
|
565
|
+
</div>'''
|
|
566
|
+
retry_stat_html = f''' <div class="stat">
|
|
567
|
+
<div class="stat-label">Retry resolution</div>
|
|
568
|
+
<div class="stat-value">{retry_rate}%</div>
|
|
569
|
+
<div class="stat-sub">{retry_resolved} of {retry_total} retries succeeded</div>
|
|
570
|
+
</div>''' if retry_total > 0 else ''
|
|
571
|
+
friction_skill_card = '''
|
|
572
|
+
<div class="card">
|
|
573
|
+
<h2>Friction by skill</h2>
|
|
574
|
+
<canvas id="frictionSkill"></canvas>
|
|
575
|
+
</div>''' if has_skill_data else ''
|
|
576
|
+
friction_section_html = f'''<div class="section">
|
|
577
|
+
<div class="section-header friction">Friction</div>
|
|
578
|
+
<div class="grid">
|
|
579
|
+
<div class="card">
|
|
580
|
+
<h2>Friction per day</h2>
|
|
581
|
+
<canvas id="frictionDay"></canvas>
|
|
582
|
+
</div>
|
|
583
|
+
<div class="card">
|
|
584
|
+
<h2>Friction by category</h2>
|
|
585
|
+
<canvas id="frictionCat"></canvas>
|
|
586
|
+
</div>
|
|
587
|
+
<div class="card">
|
|
588
|
+
<h2>Friction by tool</h2>
|
|
589
|
+
<canvas id="frictionTool"></canvas>
|
|
590
|
+
</div>
|
|
591
|
+
<div class="card">
|
|
592
|
+
<h2>Friction rate trend</h2>
|
|
593
|
+
<canvas id="frictionRate"></canvas>
|
|
594
|
+
</div>{friction_skill_card}
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
'''
|
|
599
|
+
friction_js_constants = f'''const FRICTION_DATES = {friction_dates_js};
|
|
600
|
+
const FRICTION_MAIN = {friction_main_by_date_js};
|
|
601
|
+
const FRICTION_SUB = {friction_sub_by_date_js};
|
|
602
|
+
const FRICTION_CAT_LABELS = {friction_cat_labels_js};
|
|
603
|
+
const FRICTION_CAT_VALUES = {friction_cat_values_js};
|
|
604
|
+
const FRICTION_CAT_COLORS = {friction_cat_colors_js};
|
|
605
|
+
const FRICTION_TOOL_LABELS = {friction_tool_labels_js};
|
|
606
|
+
const FRICTION_TOOL_VALUES = {friction_tool_values_js};
|
|
607
|
+
const FRICTION_RATE_DATES = {friction_rate_dates_js};
|
|
608
|
+
const FRICTION_RATE_VALUES = {friction_rate_values_js};''' + (f'''
|
|
609
|
+
const FRICTION_SKILL_LABELS = {friction_skill_labels_js};
|
|
610
|
+
const FRICTION_SKILL_VALUES = {friction_skill_values_js};''' if has_skill_data else '')
|
|
611
|
+
friction_js_charts = '''
|
|
612
|
+
// Friction per day (stacked bar)
|
|
613
|
+
new Chart(document.getElementById('frictionDay'), {
|
|
614
|
+
type: 'bar',
|
|
615
|
+
data: {
|
|
616
|
+
labels: FRICTION_DATES,
|
|
617
|
+
datasets: [
|
|
618
|
+
{ label: 'Main session', data: FRICTION_MAIN, backgroundColor: '#ef4444', borderRadius: 2 },
|
|
619
|
+
{ label: 'Subagent', data: FRICTION_SUB, backgroundColor: '#f97316', borderRadius: 2 }
|
|
620
|
+
]
|
|
621
|
+
},
|
|
622
|
+
options: { ...baseOpts, scales: { ...baseOpts.scales,
|
|
623
|
+
x: { ...baseOpts.scales.x, stacked: true },
|
|
624
|
+
y: { ...baseOpts.scales.y, stacked: true } } }
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Friction by category (horizontal bar)
|
|
628
|
+
new Chart(document.getElementById('frictionCat'), {
|
|
629
|
+
type: 'bar',
|
|
630
|
+
data: {
|
|
631
|
+
labels: FRICTION_CAT_LABELS,
|
|
632
|
+
datasets: [{ label: 'Events', data: FRICTION_CAT_VALUES,
|
|
633
|
+
backgroundColor: FRICTION_CAT_COLORS, borderRadius: 4 }]
|
|
634
|
+
},
|
|
635
|
+
options: { ...baseOpts, indexAxis: 'y' }
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Friction by tool (bar)
|
|
639
|
+
new Chart(document.getElementById('frictionTool'), {
|
|
640
|
+
type: 'bar',
|
|
641
|
+
data: {
|
|
642
|
+
labels: FRICTION_TOOL_LABELS,
|
|
643
|
+
datasets: [{ label: 'Events', data: FRICTION_TOOL_VALUES,
|
|
644
|
+
backgroundColor: '#ef4444', borderRadius: 4 }]
|
|
645
|
+
},
|
|
646
|
+
options: baseOpts
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Friction rate trend (line)
|
|
650
|
+
new Chart(document.getElementById('frictionRate'), {
|
|
651
|
+
type: 'line',
|
|
652
|
+
data: {
|
|
653
|
+
labels: FRICTION_RATE_DATES,
|
|
654
|
+
datasets: [{ label: 'Per 100 prompts', data: FRICTION_RATE_VALUES,
|
|
655
|
+
borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.15)',
|
|
656
|
+
fill: true, tension: 0.3, pointRadius: 3 }]
|
|
657
|
+
},
|
|
658
|
+
options: { ...baseOpts, plugins: { ...baseOpts.plugins,
|
|
659
|
+
tooltip: { callbacks: { label: ctx => ' ' + ctx.parsed.y + ' per 100 prompts' } } } }
|
|
660
|
+
});''' + ('''
|
|
661
|
+
|
|
662
|
+
// Friction by skill (horizontal bar)
|
|
663
|
+
new Chart(document.getElementById('frictionSkill'), {
|
|
664
|
+
type: 'bar',
|
|
665
|
+
data: {
|
|
666
|
+
labels: FRICTION_SKILL_LABELS,
|
|
667
|
+
datasets: [{ label: 'Events', data: FRICTION_SKILL_VALUES,
|
|
668
|
+
backgroundColor: '#c084fc', borderRadius: 4 }]
|
|
669
|
+
},
|
|
670
|
+
options: { ...baseOpts, indexAxis: 'y' }
|
|
671
|
+
});''' if has_skill_data else '')
|
|
672
|
+
else:
|
|
673
|
+
friction_stat_html = ''
|
|
674
|
+
retry_stat_html = ''
|
|
675
|
+
friction_section_html = ''
|
|
676
|
+
friction_js_constants = ''
|
|
677
|
+
friction_js_charts = ''
|
|
678
|
+
|
|
679
|
+
# --- Error tracking ---
|
|
680
|
+
ERROR_CATEGORIES = {'tool_error', 'command_failed', 'cascade_error'}
|
|
681
|
+
|
|
682
|
+
def classify_error(event):
|
|
683
|
+
detail = (event.get('detail') or '').strip()
|
|
684
|
+
if 'InputValidationError' in detail:
|
|
685
|
+
return 'validation_error'
|
|
686
|
+
if 'No such file' in detail or 'FileNotFoundError' in detail:
|
|
687
|
+
return 'file_not_found'
|
|
688
|
+
if 'timed out' in detail.lower() or 'timeout' in detail.lower():
|
|
689
|
+
return 'timeout'
|
|
690
|
+
if 'UnicodeDecodeError' in detail or 'encoding' in detail.lower():
|
|
691
|
+
return 'encoding_error'
|
|
692
|
+
if event.get('category') == 'command_failed':
|
|
693
|
+
import re as _re
|
|
694
|
+
m = _re.search(r'Exit code (\d+)', detail)
|
|
695
|
+
if m:
|
|
696
|
+
return f'exit_code_{m.group(1)}'
|
|
697
|
+
return 'generic'
|
|
698
|
+
|
|
699
|
+
error_events = [fe for fe in friction_data if fe.get('category') in ERROR_CATEGORIES]
|
|
700
|
+
|
|
701
|
+
error_by_type = defaultdict(int)
|
|
702
|
+
error_by_tool = defaultdict(int)
|
|
703
|
+
error_by_date = defaultdict(int)
|
|
704
|
+
for ee in error_events:
|
|
705
|
+
error_by_type[classify_error(ee)] += 1
|
|
706
|
+
tn = ee.get('tool_name')
|
|
707
|
+
if tn:
|
|
708
|
+
error_by_tool[tn] += 1
|
|
709
|
+
error_by_date[ee.get('date', 'unknown')] += 1
|
|
710
|
+
|
|
711
|
+
total_errors = len(error_events)
|
|
712
|
+
error_rate = round(total_errors / total_turns * 100, 1) if total_turns > 0 else 0
|
|
713
|
+
|
|
714
|
+
if error_events:
|
|
715
|
+
error_types_sorted = sorted(error_by_type.keys(), key=lambda t: -error_by_type[t])
|
|
716
|
+
error_tools_sorted = sorted(error_by_tool.keys(), key=lambda t: -error_by_tool[t])
|
|
717
|
+
error_dates_sorted = sorted(error_by_date.keys())
|
|
718
|
+
|
|
719
|
+
error_type_labels_js = json.dumps(error_types_sorted)
|
|
720
|
+
error_type_values_js = json.dumps([error_by_type[t] for t in error_types_sorted])
|
|
721
|
+
error_tool_labels_js = json.dumps(error_tools_sorted)
|
|
722
|
+
error_tool_values_js = json.dumps([error_by_tool[t] for t in error_tools_sorted])
|
|
723
|
+
error_dates_js = json.dumps(error_dates_sorted)
|
|
724
|
+
error_date_values_js = json.dumps([error_by_date[d] for d in error_dates_sorted])
|
|
725
|
+
|
|
726
|
+
error_stat_html = f''' <div class="stat">
|
|
727
|
+
<div class="stat-label">Errors</div>
|
|
728
|
+
<div class="stat-value">{total_errors}</div>
|
|
729
|
+
<div class="stat-sub">{error_rate} per 100 prompts</div>
|
|
730
|
+
</div>'''
|
|
731
|
+
error_section_html = f'''<div class="section">
|
|
732
|
+
<div class="section-header errors">Errors</div>
|
|
733
|
+
<div class="grid">
|
|
734
|
+
<div class="card">
|
|
735
|
+
<h2>Error types</h2>
|
|
736
|
+
<canvas id="errorTypes"></canvas>
|
|
737
|
+
</div>
|
|
738
|
+
<div class="card">
|
|
739
|
+
<h2>Errors by tool</h2>
|
|
740
|
+
<canvas id="errorTools"></canvas>
|
|
741
|
+
</div>
|
|
742
|
+
<div class="card wide">
|
|
743
|
+
<h2>Error trend per day</h2>
|
|
744
|
+
<canvas id="errorTrend"></canvas>
|
|
745
|
+
</div>
|
|
746
|
+
</div>
|
|
747
|
+
</div>
|
|
748
|
+
|
|
749
|
+
'''
|
|
750
|
+
error_js_constants = f'''const ERROR_TYPE_LABELS = {error_type_labels_js};
|
|
751
|
+
const ERROR_TYPE_VALUES = {error_type_values_js};
|
|
752
|
+
const ERROR_TOOL_LABELS = {error_tool_labels_js};
|
|
753
|
+
const ERROR_TOOL_VALUES = {error_tool_values_js};
|
|
754
|
+
const ERROR_DATES = {error_dates_js};
|
|
755
|
+
const ERROR_DATE_VALUES = {error_date_values_js};'''
|
|
756
|
+
error_js_charts = '''
|
|
757
|
+
// Error types (horizontal bar)
|
|
758
|
+
new Chart(document.getElementById('errorTypes'), {
|
|
759
|
+
type: 'bar',
|
|
760
|
+
data: {
|
|
761
|
+
labels: ERROR_TYPE_LABELS,
|
|
762
|
+
datasets: [{ label: 'Errors', data: ERROR_TYPE_VALUES,
|
|
763
|
+
backgroundColor: '#fb7185', borderRadius: 4 }]
|
|
764
|
+
},
|
|
765
|
+
options: { ...baseOpts, indexAxis: 'y' }
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// Errors by tool (bar)
|
|
769
|
+
new Chart(document.getElementById('errorTools'), {
|
|
770
|
+
type: 'bar',
|
|
771
|
+
data: {
|
|
772
|
+
labels: ERROR_TOOL_LABELS,
|
|
773
|
+
datasets: [{ label: 'Errors', data: ERROR_TOOL_VALUES,
|
|
774
|
+
backgroundColor: '#e11d48', borderRadius: 4 }]
|
|
775
|
+
},
|
|
776
|
+
options: baseOpts
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// Error trend per day (line)
|
|
780
|
+
new Chart(document.getElementById('errorTrend'), {
|
|
781
|
+
type: 'line',
|
|
782
|
+
data: {
|
|
783
|
+
labels: ERROR_DATES,
|
|
784
|
+
datasets: [{ label: 'Errors', data: ERROR_DATE_VALUES,
|
|
785
|
+
borderColor: '#e11d48', backgroundColor: 'rgba(225,29,72,0.15)',
|
|
786
|
+
fill: true, tension: 0.3, pointRadius: 3 }]
|
|
787
|
+
},
|
|
788
|
+
options: baseOpts
|
|
789
|
+
});'''
|
|
790
|
+
else:
|
|
791
|
+
error_stat_html = ''
|
|
792
|
+
error_section_html = ''
|
|
793
|
+
error_js_constants = ''
|
|
794
|
+
error_js_charts = ''
|
|
795
|
+
|
|
288
796
|
html = f"""<!DOCTYPE html>
|
|
289
797
|
<html lang="en">
|
|
290
798
|
<head>
|
|
@@ -314,6 +822,10 @@ html = f"""<!DOCTYPE html>
|
|
|
314
822
|
.section-header.cost {{ border-left: 3px solid #6366f1; color: #818cf8; }}
|
|
315
823
|
.section-header.time {{ border-left: 3px solid #34d399; color: #34d399; }}
|
|
316
824
|
.section-header.prompts {{ border-left: 3px solid #a78bfa; color: #a78bfa; }}
|
|
825
|
+
.section-header.agents {{ border-left: 3px solid #f97316; color: #fb923c; }}
|
|
826
|
+
.section-header.friction {{ border-left: 3px solid #ef4444; color: #f87171; }}
|
|
827
|
+
.section-header.errors {{ border-left: 3px solid #e11d48; color: #fb7185; }}
|
|
828
|
+
.section-header.skills {{ border-left: 3px solid #f59e0b; color: #fbbf24; }}
|
|
317
829
|
.grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }}
|
|
318
830
|
.card {{ background: #1e2330; border: 1px solid #2d3748; border-radius: 10px;
|
|
319
831
|
padding: 16px; }}
|
|
@@ -350,6 +862,11 @@ html = f"""<!DOCTYPE html>
|
|
|
350
862
|
<div class="stat-value">{total_output:,}</div>
|
|
351
863
|
<div class="stat-sub"> </div>
|
|
352
864
|
</div>
|
|
865
|
+
<div class="stat">
|
|
866
|
+
<div class="stat-label">Input tokens</div>
|
|
867
|
+
<div class="stat-value">{total_input:,}</div>
|
|
868
|
+
<div class="stat-sub"> </div>
|
|
869
|
+
</div>
|
|
353
870
|
<div class="stat">
|
|
354
871
|
<div class="stat-label">Cache read share</div>
|
|
355
872
|
<div class="stat-value">{cache_pct}%</div>
|
|
@@ -370,6 +887,11 @@ html = f"""<!DOCTYPE html>
|
|
|
370
887
|
<div class="stat-value">{overall_efficiency}%</div>
|
|
371
888
|
<div class="stat-sub">key / non-trivial (higher = better)</div>
|
|
372
889
|
</div>
|
|
890
|
+
{agents_stat_html}
|
|
891
|
+
{skills_stat_html}
|
|
892
|
+
{friction_stat_html}
|
|
893
|
+
{retry_stat_html}
|
|
894
|
+
{error_stat_html}
|
|
373
895
|
</div>
|
|
374
896
|
|
|
375
897
|
<div class="section">
|
|
@@ -396,10 +918,15 @@ html = f"""<!DOCTYPE html>
|
|
|
396
918
|
<canvas id="modelStack"></canvas>
|
|
397
919
|
</div>
|
|
398
920
|
|
|
921
|
+
<div class="card wide">
|
|
922
|
+
<h2>Token composition per day</h2>
|
|
923
|
+
<canvas id="tokenComp"></canvas>
|
|
924
|
+
</div>
|
|
925
|
+
|
|
399
926
|
</div>
|
|
400
927
|
</div>
|
|
401
928
|
|
|
402
|
-
<div class="section">
|
|
929
|
+
{agents_section_html}{skills_section_html}{friction_section_html}{error_section_html}<div class="section">
|
|
403
930
|
<div class="section-header prompts">Key Prompts</div>
|
|
404
931
|
<div class="grid">
|
|
405
932
|
|
|
@@ -477,6 +1004,9 @@ const DATES = {dates_js};
|
|
|
477
1004
|
const COST_BY_DATE = {cost_by_date_js};
|
|
478
1005
|
const SESSIONS_BY_DATE = {sessions_by_date_js};
|
|
479
1006
|
const OUTPUT_BY_DATE = {output_by_date_js};
|
|
1007
|
+
const INPUT_BY_DATE = {input_by_date_js};
|
|
1008
|
+
const CACHE_CREATE_BY_DATE = {cache_create_by_date_js};
|
|
1009
|
+
const CACHE_READ_BY_DATE = {cache_read_by_date_js};
|
|
480
1010
|
const OPUS_BY_DATE = {opus_by_date_js};
|
|
481
1011
|
const SONNET_BY_DATE = {sonnet_by_date_js};
|
|
482
1012
|
const CUMUL_LABELS = {cumul_labels_js};
|
|
@@ -501,6 +1031,10 @@ const AVG_DURATION_BY_DATE = {avg_duration_by_date_js};
|
|
|
501
1031
|
const SCATTER_DATA = {scatter_data_js};
|
|
502
1032
|
const TPM_DATA = {tpm_data_js};
|
|
503
1033
|
const DUR_HIST_RANGES = {dur_hist_ranges_js};
|
|
1034
|
+
{agents_js_constants}
|
|
1035
|
+
{skills_js_constants}
|
|
1036
|
+
{friction_js_constants}
|
|
1037
|
+
{error_js_constants}
|
|
504
1038
|
|
|
505
1039
|
function formatDuration(s) {{
|
|
506
1040
|
if (s <= 0) return '0s';
|
|
@@ -576,6 +1110,23 @@ new Chart(document.getElementById('modelStack'), {{
|
|
|
576
1110
|
tooltip: {{ callbacks: {{ label: ctx => ' $' + ctx.parsed.y.toFixed(2) }} }} }} }}
|
|
577
1111
|
}});
|
|
578
1112
|
|
|
1113
|
+
// Token composition per day (stacked bar)
|
|
1114
|
+
new Chart(document.getElementById('tokenComp'), {{
|
|
1115
|
+
type: 'bar',
|
|
1116
|
+
data: {{
|
|
1117
|
+
labels: DATES,
|
|
1118
|
+
datasets: [
|
|
1119
|
+
{{ label: 'Input', data: INPUT_BY_DATE, backgroundColor: '#6366f1', borderRadius: 2 }},
|
|
1120
|
+
{{ label: 'Cache creation', data: CACHE_CREATE_BY_DATE, backgroundColor: '#f59e0b', borderRadius: 2 }},
|
|
1121
|
+
{{ label: 'Cache read', data: CACHE_READ_BY_DATE, backgroundColor: '#22d3ee', borderRadius: 2 }},
|
|
1122
|
+
{{ label: 'Output', data: OUTPUT_BY_DATE, backgroundColor: '#a78bfa', borderRadius: 2 }}
|
|
1123
|
+
]
|
|
1124
|
+
}},
|
|
1125
|
+
options: {{ ...baseOpts, scales: {{ ...baseOpts.scales,
|
|
1126
|
+
x: {{ ...baseOpts.scales.x, stacked: true }},
|
|
1127
|
+
y: {{ ...baseOpts.scales.y, stacked: true }} }} }}
|
|
1128
|
+
}});
|
|
1129
|
+
|
|
579
1130
|
// Session duration per day
|
|
580
1131
|
new Chart(document.getElementById('durationDay'), {{
|
|
581
1132
|
type: 'bar',
|
|
@@ -757,10 +1308,14 @@ new Chart(document.getElementById('promptStack'), {{
|
|
|
757
1308
|
}}
|
|
758
1309
|
}}
|
|
759
1310
|
}});
|
|
1311
|
+
{agents_js_charts}
|
|
1312
|
+
{skills_js_charts}
|
|
1313
|
+
{friction_js_charts}
|
|
1314
|
+
{error_js_charts}
|
|
760
1315
|
</script>
|
|
761
1316
|
</body>
|
|
762
1317
|
</html>
|
|
763
1318
|
"""
|
|
764
1319
|
|
|
765
|
-
with open(output_file, "w") as f:
|
|
1320
|
+
with open(output_file, "w", encoding='utf-8') as f:
|
|
766
1321
|
f.write(html)
|