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.
Files changed (3) hide show
  1. package/README.md +48 -0
  2. package/cli.mjs +293 -0
  3. 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
+ }