cc-fail 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 +48 -0
- package/cli.mjs +293 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# cc-fail
|
|
2
|
+
|
|
3
|
+
See which tools fail most in your Claude Code sessions.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx cc-fail
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What it shows
|
|
10
|
+
|
|
11
|
+
- **Overall failure rate** — what % of tool calls return errors
|
|
12
|
+
- **Per-tool failure rates** — Bash, WebFetch, Read, Edit, Write...
|
|
13
|
+
- **Error categories** — bash-exit, token-limit, permission, timeout, network
|
|
14
|
+
- **Per-project failure rates** — which project has the most errors
|
|
15
|
+
- **Worst session** — the single session with the highest failure rate
|
|
16
|
+
|
|
17
|
+
## Example output
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
cc-fail — Tool Failure Analysis
|
|
21
|
+
════════════════════════════════════════
|
|
22
|
+
|
|
23
|
+
▸ Overview
|
|
24
|
+
Total tool calls: 144,851
|
|
25
|
+
Failed: 7,060 (4.9%)
|
|
26
|
+
Succeeded: 137,791 (95.1%)
|
|
27
|
+
|
|
28
|
+
▸ Failure rate by tool
|
|
29
|
+
WebFetch 24.7% 376/1521
|
|
30
|
+
Bash 6.2% 3377/54178
|
|
31
|
+
Read 2.1% 84/4001
|
|
32
|
+
|
|
33
|
+
▸ Error types
|
|
34
|
+
bash-exit ████████████████ 2499
|
|
35
|
+
parallel-error ██████░░░░░░░░░░ 1005
|
|
36
|
+
token-limit ██░░░░░░░░░░░░░░ 429
|
|
37
|
+
permission █░░░░░░░░░░░░░░░ 198
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## JSON output
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx cc-fail --json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Part of cc-toolkit
|
|
47
|
+
|
|
48
|
+
60 free tools for Claude Code users. → [yurukusa.github.io/cc-toolkit](https://yurukusa.github.io/cc-toolkit/)
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// cc-fail — See which tools fail most in your Claude Code sessions.
|
|
4
|
+
// Zero dependencies. Reads ~/.claude/projects/ session transcripts.
|
|
5
|
+
|
|
6
|
+
import { readdir, stat, open } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { createInterface } from 'node:readline';
|
|
10
|
+
import { createReadStream } from 'node:fs';
|
|
11
|
+
|
|
12
|
+
const CONCURRENCY = 8;
|
|
13
|
+
|
|
14
|
+
const C = {
|
|
15
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
16
|
+
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
17
|
+
cyan: '\x1b[36m',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function bar(pct, width = 20) {
|
|
21
|
+
const filled = Math.round(pct * width);
|
|
22
|
+
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function projectName(dir) {
|
|
26
|
+
const stripped = dir.replace(/^-home-[^-]+/, '').replace(/^-/, '');
|
|
27
|
+
return stripped || '~/ (home)';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Classify error message into category
|
|
31
|
+
function classifyError(msg) {
|
|
32
|
+
if (!msg) return 'unknown';
|
|
33
|
+
const m = String(msg).toLowerCase();
|
|
34
|
+
if (m.includes('exit code') || m.includes('exit status')) return 'bash-exit';
|
|
35
|
+
if (m.includes('file not found') || m.includes('no such file')) return 'file-not-found';
|
|
36
|
+
if (m.includes('permission denied')) return 'permission-denied';
|
|
37
|
+
if (m.includes('timeout') || m.includes('timed out')) return 'timeout';
|
|
38
|
+
if (m.includes('token') && m.includes('exceed')) return 'token-limit';
|
|
39
|
+
if (m.includes('sibling tool') || m.includes('parallel')) return 'parallel-error';
|
|
40
|
+
if (m.includes('parse') || m.includes('invalid json') || m.includes('syntax')) return 'parse-error';
|
|
41
|
+
if (m.includes('rate limit') || m.includes('429')) return 'rate-limit';
|
|
42
|
+
if (m.includes('network') || m.includes('connection') || m.includes('econnrefused')) return 'network';
|
|
43
|
+
if (m.includes('not allowed') || m.includes('denied') || m.includes('blocked')) return 'permission';
|
|
44
|
+
return 'other';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function analyzeFile(filePath) {
|
|
48
|
+
const result = {
|
|
49
|
+
totalCalls: 0,
|
|
50
|
+
errors: 0,
|
|
51
|
+
byTool: {}, // toolName -> { calls, errors }
|
|
52
|
+
byCategory: {}, // errorCategory -> count
|
|
53
|
+
topErrors: {}, // first 80 chars of error -> count
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const rl = createInterface({
|
|
57
|
+
input: createReadStream(filePath),
|
|
58
|
+
crlfDelay: Infinity,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Map tool_use_id -> tool_name (populated from assistant tool_use events)
|
|
62
|
+
const toolNames = {};
|
|
63
|
+
|
|
64
|
+
for await (const line of rl) {
|
|
65
|
+
if (!line) continue;
|
|
66
|
+
|
|
67
|
+
// Only parse lines that mention tool_use or tool_result
|
|
68
|
+
const hasToolUse = line.includes('"tool_use"');
|
|
69
|
+
const hasToolResult = line.includes('"tool_result"');
|
|
70
|
+
if (!hasToolUse && !hasToolResult) continue;
|
|
71
|
+
|
|
72
|
+
let data;
|
|
73
|
+
try { data = JSON.parse(line); } catch { continue; }
|
|
74
|
+
|
|
75
|
+
const msg = data.message || data;
|
|
76
|
+
if (!msg || !Array.isArray(msg.content)) continue;
|
|
77
|
+
|
|
78
|
+
for (const item of msg.content) {
|
|
79
|
+
if (!item || typeof item !== 'object') continue;
|
|
80
|
+
|
|
81
|
+
// assistant: tool_use event — record tool name by id
|
|
82
|
+
if (item.type === 'tool_use' && item.id && item.name) {
|
|
83
|
+
toolNames[item.id] = item.name;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// user: tool_result event — count success/failure
|
|
87
|
+
if (item.type === 'tool_result' && item.tool_use_id) {
|
|
88
|
+
const toolName = toolNames[item.tool_use_id] || 'unknown';
|
|
89
|
+
const isError = item.is_error === true;
|
|
90
|
+
|
|
91
|
+
result.totalCalls++;
|
|
92
|
+
if (!result.byTool[toolName]) result.byTool[toolName] = { calls: 0, errors: 0 };
|
|
93
|
+
result.byTool[toolName].calls++;
|
|
94
|
+
|
|
95
|
+
if (isError) {
|
|
96
|
+
result.errors++;
|
|
97
|
+
result.byTool[toolName].errors++;
|
|
98
|
+
|
|
99
|
+
// Error category
|
|
100
|
+
const errContent = Array.isArray(item.content)
|
|
101
|
+
? item.content.map(c => (typeof c === 'string' ? c : c.text || '')).join(' ')
|
|
102
|
+
: String(item.content || '');
|
|
103
|
+
const cat = classifyError(errContent);
|
|
104
|
+
result.byCategory[cat] = (result.byCategory[cat] || 0) + 1;
|
|
105
|
+
|
|
106
|
+
// Top error snippets
|
|
107
|
+
const snippet = errContent.slice(0, 80).replace(/\n/g, ' ').trim();
|
|
108
|
+
if (snippet) result.topErrors[snippet] = (result.topErrors[snippet] || 0) + 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function scan() {
|
|
118
|
+
const projectsDir = join(homedir(), '.claude', 'projects');
|
|
119
|
+
let projectDirs;
|
|
120
|
+
try { projectDirs = await readdir(projectsDir); } catch { return []; }
|
|
121
|
+
|
|
122
|
+
// Collect all session files
|
|
123
|
+
const tasks = [];
|
|
124
|
+
for (const pd of projectDirs) {
|
|
125
|
+
const pp = join(projectsDir, pd);
|
|
126
|
+
const ps = await stat(pp).catch(() => null);
|
|
127
|
+
if (!ps?.isDirectory()) continue;
|
|
128
|
+
const files = await readdir(pp).catch(() => []);
|
|
129
|
+
for (const f of files) {
|
|
130
|
+
if (f.endsWith('.jsonl')) {
|
|
131
|
+
tasks.push({ path: join(pp, f), project: projectName(pd) });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Subagent files
|
|
135
|
+
for (const f of files) {
|
|
136
|
+
const sp = join(pp, f, 'subagents');
|
|
137
|
+
const ss = await stat(sp).catch(() => null);
|
|
138
|
+
if (!ss?.isDirectory()) continue;
|
|
139
|
+
const sfs = await readdir(sp).catch(() => []);
|
|
140
|
+
for (const sf of sfs) {
|
|
141
|
+
if (sf.endsWith('.jsonl')) {
|
|
142
|
+
tasks.push({ path: join(sp, sf), project: projectName(pd) });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Aggregate results by project
|
|
149
|
+
const byProject = {}; // project -> { totalCalls, errors }
|
|
150
|
+
let globalCalls = 0, globalErrors = 0;
|
|
151
|
+
const globalByTool = {};
|
|
152
|
+
const globalByCategory = {};
|
|
153
|
+
const globalTopErrors = {};
|
|
154
|
+
let worstSession = { path: '', rate: 0, errors: 0, calls: 0 };
|
|
155
|
+
|
|
156
|
+
// Process in batches
|
|
157
|
+
for (let i = 0; i < tasks.length; i += CONCURRENCY) {
|
|
158
|
+
const batch = tasks.slice(i, i + CONCURRENCY);
|
|
159
|
+
const results = await Promise.all(batch.map(async t => {
|
|
160
|
+
const st = await stat(t.path).catch(() => null);
|
|
161
|
+
if (!st || st.size < 100) return null;
|
|
162
|
+
const r = await analyzeFile(t.path).catch(() => null);
|
|
163
|
+
return r ? { ...r, project: t.project, path: t.path } : null;
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
for (const r of results) {
|
|
167
|
+
if (!r || r.totalCalls === 0) continue;
|
|
168
|
+
globalCalls += r.totalCalls;
|
|
169
|
+
globalErrors += r.errors;
|
|
170
|
+
|
|
171
|
+
if (!byProject[r.project]) byProject[r.project] = { calls: 0, errors: 0 };
|
|
172
|
+
byProject[r.project].calls += r.totalCalls;
|
|
173
|
+
byProject[r.project].errors += r.errors;
|
|
174
|
+
|
|
175
|
+
for (const [t, v] of Object.entries(r.byTool)) {
|
|
176
|
+
if (!globalByTool[t]) globalByTool[t] = { calls: 0, errors: 0 };
|
|
177
|
+
globalByTool[t].calls += v.calls;
|
|
178
|
+
globalByTool[t].errors += v.errors;
|
|
179
|
+
}
|
|
180
|
+
for (const [c, n] of Object.entries(r.byCategory)) {
|
|
181
|
+
globalByCategory[c] = (globalByCategory[c] || 0) + n;
|
|
182
|
+
}
|
|
183
|
+
for (const [e, n] of Object.entries(r.topErrors)) {
|
|
184
|
+
globalTopErrors[e] = (globalTopErrors[e] || 0) + n;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const rate = r.totalCalls > 0 ? r.errors / r.totalCalls : 0;
|
|
188
|
+
if (r.errors > 5 && rate > worstSession.rate) {
|
|
189
|
+
worstSession = { path: r.path, rate, errors: r.errors, calls: r.totalCalls };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { globalCalls, globalErrors, byProject, globalByTool, globalByCategory, globalTopErrors, worstSession };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const jsonMode = process.argv.includes('--json');
|
|
198
|
+
if (!jsonMode) process.stdout.write(` ${C.dim}Analyzing tool calls...${C.reset}\r`);
|
|
199
|
+
|
|
200
|
+
const data = await scan();
|
|
201
|
+
const { globalCalls, globalErrors, byProject, globalByTool, globalByCategory, globalTopErrors, worstSession } = data;
|
|
202
|
+
|
|
203
|
+
const globalRate = globalCalls > 0 ? globalErrors / globalCalls : 0;
|
|
204
|
+
|
|
205
|
+
if (jsonMode) {
|
|
206
|
+
const byToolSorted = Object.entries(globalByTool)
|
|
207
|
+
.filter(([, v]) => v.calls >= 5)
|
|
208
|
+
.sort((a, b) => b[1].errors - a[1].errors)
|
|
209
|
+
.map(([name, v]) => ({ name, calls: v.calls, errors: v.errors, rate: v.calls > 0 ? +(v.errors / v.calls).toFixed(3) : 0 }));
|
|
210
|
+
const byProjectSorted = Object.entries(byProject)
|
|
211
|
+
.filter(([, v]) => v.calls >= 5)
|
|
212
|
+
.sort((a, b) => (b[1].errors / b[1].calls) - (a[1].errors / a[1].calls))
|
|
213
|
+
.map(([name, v]) => ({ name, calls: v.calls, errors: v.errors, rate: +(v.errors / v.calls).toFixed(3) }));
|
|
214
|
+
console.log(JSON.stringify({
|
|
215
|
+
version: '1.0.0',
|
|
216
|
+
totalCalls: globalCalls,
|
|
217
|
+
totalErrors: globalErrors,
|
|
218
|
+
errorRate: +globalRate.toFixed(3),
|
|
219
|
+
byTool: byToolSorted,
|
|
220
|
+
byProject: byProjectSorted,
|
|
221
|
+
byCategory: globalByCategory,
|
|
222
|
+
}, null, 2));
|
|
223
|
+
process.exit(0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Display ──────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
const pct = (n, d) => d > 0 ? (n / d * 100).toFixed(1) + '%' : '0%';
|
|
229
|
+
|
|
230
|
+
console.log(`\n ${C.bold}${C.cyan}cc-fail — Tool Failure Analysis${C.reset}`);
|
|
231
|
+
console.log(` ${'═'.repeat(40)}`);
|
|
232
|
+
|
|
233
|
+
console.log(`\n ${C.bold}▸ Overview${C.reset}`);
|
|
234
|
+
console.log(` Total tool calls: ${C.bold}${globalCalls.toLocaleString()}${C.reset}`);
|
|
235
|
+
console.log(` Failed: ${C.bold}${C.red}${globalErrors.toLocaleString()}${C.reset} ${C.dim}(${pct(globalErrors, globalCalls)})${C.reset}`);
|
|
236
|
+
console.log(` Succeeded: ${C.bold}${C.green}${(globalCalls - globalErrors).toLocaleString()}${C.reset} ${C.dim}(${pct(globalCalls - globalErrors, globalCalls)})${C.reset}`);
|
|
237
|
+
|
|
238
|
+
// Per-tool breakdown
|
|
239
|
+
const tools = Object.entries(globalByTool)
|
|
240
|
+
.filter(([, v]) => v.calls >= 5)
|
|
241
|
+
.sort((a, b) => (b[1].errors / b[1].calls) - (a[1].errors / a[1].calls))
|
|
242
|
+
.slice(0, 10);
|
|
243
|
+
|
|
244
|
+
if (tools.length > 0) {
|
|
245
|
+
console.log(`\n ${C.bold}▸ Failure rate by tool${C.reset}`);
|
|
246
|
+
const maxCalls = Math.max(...tools.map(([, v]) => v.calls));
|
|
247
|
+
for (const [name, v] of tools) {
|
|
248
|
+
const rate = v.calls > 0 ? v.errors / v.calls : 0;
|
|
249
|
+
const color = rate > 0.2 ? C.red : rate > 0.1 ? C.yellow : C.green;
|
|
250
|
+
const nameP = name.padEnd(20);
|
|
251
|
+
console.log(` ${C.dim}${nameP}${C.reset} ${color}${pct(v.errors, v.calls).padStart(6)}${C.reset} ${C.dim}${v.errors}/${v.calls}${C.reset}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Error categories
|
|
256
|
+
const cats = Object.entries(globalByCategory).sort((a, b) => b[1] - a[1]).slice(0, 6);
|
|
257
|
+
if (cats.length > 0) {
|
|
258
|
+
console.log(`\n ${C.bold}▸ Error types${C.reset}`);
|
|
259
|
+
const maxCat = cats[0][1];
|
|
260
|
+
for (const [cat, n] of cats) {
|
|
261
|
+
const b = bar(n / maxCat, 16);
|
|
262
|
+
console.log(` ${cat.padEnd(20)} ${C.red}${b}${C.reset} ${n}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Per-project
|
|
267
|
+
const projects = Object.entries(byProject)
|
|
268
|
+
.filter(([, v]) => v.calls >= 10)
|
|
269
|
+
.sort((a, b) => (b[1].errors / b[1].calls) - (a[1].errors / a[1].calls))
|
|
270
|
+
.slice(0, 6);
|
|
271
|
+
|
|
272
|
+
if (projects.length > 0) {
|
|
273
|
+
console.log(`\n ${C.bold}▸ Failure rate by project${C.reset}`);
|
|
274
|
+
for (const [name, v] of projects) {
|
|
275
|
+
const rate = v.calls > 0 ? v.errors / v.calls : 0;
|
|
276
|
+
const color = rate > 0.15 ? C.red : rate > 0.08 ? C.yellow : C.dim;
|
|
277
|
+
console.log(` ${color}${name.slice(0, 28).padEnd(28)}${C.reset} ${pct(v.errors, v.calls).padStart(6)} ${C.dim}${v.errors}/${v.calls}${C.reset}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Worst session
|
|
282
|
+
if (worstSession.calls > 0) {
|
|
283
|
+
const sessionName = worstSession.path.split('/').slice(-3).join('/');
|
|
284
|
+
console.log(`\n ${C.bold}▸ Worst session${C.reset}`);
|
|
285
|
+
console.log(` ${C.red}${pct(worstSession.errors, worstSession.calls)}${C.reset} failure rate`);
|
|
286
|
+
console.log(` ${C.dim}${sessionName.slice(0, 60)}${C.reset}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log();
|
|
290
|
+
console.log(` ${C.dim}─── Share ───${C.reset}`);
|
|
291
|
+
console.log(` ${C.dim}My tool failure rate: ${pct(globalErrors, globalCalls)} (${globalErrors.toLocaleString()} failures / ${globalCalls.toLocaleString()} calls)`);
|
|
292
|
+
console.log(` #ClaudeCode${C.reset}`);
|
|
293
|
+
console.log();
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-fail",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "See which tools fail most in your Claude Code sessions. Bash exit rates, WebFetch errors, permission denials and more.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"cc-fail": "cli.mjs"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"claude-code",
|
|
11
|
+
"claude",
|
|
12
|
+
"ai",
|
|
13
|
+
"debugging",
|
|
14
|
+
"error-analysis",
|
|
15
|
+
"tool-failure",
|
|
16
|
+
"bash",
|
|
17
|
+
"productivity"
|
|
18
|
+
],
|
|
19
|
+
"author": "yurukusa",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/yurukusa/cc-fail.git"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"cli.mjs",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
]
|
|
33
|
+
}
|