@yurukusa/cc-streak 1.0.1

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.
Files changed (4) hide show
  1. package/README.md +50 -0
  2. package/cli.mjs +224 -0
  3. package/index.html +265 -0
  4. package/package.json +26 -0
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # cc-streak
2
+
3
+ How long can Claude Code go without an error?
4
+
5
+ ```
6
+ npx cc-streak
7
+ ```
8
+
9
+ Measures consecutive successful tool calls between errors across all your Claude Code sessions.
10
+
11
+ ## Output
12
+
13
+ ```
14
+ cc-streak — Error-Free Streaks in Claude Code
15
+ ====================================================
16
+ Sessions: 1,994 | Streaks: 6,221 | Median: 12 | Max: 829
17
+
18
+ Streak length distribution:
19
+ 1-2 calls █████████░░░░░░░░░░░ 16.7% (1,036)
20
+ 3-5 ████████░░░░░░░░░░░░ 15.3% (954)
21
+ 6-20 ████████████████████ 36.2% (2,254)
22
+ 21-50 ████████████░░░░░░░░ 21.3% (1,328)
23
+ 51-100 ████░░░░░░░░░░░░░░░░ 7.3% (455)
24
+ 101+ ██░░░░░░░░░░░░░░░░░░ 3.1% (194)
25
+
26
+ Stats: median 12 | mean 22.3 | p90 52 | p99 161 | max 829
27
+
28
+ Streak breakers (which tool ends the run):
29
+ Bash ████████████████████ 51.5% (3,358)
30
+ Read █████████░░░░░░░░░░░ 24.0% (1,562)
31
+ Edit ████░░░░░░░░░░░░░░░░ 9.5% (617)
32
+ WebFetch ██░░░░░░░░░░░░░░░░░░ 5.8% (380)
33
+
34
+ Longest streak per session: median 16 | p90 50 | max 829
35
+ ```
36
+
37
+ ## Options
38
+
39
+ ```
40
+ npx cc-streak # terminal output
41
+ npx cc-streak --json # JSON output
42
+ ```
43
+
44
+ ## Browser Version
45
+
46
+ Open [cc-streak](https://yurukusa.github.io/cc-streak/) and drop your `~/.claude/projects/` folder.
47
+
48
+ ## Part of cc-toolkit
49
+
50
+ [106 free tools for Claude Code](https://yurukusa.github.io/cc-toolkit/)
package/cli.mjs ADDED
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+ // cc-streak — How long can Claude Code go without an error?
3
+ // Measures consecutive successful tool calls between errors.
4
+
5
+ import { readFileSync, readdirSync, statSync } from 'fs';
6
+ import { join, resolve } from 'path';
7
+ import { cpus } from 'os';
8
+
9
+ const CONCURRENCY = Math.min(cpus().length, 8);
10
+ const MIN_TOOLS = 3;
11
+
12
+ // UX tools that "error" by design
13
+ const UX_TOOLS = new Set(['ExitPlanMode', 'AskUserQuestion', 'EnterPlanMode']);
14
+
15
+ function analyzeFile(text) {
16
+ const toolMap = {};
17
+ let streak = 0;
18
+ const streaks = [];
19
+ const breakers = {};
20
+ let totalCalls = 0;
21
+ let totalErrors = 0;
22
+
23
+ for (const line of text.split('\n')) {
24
+ if (!line) continue;
25
+ let obj;
26
+ try { obj = JSON.parse(line); } catch { continue; }
27
+ const content = (obj.message || obj).content;
28
+ if (!Array.isArray(content)) continue;
29
+
30
+ for (const b of content) {
31
+ if (b.type === 'tool_use' && b.id && b.name) {
32
+ toolMap[b.id] = b.name;
33
+ } else if (b.type === 'tool_result') {
34
+ const name = toolMap[b.tool_use_id || ''] || 'unknown';
35
+ if (UX_TOOLS.has(name)) continue;
36
+ totalCalls++;
37
+ if (b.is_error) {
38
+ totalErrors++;
39
+ if (streak > 0) streaks.push(streak);
40
+ breakers[name] = (breakers[name] || 0) + 1;
41
+ streak = 0;
42
+ } else {
43
+ streak++;
44
+ }
45
+ }
46
+ }
47
+ }
48
+ if (streak > 0) streaks.push(streak);
49
+
50
+ return { streaks, breakers, totalCalls, totalErrors, hasData: totalCalls >= MIN_TOOLS };
51
+ }
52
+
53
+ function mergeResults(results) {
54
+ const merged = {
55
+ sessions: 0,
56
+ totalCalls: 0,
57
+ totalErrors: 0,
58
+ allStreaks: [],
59
+ breakers: {},
60
+ sessionLongest: [],
61
+ };
62
+
63
+ for (const r of results) {
64
+ if (!r.hasData) continue;
65
+ merged.sessions++;
66
+ merged.totalCalls += r.totalCalls;
67
+ merged.totalErrors += r.totalErrors;
68
+ merged.allStreaks.push(...r.streaks);
69
+ if (r.streaks.length > 0) {
70
+ merged.sessionLongest.push(Math.max(...r.streaks));
71
+ }
72
+ for (const [k, v] of Object.entries(r.breakers)) {
73
+ merged.breakers[k] = (merged.breakers[k] || 0) + v;
74
+ }
75
+ }
76
+
77
+ merged.allStreaks.sort((a, b) => a - b);
78
+ merged.sessionLongest.sort((a, b) => a - b);
79
+ return merged;
80
+ }
81
+
82
+ function findJsonlFiles(dir) {
83
+ const files = [];
84
+ try {
85
+ for (const name of readdirSync(dir)) {
86
+ const p = join(dir, name);
87
+ try {
88
+ const st = statSync(p);
89
+ if (st.isDirectory()) files.push(...findJsonlFiles(p));
90
+ else if (name.endsWith('.jsonl')) files.push(p);
91
+ } catch {}
92
+ }
93
+ } catch {}
94
+ return files;
95
+ }
96
+
97
+ async function processFiles(files) {
98
+ const results = [];
99
+ let idx = 0;
100
+ async function worker() {
101
+ while (idx < files.length) {
102
+ const f = files[idx++];
103
+ try { results.push(analyzeFile(readFileSync(f, 'utf8'))); } catch {}
104
+ }
105
+ }
106
+ await Promise.all(Array.from({ length: CONCURRENCY }, worker));
107
+ return results;
108
+ }
109
+
110
+ function bar(n, max, width = 20) {
111
+ const f = max > 0 ? Math.round((n / max) * width) : 0;
112
+ return '█'.repeat(f) + '░'.repeat(width - f);
113
+ }
114
+
115
+ function pct(n, d) {
116
+ return d > 0 ? (n / d * 100).toFixed(1) : '0.0';
117
+ }
118
+
119
+ function median(arr) {
120
+ if (arr.length === 0) return 0;
121
+ return arr[Math.floor(arr.length / 2)];
122
+ }
123
+
124
+ function percentile(arr, p) {
125
+ if (arr.length === 0) return 0;
126
+ return arr[Math.floor(arr.length * p)];
127
+ }
128
+
129
+ const TIERS = [
130
+ { name: 'micro', min: 1, max: 2, desc: '1-2 calls' },
131
+ { name: 'short', min: 3, max: 5, desc: '3-5' },
132
+ { name: 'medium', min: 6, max: 20, desc: '6-20' },
133
+ { name: 'long', min: 21, max: 50, desc: '21-50' },
134
+ { name: 'marathon', min: 51, max: 100, desc: '51-100' },
135
+ { name: 'epic', min: 101, max: Infinity, desc: '101+' },
136
+ ];
137
+
138
+ function renderOutput(m, isJson) {
139
+ const s = m.allStreaks;
140
+ const med = median(s);
141
+ const mean = s.length > 0 ? (s.reduce((a, v) => a + v, 0) / s.length).toFixed(1) : '0';
142
+ const p90 = percentile(s, 0.9);
143
+ const p99 = percentile(s, 0.99);
144
+ const max = s.length > 0 ? s[s.length - 1] : 0;
145
+
146
+ const tiers = TIERS.map(t => {
147
+ const count = s.filter(v => v >= t.min && v <= t.max).length;
148
+ return { ...t, count, pct: +(pct(count, s.length)) };
149
+ });
150
+
151
+ const totalBreaks = Object.values(m.breakers).reduce((a, v) => a + v, 0);
152
+ const breakerList = Object.entries(m.breakers)
153
+ .sort((a, b) => b[1] - a[1])
154
+ .slice(0, 8)
155
+ .map(([tool, count]) => ({ tool, count, pct: +(pct(count, totalBreaks)) }));
156
+
157
+ if (isJson) {
158
+ console.log(JSON.stringify({
159
+ sessions: m.sessions,
160
+ totalStreaks: s.length,
161
+ median: med,
162
+ mean: +mean,
163
+ p90,
164
+ p99,
165
+ max,
166
+ tiers,
167
+ breakers: breakerList,
168
+ perSession: {
169
+ medianLongest: median(m.sessionLongest),
170
+ p90Longest: percentile(m.sessionLongest, 0.9),
171
+ maxLongest: m.sessionLongest.length > 0 ? m.sessionLongest[m.sessionLongest.length - 1] : 0,
172
+ },
173
+ }, null, 2));
174
+ return;
175
+ }
176
+
177
+ console.log('\ncc-streak — Error-Free Streaks in Claude Code');
178
+ console.log('='.repeat(52));
179
+ console.log(`Sessions: ${m.sessions.toLocaleString()} | Streaks: ${s.length.toLocaleString()} | Median: ${med} | Max: ${max}`);
180
+
181
+ console.log('\nStreak length distribution:');
182
+ const maxTier = Math.max(...tiers.map(t => t.count));
183
+ for (const t of tiers) {
184
+ console.log(` ${t.desc.padEnd(10)} ${bar(t.count, maxTier)} ${String(t.pct).padStart(5)}% (${t.count.toLocaleString()})`);
185
+ }
186
+
187
+ console.log(`\nStats: median ${med} | mean ${mean} | p90 ${p90} | p99 ${p99} | max ${max}`);
188
+
189
+ console.log('\nStreak breakers (which tool ends the run):');
190
+ const maxBreak = breakerList.length > 0 ? breakerList[0].count : 1;
191
+ for (const b of breakerList) {
192
+ console.log(` ${b.tool.padEnd(20)} ${bar(b.count, maxBreak)} ${String(b.pct).padStart(5)}% (${b.count.toLocaleString()})`);
193
+ }
194
+
195
+ const sessMed = median(m.sessionLongest);
196
+ const sessMax = m.sessionLongest.length > 0 ? m.sessionLongest[m.sessionLongest.length - 1] : 0;
197
+ const sessP90 = percentile(m.sessionLongest, 0.9);
198
+ console.log(`\nLongest streak per session: median ${sessMed} | p90 ${sessP90} | max ${sessMax}`);
199
+ console.log('');
200
+ }
201
+
202
+ const args = process.argv.slice(2);
203
+ const isJson = args.includes('--json');
204
+
205
+ const dataDir = resolve(process.env.HOME || '~', '.claude', 'projects');
206
+ const files = findJsonlFiles(dataDir);
207
+
208
+ if (files.length === 0) {
209
+ console.error('No .jsonl files found in ~/.claude/projects/');
210
+ process.exit(1);
211
+ }
212
+
213
+ const rawResults = await processFiles(files);
214
+ const merged = mergeResults(rawResults);
215
+ renderOutput(merged, isJson);
216
+
217
+ if (!isJson) {
218
+ const dim = process.stdout.isTTY ? '\x1b[2m' : '';
219
+ const reset = process.stdout.isTTY ? '\x1b[0m' : '';
220
+ console.log();
221
+ console.log(` ${dim}Running Claude Code autonomously? Check your safety score:${reset}`);
222
+ console.log(` ${dim}npx cc-health-check${reset}`);
223
+ console.log(` ${dim}Full production kit: https://yurukusa.gumroad.com/l/pkbbl?utm_source=npm&utm_medium=cli&utm_campaign=ops-kit${reset}`);
224
+ }
package/index.html ADDED
@@ -0,0 +1,265 @@
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-streak — Error-Free Streaks in Claude Code</title>
7
+ <meta name="description" content="Median 12 successful tool calls between errors. Longest streak: 829. Bash breaks 52% of streaks. See how long Claude can go without failing.">
8
+ <meta property="og:title" content="cc-streak — Median 12 Tools Between Errors">
9
+ <meta property="og:description" content="Claude's longest error-free streak: 829 tool calls. Bash breaks 52% of streaks. Analyze your own patterns.">
10
+ <meta property="og:image" content="https://yurukusa.github.io/cc-streak/og.png">
11
+ <meta property="og:image:width" content="1200">
12
+ <meta property="og:image:height" content="630">
13
+ <meta name="twitter:card" content="summary_large_image">
14
+ <style>
15
+ *{box-sizing:border-box;margin:0;padding:0}
16
+ body{background:#0d1117;color:#e6edf3;font-family:'Segoe UI',system-ui,sans-serif;min-height:100vh}
17
+ .hero{background:linear-gradient(135deg,#161b22 0%,#0d1117 100%);border-bottom:1px solid #21262d;padding:48px 24px 40px;text-align:center}
18
+ .hero h1{font-size:2.2rem;font-weight:700;color:#f0f6fc;letter-spacing:-0.5px}
19
+ .hero h1 span{color:#3fb950}
20
+ .hero p{color:#8b949e;margin-top:10px;font-size:1.05rem;max-width:560px;margin-inline:auto}
21
+ .cmd{display:inline-block;background:#161b22;border:1px solid #30363d;border-radius:6px;padding:8px 18px;font-family:monospace;font-size:1rem;color:#79c0ff;margin-top:18px}
22
+ .drop-zone{border:2px dashed #30363d;border-radius:12px;padding:32px;margin:28px auto;max-width:500px;text-align:center;cursor:pointer;transition:.2s;color:#8b949e;font-size:.95rem}
23
+ .drop-zone:hover,.drop-zone.drag{border-color:#3fb950;color:#3fb950}
24
+ .drop-zone.loaded{border-color:#3fb950;color:#3fb950}
25
+ #status{text-align:center;color:#8b949e;font-size:.9rem;margin-bottom:8px}
26
+ .main{max-width:900px;margin:0 auto;padding:32px 16px}
27
+
28
+ .stats-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:12px;margin-bottom:24px}
29
+ .stat-box{background:#161b22;border:1px solid #21262d;border-radius:8px;padding:14px 16px;text-align:center}
30
+ .stat-val{font-size:1.7rem;font-weight:700;color:#3fb950}
31
+ .stat-val.orange{color:#ffa657}
32
+ .stat-val.red{color:#f85149}
33
+ .stat-lbl{font-size:.8rem;color:#8b949e;margin-top:3px}
34
+
35
+ .card{background:#161b22;border:1px solid #21262d;border-radius:10px;padding:20px;margin-bottom:24px}
36
+ .card-title{font-size:1rem;font-weight:600;color:#f0f6fc;margin-bottom:6px}
37
+ .card-sub{font-size:.82rem;color:#8b949e;margin-bottom:16px}
38
+
39
+ .bar-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}
40
+ .bar-label{width:100px;font-size:.83rem;font-family:monospace;color:#c9d1d9;flex-shrink:0}
41
+ .bar-track{flex:1;background:#21262d;border-radius:4px;height:26px;position:relative;overflow:hidden}
42
+ .bar-fill{height:100%;border-radius:4px;transition:width .5s ease}
43
+ .bar-pct{position:absolute;right:8px;top:50%;transform:translateY(-50%);font-size:.76rem;font-family:monospace;color:#e6edf3}
44
+ .bar-count{width:80px;font-size:.74rem;color:#484f58;flex-shrink:0;text-align:right;font-family:monospace}
45
+
46
+ .callout{border-radius:10px;padding:20px;margin-bottom:24px;text-align:center;background:linear-gradient(135deg,rgba(63,185,80,.12) 0%,rgba(63,185,80,.03) 100%);border:1px solid rgba(63,185,80,.35)}
47
+ .callout-num{font-size:3.5rem;font-weight:700;line-height:1;color:#3fb950}
48
+ .callout-label{font-size:.95rem;color:#8b949e;margin-top:8px}
49
+
50
+ .insight ul{list-style:none;display:flex;flex-direction:column;gap:7px;margin-top:10px}
51
+ .insight li{font-size:.88rem;color:#8b949e;padding-left:18px;position:relative;line-height:1.5}
52
+ .insight li::before{content:'→';position:absolute;left:0;color:#3fb950}
53
+ .insight li strong{color:#e6edf3}
54
+
55
+ .footer{text-align:center;padding:32px 16px;color:#8b949e;font-size:.85rem;border-top:1px solid #21262d;margin-top:16px}
56
+ .footer a{color:#58a6ff;text-decoration:none}
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <div class="hero">
61
+ <h1>cc-<span>streak</span></h1>
62
+ <p>How long can Claude Code go without an error? Measure consecutive successful tool calls between failures.</p>
63
+ <div class="cmd">npx cc-streak</div>
64
+ </div>
65
+
66
+ <div class="main">
67
+ <div class="drop-zone" id="dropZone">
68
+ Drop your <code>~/.claude/projects/</code> folder here<br>
69
+ <small style="margin-top:6px;display:block">or click to select</small>
70
+ <input type="file" id="fileInput" webkitdirectory multiple style="display:none">
71
+ </div>
72
+ <div id="status"></div>
73
+
74
+ <div id="output" style="display:none">
75
+ <div class="callout" id="mainCallout"></div>
76
+ <div class="stats-row" id="statsRow"></div>
77
+ <div class="card" id="distCard"></div>
78
+ <div class="card" id="breakerCard"></div>
79
+ <div class="card insight" id="insightCard"></div>
80
+ </div>
81
+
82
+ <div id="sample">
83
+ <div class="callout">
84
+ <div class="callout-num">12</div>
85
+ <div class="callout-label"><strong>median successful tool calls</strong> between errors<br><span style="font-size:.85rem">6,221 streaks across 1,994 sessions · longest: 829</span></div>
86
+ </div>
87
+ <div class="stats-row">
88
+ <div class="stat-box"><div class="stat-val">1,994</div><div class="stat-lbl">Sessions</div></div>
89
+ <div class="stat-box"><div class="stat-val">6,221</div><div class="stat-lbl">Streaks</div></div>
90
+ <div class="stat-box"><div class="stat-val">12</div><div class="stat-lbl">Median</div></div>
91
+ <div class="stat-box"><div class="stat-val orange">22.3</div><div class="stat-lbl">Mean</div></div>
92
+ <div class="stat-box"><div class="stat-val">52</div><div class="stat-lbl">P90</div></div>
93
+ <div class="stat-box"><div class="stat-val red">829</div><div class="stat-lbl">Max</div></div>
94
+ </div>
95
+ <div class="card">
96
+ <div class="card-title">Streak length distribution</div>
97
+ <div class="card-sub">How long Claude runs before the next error</div>
98
+ <div class="bar-row"><div class="bar-label">1-2 calls</div><div class="bar-track"><div class="bar-fill" style="width:46%;background:#f85149"></div><div class="bar-pct">16.7%</div></div><div class="bar-count">1,036</div></div>
99
+ <div class="bar-row"><div class="bar-label">3-5</div><div class="bar-track"><div class="bar-fill" style="width:42%;background:#ffa657"></div><div class="bar-pct">15.3%</div></div><div class="bar-count">954</div></div>
100
+ <div class="bar-row"><div class="bar-label">6-20</div><div class="bar-track"><div class="bar-fill" style="width:100%;background:#3fb950"></div><div class="bar-pct">36.2%</div></div><div class="bar-count">2,254</div></div>
101
+ <div class="bar-row"><div class="bar-label">21-50</div><div class="bar-track"><div class="bar-fill" style="width:59%;background:#58a6ff"></div><div class="bar-pct">21.3%</div></div><div class="bar-count">1,328</div></div>
102
+ <div class="bar-row"><div class="bar-label">51-100</div><div class="bar-track"><div class="bar-fill" style="width:20%;background:#bc8cff"></div><div class="bar-pct">7.3%</div></div><div class="bar-count">455</div></div>
103
+ <div class="bar-row"><div class="bar-label">101+</div><div class="bar-track"><div class="bar-fill" style="width:9%;background:#f0883e"></div><div class="bar-pct">3.1%</div></div><div class="bar-count">194</div></div>
104
+ </div>
105
+ <div class="card">
106
+ <div class="card-title">Streak breakers</div>
107
+ <div class="card-sub">Which tool ends the error-free run?</div>
108
+ <div class="bar-row"><div class="bar-label">Bash</div><div class="bar-track"><div class="bar-fill" style="width:100%;background:#f85149"></div><div class="bar-pct">51.5%</div></div><div class="bar-count">3,358</div></div>
109
+ <div class="bar-row"><div class="bar-label">Read</div><div class="bar-track"><div class="bar-fill" style="width:47%;background:#ffa657"></div><div class="bar-pct">24.0%</div></div><div class="bar-count">1,562</div></div>
110
+ <div class="bar-row"><div class="bar-label">Edit</div><div class="bar-track"><div class="bar-fill" style="width:18%;background:#e3b341"></div><div class="bar-pct">9.5%</div></div><div class="bar-count">617</div></div>
111
+ <div class="bar-row"><div class="bar-label">WebFetch</div><div class="bar-track"><div class="bar-fill" style="width:11%;background:#3fb950"></div><div class="bar-pct">5.8%</div></div><div class="bar-count">380</div></div>
112
+ <div class="bar-row"><div class="bar-label">Write</div><div class="bar-track"><div class="bar-fill" style="width:5%;background:#58a6ff"></div><div class="bar-pct">2.8%</div></div><div class="bar-count">180</div></div>
113
+ </div>
114
+ <div class="card insight">
115
+ <div class="card-title">What this means</div>
116
+ <ul>
117
+ <li><strong>Median 12 calls between errors</strong> — Claude hits roughly 1 error every 12 tool calls</li>
118
+ <li><strong>36% of streaks are 6-20 calls</strong> — the sweet spot. Long enough to make progress, short enough to be typical</li>
119
+ <li><strong>Bash breaks 52% of all streaks</strong> — shell commands are the #1 source of streak-ending errors</li>
120
+ <li><strong>3.1% reach 100+ calls</strong> — marathon streaks exist but are rare. The longest: 829 consecutive successes</li>
121
+ <li><strong>Mean (22) &gt; Median (12)</strong> — right-skewed distribution. A few very long streaks pull the average up</li>
122
+ </ul>
123
+ </div>
124
+ </div>
125
+ </div>
126
+
127
+ <div class="footer">
128
+ Part of <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit</a> —
129
+ 106 free tools for Claude Code users ·
130
+ <a href="https://github.com/yurukusa/cc-streak" target="_blank">GitHub</a>
131
+ </div>
132
+
133
+ <script>
134
+ const UX_TOOLS = new Set(['ExitPlanMode','AskUserQuestion','EnterPlanMode']);
135
+ const TIERS = [
136
+ {name:'1-2 calls',min:1,max:2,color:'#f85149'},
137
+ {name:'3-5',min:3,max:5,color:'#ffa657'},
138
+ {name:'6-20',min:6,max:20,color:'#3fb950'},
139
+ {name:'21-50',min:21,max:50,color:'#58a6ff'},
140
+ {name:'51-100',min:51,max:100,color:'#bc8cff'},
141
+ {name:'101+',min:101,max:Infinity,color:'#f0883e'},
142
+ ];
143
+
144
+ function analyzeSession(lines) {
145
+ const toolMap = {};
146
+ let streak = 0;
147
+ const streaks = [];
148
+ const breakers = {};
149
+ let totalCalls = 0;
150
+
151
+ for (const line of lines) {
152
+ if (!line) continue;
153
+ let obj; try { obj = JSON.parse(line); } catch { continue; }
154
+ const content = (obj.message || obj).content;
155
+ if (!Array.isArray(content)) continue;
156
+ for (const b of content) {
157
+ if (b.type === 'tool_use' && b.id && b.name) toolMap[b.id] = b.name;
158
+ else if (b.type === 'tool_result') {
159
+ const name = toolMap[b.tool_use_id || ''] || 'unknown';
160
+ if (UX_TOOLS.has(name)) continue;
161
+ totalCalls++;
162
+ if (b.is_error) {
163
+ if (streak > 0) streaks.push(streak);
164
+ breakers[name] = (breakers[name] || 0) + 1;
165
+ streak = 0;
166
+ } else streak++;
167
+ }
168
+ }
169
+ }
170
+ if (streak > 0) streaks.push(streak);
171
+ return { streaks, breakers, totalCalls };
172
+ }
173
+
174
+ function pct(n,d){return d>0?(n/d*100).toFixed(1):'0.0';}
175
+ function median(a){return a.length?a[Math.floor(a.length/2)]:0;}
176
+ function p(a,v){return a.length?a[Math.floor(a.length*v)]:0;}
177
+
178
+ function renderResults(sessions) {
179
+ const allStreaks=[], allBreakers={};
180
+ let sessionCount=0;
181
+ for (const s of sessions) {
182
+ if (s.totalCalls<3) continue;
183
+ sessionCount++;
184
+ allStreaks.push(...s.streaks);
185
+ for (const [k,v] of Object.entries(s.breakers)) allBreakers[k]=(allBreakers[k]||0)+v;
186
+ }
187
+ allStreaks.sort((a,b)=>a-b);
188
+ const med=median(allStreaks), mean=allStreaks.length?(allStreaks.reduce((a,v)=>a+v,0)/allStreaks.length).toFixed(1):'0';
189
+ const p90=p(allStreaks,.9), p99=p(allStreaks,.99), max=allStreaks.length?allStreaks[allStreaks.length-1]:0;
190
+
191
+ document.getElementById('sample').style.display='none';
192
+ document.getElementById('output').style.display='block';
193
+
194
+ document.getElementById('mainCallout').innerHTML=`
195
+ <div class="callout-num">${med}</div>
196
+ <div class="callout-label"><strong>median successful tool calls</strong> between errors<br>
197
+ <span style="font-size:.85rem">${allStreaks.length.toLocaleString()} streaks across ${sessionCount.toLocaleString()} sessions · longest: ${max.toLocaleString()}</span></div>`;
198
+
199
+ document.getElementById('statsRow').innerHTML=[
200
+ {val:sessionCount.toLocaleString(),lbl:'Sessions'},
201
+ {val:allStreaks.length.toLocaleString(),lbl:'Streaks'},
202
+ {val:med,lbl:'Median'},
203
+ {val:mean,lbl:'Mean',cls:'orange'},
204
+ {val:p90,lbl:'P90'},
205
+ {val:max.toLocaleString(),lbl:'Max',cls:'red'},
206
+ ].map(s=>`<div class="stat-box"><div class="stat-val ${s.cls||''}">${s.val}</div><div class="stat-lbl">${s.lbl}</div></div>`).join('');
207
+
208
+ const tierData=TIERS.map(t=>{const c=allStreaks.filter(v=>v>=t.min&&v<=t.max).length;return{...t,count:c};});
209
+ const maxTier=Math.max(...tierData.map(t=>t.count));
210
+ document.getElementById('distCard').innerHTML=`
211
+ <div class="card-title">Streak length distribution</div>
212
+ <div class="card-sub">How long Claude runs before the next error</div>
213
+ ${tierData.map(t=>{const w=maxTier>0?Math.round(t.count/maxTier*100):0;
214
+ return `<div class="bar-row"><div class="bar-label">${t.name}</div><div class="bar-track"><div class="bar-fill" style="width:${w}%;background:${t.color}"></div><div class="bar-pct">${pct(t.count,allStreaks.length)}%</div></div><div class="bar-count">${t.count.toLocaleString()}</div></div>`;
215
+ }).join('')}`;
216
+
217
+ const totalB=Object.values(allBreakers).reduce((a,v)=>a+v,0);
218
+ const bList=Object.entries(allBreakers).sort((a,b)=>b[1]-a[1]).slice(0,6);
219
+ const maxB=bList.length?bList[0][1]:1;
220
+ const bColors=['#f85149','#ffa657','#e3b341','#3fb950','#58a6ff','#bc8cff'];
221
+ document.getElementById('breakerCard').innerHTML=`
222
+ <div class="card-title">Streak breakers</div>
223
+ <div class="card-sub">Which tool ends the error-free run?</div>
224
+ ${bList.map(([t,c],i)=>{const w=Math.round(c/maxB*100);
225
+ return `<div class="bar-row"><div class="bar-label">${t}</div><div class="bar-track"><div class="bar-fill" style="width:${w}%;background:${bColors[i]||'#8b949e'}"></div><div class="bar-pct">${pct(c,totalB)}%</div></div><div class="bar-count">${c.toLocaleString()}</div></div>`;
226
+ }).join('')}`;
227
+
228
+ document.getElementById('insightCard').innerHTML=`
229
+ <div class="card-title">What this means</div>
230
+ <ul>
231
+ <li><strong>Median ${med} calls between errors</strong> — Claude hits roughly 1 error every ${med} tool calls</li>
232
+ <li><strong>Mean (${mean}) > Median (${med})</strong> — right-skewed. A few long streaks pull the average up</li>
233
+ <li><strong>${bList[0]?bList[0][0]:'Bash'} breaks ${bList[0]?pct(bList[0][1],totalB):'0'}% of streaks</strong> — the #1 source of streak-ending errors</li>
234
+ <li><strong>${pct(tierData[5].count,allStreaks.length)}% reach 100+ calls</strong> — marathon streaks exist but are rare</li>
235
+ <li><strong>Longest streak: ${max.toLocaleString()}</strong> — consecutive successful tool calls without a single error</li>
236
+ </ul>`;
237
+ }
238
+
239
+ const dz=document.getElementById('dropZone');
240
+ const fi=document.getElementById('fileInput');
241
+ dz.onclick=()=>fi.click();
242
+ dz.ondragover=e=>{e.preventDefault();dz.classList.add('drag');};
243
+ dz.ondragleave=()=>dz.classList.remove('drag');
244
+ dz.ondrop=e=>{e.preventDefault();dz.classList.remove('drag');handleFiles(e.dataTransfer.files);};
245
+ fi.onchange=e=>handleFiles(e.target.files);
246
+
247
+ async function handleFiles(files) {
248
+ const jsonlFiles=[...files].filter(f=>f.name.endsWith('.jsonl'));
249
+ if(!jsonlFiles.length){document.getElementById('status').textContent='No .jsonl files found';return;}
250
+ dz.classList.add('loaded');
251
+ document.getElementById('status').textContent=`Processing ${jsonlFiles.length} sessions...`;
252
+ const sessions=[];
253
+ const BATCH=50;
254
+ for(let i=0;i<jsonlFiles.length;i+=BATCH){
255
+ const batch=jsonlFiles.slice(i,i+BATCH);
256
+ const results=await Promise.all(batch.map(f=>f.text().then(t=>analyzeSession(t.split('\n')))));
257
+ sessions.push(...results);
258
+ document.getElementById('status').textContent=`Processing ${Math.min(i+BATCH,jsonlFiles.length)}/${jsonlFiles.length} sessions...`;
259
+ }
260
+ renderResults(sessions);
261
+ document.getElementById('status').textContent=`${sessions.filter(s=>s.totalCalls>=3).length} sessions analyzed`;
262
+ }
263
+ </script>
264
+ </body>
265
+ </html>
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@yurukusa/cc-streak",
3
+ "version": "1.0.1",
4
+ "description": "How long can Claude Code go without an error? Median 12 successful tool calls between errors. Longest streak: 829. Bash breaks 52% of streaks.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cc-streak": "./cli.mjs"
8
+ },
9
+ "scripts": {
10
+ "start": "node cli.mjs"
11
+ },
12
+ "keywords": [
13
+ "claude", "claude-code", "anthropic", "cli", "productivity",
14
+ "ai", "developer-tools", "session-analysis", "streaks", "reliability"
15
+ ],
16
+ "author": "yurukusa",
17
+ "license": "MIT",
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/yurukusa/cc-streak"
24
+ },
25
+ "homepage": "https://yurukusa.github.io/cc-streak/"
26
+ }