@yurukusa/cc-speed 1.0.0
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 +50 -0
- package/cli.mjs +220 -0
- package/index.html +137 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# cc-speed
|
|
2
|
+
|
|
3
|
+
> How fast does Claude Code actually work?
|
|
4
|
+
|
|
5
|
+
Measures tool execution rate (tools/hour) per session — the pace at which Claude calls tools, classified into slow / medium / fast / burst tiers.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx cc-speed
|
|
11
|
+
npx cc-speed --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or open `index.html` in a browser and drag in `.jsonl` files.
|
|
15
|
+
|
|
16
|
+
## Metrics
|
|
17
|
+
|
|
18
|
+
- **Median tools/hr** — typical execution pace
|
|
19
|
+
- **p90** — what a fast session looks like
|
|
20
|
+
- **Pace tiers** — slow (<50) / medium (50–199) / fast (200–599) / burst (600+)
|
|
21
|
+
- **Top sessions** — fastest sessions with tool count and duration
|
|
22
|
+
|
|
23
|
+
## Sample output
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
cc-speed — How fast does Claude Code actually work?
|
|
27
|
+
|
|
28
|
+
Sessions analyzed: 393
|
|
29
|
+
|
|
30
|
+
Execution rate
|
|
31
|
+
median 99 tools/hr
|
|
32
|
+
mean 169 tools/hr
|
|
33
|
+
p90 410 tools/hr
|
|
34
|
+
max 1,435 tools/hr
|
|
35
|
+
|
|
36
|
+
Pace tiers
|
|
37
|
+
slow ██████░░░░░░░░░░░░░░░░ 28% <50 tools/hr — careful work
|
|
38
|
+
medium ████████░░░░░░░░░░░░░░ 38% 50–199 tools/hr
|
|
39
|
+
fast ███████░░░░░░░░░░░░░░░ 30% 200–599 tools/hr
|
|
40
|
+
burst █░░░░░░░░░░░░░░░░░░░░░ 3% 600+ tools/hr
|
|
41
|
+
|
|
42
|
+
Fastest sessions
|
|
43
|
+
#1 1,435 tools/hr — 12 tools in 1 min
|
|
44
|
+
#2 1,005 tools/hr — 15 tools in 1 min
|
|
45
|
+
#3 851 tools/hr — 20 tools in 1 min
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cc-speed — How fast does Claude Code actually work?
|
|
4
|
+
* Measures tool execution rate (tools/hour) per session.
|
|
5
|
+
* Shows pace tiers, median speed, and top burst sessions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdirSync, statSync } from 'fs';
|
|
9
|
+
import { createInterface } from 'readline';
|
|
10
|
+
import { createReadStream } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const jsonMode = args.includes('--json');
|
|
16
|
+
const showHelp = args.includes('--help') || args.includes('-h');
|
|
17
|
+
|
|
18
|
+
if (showHelp) {
|
|
19
|
+
process.stdout.write(`cc-speed — How fast does Claude Code actually work?
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
npx cc-speed # Tool execution rate analysis
|
|
23
|
+
npx cc-speed --json # JSON output
|
|
24
|
+
|
|
25
|
+
Metrics:
|
|
26
|
+
- Tools/hour rate per session (pace)
|
|
27
|
+
- Pace tiers: slow / medium / fast / burst
|
|
28
|
+
- Median, mean, p90 rate across sessions
|
|
29
|
+
- Top-speed session details
|
|
30
|
+
`);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const C = {
|
|
35
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
36
|
+
cyan: '\x1b[36m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
37
|
+
blue: '\x1b[34m', purple: '\x1b[35m', red: '\x1b[31m',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function bar(pct, width = 24) {
|
|
41
|
+
const filled = Math.round(pct * width);
|
|
42
|
+
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function humanNum(n) {
|
|
46
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
47
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
|
48
|
+
return n.toString();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fmt(n) { return Math.round(n).toLocaleString(); }
|
|
52
|
+
|
|
53
|
+
const claudeDir = join(homedir(), '.claude', 'projects');
|
|
54
|
+
|
|
55
|
+
const sessions = [];
|
|
56
|
+
|
|
57
|
+
async function processFile(filePath) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const rl = createInterface({
|
|
60
|
+
input: createReadStream(filePath),
|
|
61
|
+
crlfDelay: Infinity,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const timestamps = [];
|
|
65
|
+
let toolCount = 0;
|
|
66
|
+
|
|
67
|
+
rl.on('line', (line) => {
|
|
68
|
+
if (!line) return;
|
|
69
|
+
let d;
|
|
70
|
+
try { d = JSON.parse(line); } catch { return; }
|
|
71
|
+
|
|
72
|
+
// Grab timestamps
|
|
73
|
+
if (d.timestamp) {
|
|
74
|
+
const t = new Date(d.timestamp).getTime();
|
|
75
|
+
if (!isNaN(t)) timestamps.push(t);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Count tool calls in assistant messages
|
|
79
|
+
const msg = d.message || d;
|
|
80
|
+
if (!msg || msg.role !== 'assistant') return;
|
|
81
|
+
for (const c of (msg.content || [])) {
|
|
82
|
+
if (c && c.type === 'tool_use') toolCount++;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
rl.on('close', () => {
|
|
87
|
+
if (timestamps.length < 2 || toolCount < 3) {
|
|
88
|
+
resolve(null);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let minTs = timestamps[0], maxTs = timestamps[0];
|
|
92
|
+
for (const t of timestamps) { if (t < minTs) minTs = t; if (t > maxTs) maxTs = t; }
|
|
93
|
+
const durationMin = (maxTs - minTs) / 60000;
|
|
94
|
+
if (durationMin < 0.5) {
|
|
95
|
+
resolve(null);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const rate = toolCount / (durationMin / 60); // tools per hour
|
|
99
|
+
resolve({ rate, toolCount, durationMin });
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function main() {
|
|
105
|
+
let projectDirs;
|
|
106
|
+
try {
|
|
107
|
+
projectDirs = readdirSync(claudeDir);
|
|
108
|
+
} catch {
|
|
109
|
+
process.stderr.write(`Cannot read ${claudeDir}\n`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const allFiles = [];
|
|
114
|
+
for (const pd of projectDirs) {
|
|
115
|
+
const pdPath = join(claudeDir, pd);
|
|
116
|
+
try {
|
|
117
|
+
const files = readdirSync(pdPath).filter(f => f.endsWith('.jsonl'));
|
|
118
|
+
for (const f of files) allFiles.push(join(pdPath, f));
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const BATCH = 16;
|
|
123
|
+
for (let i = 0; i < allFiles.length; i += BATCH) {
|
|
124
|
+
const batch = allFiles.slice(i, i + BATCH);
|
|
125
|
+
const results = await Promise.all(batch.map(f => processFile(f)));
|
|
126
|
+
for (const r of results) {
|
|
127
|
+
if (r) sessions.push(r);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (sessions.length === 0) {
|
|
132
|
+
process.stderr.write('No sessions with timing data found.\n');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const rates = sessions.map(s => s.rate).sort((a, b) => a - b);
|
|
137
|
+
const n = rates.length;
|
|
138
|
+
const median = rates[Math.floor(n / 2)];
|
|
139
|
+
const mean = rates.reduce((a, b) => a + b, 0) / n;
|
|
140
|
+
const p90 = rates[Math.floor(n * 0.9)];
|
|
141
|
+
const max = rates[n - 1];
|
|
142
|
+
|
|
143
|
+
const slow = rates.filter(r => r < 50).length;
|
|
144
|
+
const medium = rates.filter(r => r >= 50 && r < 200).length;
|
|
145
|
+
const fast = rates.filter(r => r >= 200 && r < 600).length;
|
|
146
|
+
const burst = rates.filter(r => r >= 600).length;
|
|
147
|
+
|
|
148
|
+
// Top sessions by rate
|
|
149
|
+
const top = [...sessions].sort((a, b) => b.rate - a.rate).slice(0, 3);
|
|
150
|
+
|
|
151
|
+
if (jsonMode) {
|
|
152
|
+
process.stdout.write(JSON.stringify({
|
|
153
|
+
sessionsAnalyzed: n,
|
|
154
|
+
rate: {
|
|
155
|
+
median: Math.round(median),
|
|
156
|
+
mean: Math.round(mean),
|
|
157
|
+
p90: Math.round(p90),
|
|
158
|
+
max: Math.round(max),
|
|
159
|
+
},
|
|
160
|
+
tiers: { slow, medium, fast, burst },
|
|
161
|
+
topSessions: top.map(s => ({
|
|
162
|
+
rate: Math.round(s.rate),
|
|
163
|
+
tools: s.toolCount,
|
|
164
|
+
durationMin: Math.round(s.durationMin),
|
|
165
|
+
})),
|
|
166
|
+
}, null, 2) + '\n');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Pretty output ─────────────────────────────────────────────────────────
|
|
171
|
+
process.stdout.write(`\n${C.bold}${C.cyan}cc-speed${C.reset} — How fast does Claude Code actually work?\n\n`);
|
|
172
|
+
process.stdout.write(`${C.bold}Sessions analyzed:${C.reset} ${humanNum(n)}\n\n`);
|
|
173
|
+
|
|
174
|
+
// Speed summary
|
|
175
|
+
process.stdout.write(`${C.bold}Execution rate${C.reset}\n`);
|
|
176
|
+
process.stdout.write(` median ${C.bold}${C.green}${fmt(median)}${C.reset} tools/hr\n`);
|
|
177
|
+
process.stdout.write(` mean ${C.bold}${fmt(mean)}${C.reset} tools/hr\n`);
|
|
178
|
+
process.stdout.write(` p90 ${C.bold}${fmt(p90)}${C.reset} tools/hr\n`);
|
|
179
|
+
process.stdout.write(` max ${C.bold}${C.purple}${fmt(max)}${C.reset} tools/hr\n`);
|
|
180
|
+
process.stdout.write(`\n`);
|
|
181
|
+
|
|
182
|
+
// Tier distribution
|
|
183
|
+
process.stdout.write(`${C.bold}Pace tiers${C.reset}\n`);
|
|
184
|
+
const tiers = [
|
|
185
|
+
['slow ', slow, C.dim, '<50 tools/hr — careful, deliberate work'],
|
|
186
|
+
['medium', medium, C.blue, '50–199 tools/hr — steady pace'],
|
|
187
|
+
['fast ', fast, C.green, '200–599 tools/hr — moving quickly'],
|
|
188
|
+
['burst ', burst, C.purple, '600+ tools/hr — full sprint'],
|
|
189
|
+
];
|
|
190
|
+
for (const [label, count, color, desc] of tiers) {
|
|
191
|
+
const pct = count / n;
|
|
192
|
+
process.stdout.write(
|
|
193
|
+
` ${label} ${color}${bar(pct, 22)}${C.reset} ${C.bold}${(pct * 100).toFixed(0).padStart(3)}%${C.reset} ${C.dim}(${humanNum(count)}) ${desc}${C.reset}\n`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
process.stdout.write(`\n`);
|
|
197
|
+
|
|
198
|
+
// Top sessions
|
|
199
|
+
process.stdout.write(`${C.bold}Fastest sessions${C.reset}\n`);
|
|
200
|
+
for (let i = 0; i < top.length; i++) {
|
|
201
|
+
const s = top[i];
|
|
202
|
+
process.stdout.write(
|
|
203
|
+
` #${i + 1} ${C.bold}${C.purple}${fmt(s.rate)}${C.reset} tools/hr — ${s.toolCount} tools in ${Math.round(s.durationMin)} min\n`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
process.stdout.write(`\n`);
|
|
207
|
+
|
|
208
|
+
// Insight
|
|
209
|
+
process.stdout.write(`${C.dim}─────────────────────────────────────────────${C.reset}\n`);
|
|
210
|
+
const burstPct = (burst / n * 100).toFixed(0);
|
|
211
|
+
if (burst / n >= 0.4) {
|
|
212
|
+
process.stdout.write(`${C.purple}${C.bold}${burstPct}% of sessions run at 600+ tools/hr.${C.reset}\n`);
|
|
213
|
+
process.stdout.write(`${C.dim}At median ${fmt(median)} tools/hr, that's ~${Math.round(median / 60)} tool calls per minute.${C.reset}\n`);
|
|
214
|
+
} else {
|
|
215
|
+
process.stdout.write(`${C.green}${C.bold}Median pace: ${fmt(median)} tools/hr${C.reset}${C.dim} — ${Math.round(median / 60)} tool calls/min.${C.reset}\n`);
|
|
216
|
+
}
|
|
217
|
+
process.stdout.write(`\n`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
main().catch(e => { process.stderr.write(e.message + '\n'); process.exit(1); });
|
package/index.html
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>cc-speed — How fast does Claude Code work?</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#e6edf3;--dim:#8b949e;
|
|
10
|
+
--green:#3fb950;--yellow:#d29922;--blue:#58a6ff;--purple:#bc8cff;--accent:#3fb950;
|
|
11
|
+
}
|
|
12
|
+
*{box-sizing:border-box;margin:0;padding:0;}
|
|
13
|
+
body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,sans-serif;min-height:100vh;padding:2rem 1rem;}
|
|
14
|
+
.container{max-width:780px;margin:0 auto;}
|
|
15
|
+
h1{font-size:1.5rem;font-weight:700;margin-bottom:.25rem;}
|
|
16
|
+
h1 span{color:var(--accent);}
|
|
17
|
+
.subtitle{color:var(--dim);font-size:.9rem;margin-bottom:2rem;}
|
|
18
|
+
.upload-zone{border:2px dashed var(--border);border-radius:10px;padding:2.5rem;text-align:center;cursor:pointer;transition:border-color .2s;margin-bottom:2rem;}
|
|
19
|
+
.upload-zone:hover,.upload-zone.drag{border-color:var(--accent);background:#0f1e13;}
|
|
20
|
+
.upload-zone p{color:var(--dim);font-size:.9rem;margin-top:.5rem;}
|
|
21
|
+
.upload-btn{background:var(--accent);color:#0d1117;border:none;padding:.5rem 1.2rem;border-radius:6px;cursor:pointer;font-weight:600;font-size:.9rem;}
|
|
22
|
+
.demo-btn{background:transparent;border:1px solid var(--border);color:var(--dim);padding:.4rem 1rem;border-radius:6px;cursor:pointer;font-size:.85rem;margin-top:.75rem;}
|
|
23
|
+
.demo-btn:hover{border-color:var(--accent);color:var(--text);}
|
|
24
|
+
#results{display:none;}
|
|
25
|
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:1rem;}
|
|
26
|
+
.card-title{font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--dim);margin-bottom:.75rem;}
|
|
27
|
+
.big-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:.75rem;margin-bottom:1rem;}
|
|
28
|
+
.stat-box{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:1rem;text-align:center;}
|
|
29
|
+
.stat-val{font-size:1.6rem;font-weight:800;line-height:1;}
|
|
30
|
+
.stat-key{font-size:.75rem;color:var(--dim);margin-top:.3rem;}
|
|
31
|
+
.bar-row{display:flex;align-items:center;gap:.75rem;margin-bottom:.6rem;font-size:.85rem;}
|
|
32
|
+
.bar-label{width:70px;color:var(--dim);flex-shrink:0;}
|
|
33
|
+
.bar-track{flex:1;height:10px;background:var(--border);border-radius:5px;overflow:hidden;}
|
|
34
|
+
.bar-fill{height:100%;border-radius:5px;transition:width .6s ease;}
|
|
35
|
+
.bar-pct{width:42px;text-align:right;font-weight:600;}
|
|
36
|
+
.bar-count{width:55px;color:var(--dim);font-size:.78rem;}
|
|
37
|
+
.top-item{display:flex;align-items:center;gap:.75rem;padding:.4rem 0;border-bottom:1px solid var(--border);font-size:.85rem;}
|
|
38
|
+
.top-item:last-child{border:none;}
|
|
39
|
+
.top-rank{width:28px;color:var(--dim);font-size:.8rem;}
|
|
40
|
+
.top-rate{font-weight:700;color:var(--purple);min-width:70px;}
|
|
41
|
+
.top-detail{color:var(--dim);}
|
|
42
|
+
.insight{background:#0f1e13;border:1px solid var(--green);border-radius:8px;padding:1rem 1.25rem;font-size:.9rem;line-height:1.5;margin-bottom:1rem;}
|
|
43
|
+
.path-hint{background:#1e1a0c;border:1px solid var(--border);border-radius:6px;padding:.6rem 1rem;font-size:.8rem;color:var(--dim);font-family:monospace;margin-bottom:2rem;}
|
|
44
|
+
@media(max-width:540px){.big-stats{grid-template-columns:1fr 1fr;}}
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
<div class="container">
|
|
49
|
+
<h1><span>cc-speed</span></h1>
|
|
50
|
+
<p class="subtitle">How fast does Claude Code actually work?</p>
|
|
51
|
+
<div class="path-hint">📁 ~/.claude/projects/<project>/*.jsonl — drag files below</div>
|
|
52
|
+
<div class="upload-zone" id="dz">
|
|
53
|
+
<button class="upload-btn" onclick="document.getElementById('fi').click()">Upload JSONL files</button>
|
|
54
|
+
<p>or drag and drop from ~/.claude/projects/</p><br>
|
|
55
|
+
<button class="demo-btn" onclick="loadDemo()">Try with demo data</button>
|
|
56
|
+
<input type="file" id="fi" accept=".jsonl" multiple style="display:none" onchange="handleFiles(this.files)">
|
|
57
|
+
</div>
|
|
58
|
+
<div id="results">
|
|
59
|
+
<div class="big-stats">
|
|
60
|
+
<div class="stat-box"><div class="stat-val" id="vSessions" style="color:var(--blue)">—</div><div class="stat-key">sessions</div></div>
|
|
61
|
+
<div class="stat-box"><div class="stat-val" id="vMedian" style="color:var(--green)">—</div><div class="stat-key">median tools/hr</div></div>
|
|
62
|
+
<div class="stat-box"><div class="stat-val" id="vP90" style="color:var(--yellow)">—</div><div class="stat-key">p90 tools/hr</div></div>
|
|
63
|
+
<div class="stat-box"><div class="stat-val" id="vMax" style="color:var(--purple)">—</div><div class="stat-key">max tools/hr</div></div>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="card">
|
|
66
|
+
<div class="card-title">Pace tiers</div>
|
|
67
|
+
<div class="bar-row"><span class="bar-label">slow</span><div class="bar-track"><div class="bar-fill" id="bSlow" style="background:var(--dim)"></div></div><span class="bar-pct" id="pSlow">—</span><span class="bar-count" id="cSlow"></span></div>
|
|
68
|
+
<div class="bar-row"><span class="bar-label">medium</span><div class="bar-track"><div class="bar-fill" id="bMed" style="background:var(--blue)"></div></div><span class="bar-pct" id="pMed">—</span><span class="bar-count" id="cMed"></span></div>
|
|
69
|
+
<div class="bar-row"><span class="bar-label">fast</span><div class="bar-track"><div class="bar-fill" id="bFast" style="background:var(--green)"></div></div><span class="bar-pct" id="pFast">—</span><span class="bar-count" id="cFast"></span></div>
|
|
70
|
+
<div class="bar-row"><span class="bar-label">burst</span><div class="bar-track"><div class="bar-fill" id="bBurst" style="background:var(--purple)"></div></div><span class="bar-pct" id="pBurst">—</span><span class="bar-count" id="cBurst"></span></div>
|
|
71
|
+
<p style="font-size:.75rem;color:var(--dim);margin-top:.75rem">slow <50 | medium 50–199 | fast 200–599 | burst 600+ (tools/hr)</p>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="card">
|
|
74
|
+
<div class="card-title">Fastest sessions</div>
|
|
75
|
+
<div id="topList"></div>
|
|
76
|
+
</div>
|
|
77
|
+
<div id="insightBox"></div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<script>
|
|
81
|
+
const dz=document.getElementById('dz');
|
|
82
|
+
dz.addEventListener('dragover',e=>{e.preventDefault();dz.classList.add('drag');});
|
|
83
|
+
dz.addEventListener('dragleave',()=>dz.classList.remove('drag'));
|
|
84
|
+
dz.addEventListener('drop',e=>{e.preventDefault();dz.classList.remove('drag');handleFiles(e.dataTransfer.files);});
|
|
85
|
+
function handleFiles(files){
|
|
86
|
+
Promise.all([...files].map(f=>new Promise((res,rej)=>{const r=new FileReader();r.onload=e=>res(e.target.result);r.onerror=rej;r.readAsText(f);})))
|
|
87
|
+
.then(texts=>analyze(texts.join('\n')));
|
|
88
|
+
}
|
|
89
|
+
function analyze(raw){
|
|
90
|
+
const sessionMap={};
|
|
91
|
+
for(const line of raw.split('\n')){
|
|
92
|
+
if(!line)continue;
|
|
93
|
+
let d;try{d=JSON.parse(line);}catch{continue;}
|
|
94
|
+
const sid=d.sessionId||d.session_id||'x';
|
|
95
|
+
if(!sessionMap[sid])sessionMap[sid]={ts:[],tools:0};
|
|
96
|
+
if(d.timestamp){const t=new Date(d.timestamp).getTime();if(!isNaN(t))sessionMap[sid].ts.push(t);}
|
|
97
|
+
const msg=d.message||d;
|
|
98
|
+
if(msg&&msg.role==='assistant')for(const c of(msg.content||[]))if(c&&c.type==='tool_use')sessionMap[sid].tools++;
|
|
99
|
+
}
|
|
100
|
+
const sessions=[];
|
|
101
|
+
for(const s of Object.values(sessionMap)){
|
|
102
|
+
if(s.ts.length<2||s.tools<3)continue;
|
|
103
|
+
let mn=s.ts[0],mx=s.ts[0];for(const t of s.ts){if(t<mn)mn=t;if(t>mx)mx=t;}
|
|
104
|
+
const dur=(mx-mn)/60000;if(dur<0.5)continue;
|
|
105
|
+
sessions.push({rate:s.tools/(dur/60),tools:s.tools,dur});
|
|
106
|
+
}
|
|
107
|
+
if(!sessions.length){alert('No sessions with timing data found.');return;}
|
|
108
|
+
const rates=sessions.map(s=>s.rate).sort((a,b)=>a-b);
|
|
109
|
+
const n=rates.length;
|
|
110
|
+
const median=rates[Math.floor(n/2)];
|
|
111
|
+
const mean=rates.reduce((a,b)=>a+b,0)/n;
|
|
112
|
+
const p90=rates[Math.floor(n*0.9)];
|
|
113
|
+
const max=rates[n-1];
|
|
114
|
+
const slow=rates.filter(r=>r<50).length;
|
|
115
|
+
const med=rates.filter(r=>r>=50&&r<200).length;
|
|
116
|
+
const fast=rates.filter(r=>r>=200&&r<600).length;
|
|
117
|
+
const burst=rates.filter(r=>r>=600).length;
|
|
118
|
+
const top=[...sessions].sort((a,b)=>b.rate-a.rate).slice(0,5);
|
|
119
|
+
render({n,median,mean,p90,max,slow,med,fast,burst,top});
|
|
120
|
+
}
|
|
121
|
+
function hn(n){return n>=1e3?(n/1e3).toFixed(1)+'K':Math.round(n).toLocaleString();}
|
|
122
|
+
function pct(a,t){return t?Math.round(a/t*100):0;}
|
|
123
|
+
function render(d){
|
|
124
|
+
document.getElementById('results').style.display='block';
|
|
125
|
+
document.getElementById('vSessions').textContent=hn(d.n);
|
|
126
|
+
document.getElementById('vMedian').textContent=hn(d.median);
|
|
127
|
+
document.getElementById('vP90').textContent=hn(d.p90);
|
|
128
|
+
document.getElementById('vMax').textContent=hn(d.max);
|
|
129
|
+
const sb=(bId,pId,cId,n)=>{const p=pct(n,d.n);document.getElementById(bId).style.width=p+'%';document.getElementById(pId).textContent=p+'%';document.getElementById(cId).textContent='('+n+')';};
|
|
130
|
+
sb('bSlow','pSlow','cSlow',d.slow);sb('bMed','pMed','cMed',d.med);sb('bFast','pFast','cFast',d.fast);sb('bBurst','pBurst','cBurst',d.burst);
|
|
131
|
+
document.getElementById('topList').innerHTML=d.top.map((s,i)=>`<div class="top-item"><span class="top-rank">#${i+1}</span><span class="top-rate">${hn(s.rate)} /hr</span><span class="top-detail">${s.tools} tools in ${Math.round(s.dur)} min</span></div>`).join('');
|
|
132
|
+
document.getElementById('insightBox').innerHTML=`<div class="insight"><strong>Median: ${hn(d.median)} tools/hr</strong> — that's ~${Math.round(d.median/60)} tool calls per minute.<br>Top ${pct(d.fast+d.burst,d.n)}% of sessions run at 200+ tools/hr. Burst sessions hit ${hn(d.max)} tools/hr.</div>`;
|
|
133
|
+
}
|
|
134
|
+
function loadDemo(){render({n:393,median:99,mean:169,p90:410,max:1435,slow:112,med:150,fast:119,burst:12,top:[{rate:1435,tools:12,dur:1},{rate:1005,tools:15,dur:1},{rate:851,tools:20,dur:1},{rate:720,tools:18,dur:1.5},{rate:600,tools:25,dur:2.5}]});}
|
|
135
|
+
</script>
|
|
136
|
+
</body>
|
|
137
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yurukusa/cc-speed",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "How fast does Claude Code actually work? Tool execution rate (tools/hour), pace tiers, and burst session analysis.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc-speed": "./cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node cli.mjs"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude",
|
|
14
|
+
"claude-code",
|
|
15
|
+
"ai",
|
|
16
|
+
"developer-tools",
|
|
17
|
+
"analytics",
|
|
18
|
+
"performance"
|
|
19
|
+
],
|
|
20
|
+
"author": "yurukusa",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
}
|
|
25
|
+
}
|