cc-file-churn 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 (4) hide show
  1. package/README.md +60 -0
  2. package/cli.mjs +182 -0
  3. package/index.html +237 -0
  4. package/package.json +27 -0
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # cc-file-churn
2
+
3
+ Which files does Claude Code touch the most? Find the hotspots.
4
+
5
+ ```
6
+ npx cc-file-churn
7
+ ```
8
+
9
+ ## What it does
10
+
11
+ Scans all your `~/.claude/projects/` session transcripts and counts every `Edit`, `Write`, `Read`, `Grep`, and `Glob` tool call per file. Shows a ranked list with color-coded progress bars.
12
+
13
+ ## Example output
14
+
15
+ ```
16
+ cc-file-churn — Most-touched files in your Claude Code sessions
17
+
18
+ Scanned 755 session files · 2,219 unique files · 12,476 tool calls
19
+
20
+ ① ████████████████████ 933 total
21
+ ~/.claude/plans/staged-hatching-backus.md
22
+ 669 writes 239 reads 25 searches
23
+
24
+ ② ██████████████░░░░░░ 671 total
25
+ ~/projects/dungeon-diablo/dungeon_game.py
26
+ 70 writes 313 reads 288 searches
27
+
28
+ ③ ███████████░░░░░░░░░ 493 total
29
+ ~/schedule-app/index.html
30
+ 215 writes 218 reads 60 searches
31
+ ```
32
+
33
+ ## Why this is interesting
34
+
35
+ The #1 most-touched file in my 60 days: a planning file with 669 writes. #2: the game I spent 3 months building. #3: a schedule app I built in one session.
36
+
37
+ Your highest-churn files reveal:
38
+ - **Where the AI spends most effort** (vs where you think it does)
39
+ - **Which files are becoming complexity magnets**
40
+ - **Which projects actually got attention** (vs the ones that felt important)
41
+
42
+ ## Options
43
+
44
+ ```bash
45
+ npx cc-file-churn # Top 20 files by total activity
46
+ npx cc-file-churn 10 # Top 10
47
+ npx cc-file-churn --writes # Rank by write frequency (most-modified)
48
+ npx cc-file-churn --reads # Rank by read frequency (most-referenced)
49
+ npx cc-file-churn --days=7 # Only last 7 days
50
+ npx cc-file-churn --project=my-app # Filter to a specific project
51
+ npx cc-file-churn --json # JSON output for scripting
52
+ ```
53
+
54
+ ## Part of cc-toolkit
55
+
56
+ One of 50 free tools for Claude Code users → [cc-toolkit](https://yurukusa.github.io/cc-toolkit/)
57
+
58
+ ## License
59
+
60
+ MIT
package/cli.mjs ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+
3
+ // cc-file-churn — Which files does Claude Code touch the most?
4
+ // Scans ~/.claude/projects/ session transcripts, tallies Edit/Write/Read/Grep tool calls per file.
5
+ // Zero dependencies. Works across all Claude Code projects.
6
+
7
+ import { readdir, stat, open } from 'node:fs/promises';
8
+ import { join, basename } from 'node:path';
9
+ import { homedir } from 'node:os';
10
+
11
+ // ── Config ──────────────────────────────────────────────────────
12
+
13
+ const READ_TOOLS = new Set(['Read']);
14
+ const WRITE_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit']);
15
+ const SEARCH_TOOLS = new Set(['Grep', 'Glob']);
16
+ const ALL_TRACKED = new Set([...READ_TOOLS, ...WRITE_TOOLS, ...SEARCH_TOOLS]);
17
+
18
+ const TAIL_BYTES = 4_194_304; // 4MB per file (faster than full scan)
19
+
20
+ // ── Colors ──────────────────────────────────────────────────────
21
+
22
+ const C = {
23
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
24
+ red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
25
+ blue: '\x1b[34m', cyan: '\x1b[36m', white: '\x1b[37m',
26
+ orange: '\x1b[38;5;208m',
27
+ };
28
+
29
+ function bar(n, max, width = 20) {
30
+ const filled = max > 0 ? Math.round((n / max) * width) : 0;
31
+ return C.cyan + '█'.repeat(filled) + C.dim + '░'.repeat(width - filled) + C.reset;
32
+ }
33
+
34
+ // ── File reading ─────────────────────────────────────────────────
35
+
36
+ async function readChunk(filePath, maxBytes) {
37
+ const fh = await open(filePath, 'r');
38
+ try {
39
+ const { size } = await fh.stat();
40
+ const start = Math.max(0, size - maxBytes);
41
+ const buf = Buffer.alloc(size - start);
42
+ const { bytesRead } = await fh.read(buf, 0, buf.length, start);
43
+ return buf.toString('utf8', 0, bytesRead);
44
+ } finally {
45
+ await fh.close();
46
+ }
47
+ }
48
+
49
+ // ── Parse tool calls from jsonl chunk ───────────────────────────
50
+
51
+ function parseChunk(chunk, fileMap, toolMap) {
52
+ const lines = chunk.split('\n');
53
+ for (const line of lines) {
54
+ if (!line.trim()) continue;
55
+ let d;
56
+ try { d = JSON.parse(line); } catch { continue; }
57
+ if (d.type !== 'assistant') continue;
58
+ for (const content of d?.message?.content ?? []) {
59
+ if (content?.type !== 'tool_use') continue;
60
+ const toolName = content.name;
61
+ if (!ALL_TRACKED.has(toolName)) continue;
62
+ const inp = content.input ?? {};
63
+ const path = inp.file_path ?? inp.path ?? '';
64
+ if (!path || typeof path !== 'string') continue;
65
+ // Normalize: remove absolute prefix up to home
66
+ const norm = path.replace(/^\/home\/[^/]+/, '~');
67
+ if (!fileMap.has(norm)) fileMap.set(norm, { r: 0, w: 0, s: 0 });
68
+ const e = fileMap.get(norm);
69
+ if (READ_TOOLS.has(toolName)) e.r++;
70
+ else if (WRITE_TOOLS.has(toolName)) e.w++;
71
+ else if (SEARCH_TOOLS.has(toolName)) e.s++;
72
+ toolMap.set(toolName, (toolMap.get(toolName) ?? 0) + 1);
73
+ }
74
+ }
75
+ }
76
+
77
+ // ── Find all jsonl files ─────────────────────────────────────────
78
+
79
+ async function findJsonlFiles(projectFilter) {
80
+ const base = join(homedir(), '.claude', 'projects');
81
+ let dirs;
82
+ try { dirs = await readdir(base); } catch { return []; }
83
+ const files = [];
84
+ for (const dir of dirs) {
85
+ if (projectFilter && !dir.includes(projectFilter)) continue;
86
+ const dirPath = join(base, dir);
87
+ let entries;
88
+ try { entries = await readdir(dirPath); } catch { continue; }
89
+ for (const e of entries) {
90
+ if (!e.endsWith('.jsonl')) continue;
91
+ const fp = join(dirPath, e);
92
+ try {
93
+ const s = await stat(fp);
94
+ files.push({ fp, size: s.size, mtime: s.mtimeMs, project: dir });
95
+ } catch {}
96
+ }
97
+ }
98
+ return files;
99
+ }
100
+
101
+ // ── Main ─────────────────────────────────────────────────────────
102
+
103
+ async function main() {
104
+ const args = process.argv.slice(2);
105
+ const topN = parseInt(args.find(a => a.match(/^\d+$/)) ?? '20');
106
+ const showWrites = args.includes('--writes') || args.includes('-w');
107
+ const showReads = args.includes('--reads') || args.includes('-r');
108
+ const showAll = args.includes('--all') || args.includes('-a');
109
+ const jsonOut = args.includes('--json');
110
+ const projectFilter = args.find(a => a.startsWith('--project='))?.split('=')[1];
111
+ const daysArg = args.find(a => a.startsWith('--days='))?.split('=')[1];
112
+ const sinceMs = daysArg ? Date.now() - parseInt(daysArg) * 86400_000 : 0;
113
+
114
+ const files = await findJsonlFiles(projectFilter);
115
+ if (!files.length) {
116
+ console.error('No Claude Code session files found.');
117
+ process.exit(1);
118
+ }
119
+
120
+ const filtered = sinceMs ? files.filter(f => f.mtime >= sinceMs) : files;
121
+
122
+ const fileMap = new Map();
123
+ const toolMap = new Map();
124
+ let processed = 0;
125
+
126
+ for (const f of filtered) {
127
+ try {
128
+ const chunk = await readChunk(f.fp, TAIL_BYTES);
129
+ parseChunk(chunk, fileMap, toolMap);
130
+ processed++;
131
+ } catch {}
132
+ }
133
+
134
+ // Build sorted list
135
+ const entries = [...fileMap.entries()].map(([path, c]) => ({
136
+ path, reads: c.r, writes: c.w, searches: c.s, total: c.r + c.w + c.s,
137
+ }));
138
+
139
+ // Filter mode
140
+ let display = showWrites ? entries.filter(e => e.writes > 0) :
141
+ showReads ? entries.filter(e => e.reads > 0) :
142
+ entries;
143
+ display.sort((a, b) => (showWrites ? b.writes - a.writes :
144
+ showReads ? b.reads - a.reads :
145
+ b.total - a.total));
146
+ display = display.slice(0, showAll ? 50 : topN);
147
+
148
+ if (jsonOut) {
149
+ console.log(JSON.stringify({ files_analyzed: processed, top: display }, null, 2));
150
+ return;
151
+ }
152
+
153
+ const maxVal = display[0]?.total ?? 1;
154
+ const totalOps = [...fileMap.values()].reduce((s, c) => s + c.r + c.w + c.s, 0);
155
+
156
+ console.log(`\n${C.bold}cc-file-churn${C.reset} — Most-touched files in your Claude Code sessions\n`);
157
+ console.log(`${C.dim}Scanned ${processed} session files · ${fileMap.size} unique files · ${totalOps} tool calls${daysArg ? ` (last ${daysArg}d)` : ''}${C.reset}\n`);
158
+
159
+ const rank = [C.yellow + '①', C.white + '②', C.dim + '③'];
160
+ display.forEach((e, i) => {
161
+ const medal = rank[i] ?? C.dim + `${String(i + 1).padStart(2)}`;
162
+ const shortPath = e.path.length > 60 ? '…' + e.path.slice(-59) : e.path;
163
+ const barVal = showWrites ? e.writes : showReads ? e.reads : e.total;
164
+ console.log(`${medal}${C.reset} ${bar(barVal, maxVal)} ${C.bold}${barVal}${C.reset} ${C.dim}total${C.reset}`);
165
+ console.log(` ${C.cyan}${shortPath}${C.reset}`);
166
+ if (e.writes > 0 || e.reads > 0 || e.searches > 0) {
167
+ const parts = [];
168
+ if (e.writes > 0) parts.push(`${C.orange}${e.writes} writes${C.reset}`);
169
+ if (e.reads > 0) parts.push(`${C.green}${e.reads} reads${C.reset}`);
170
+ if (e.searches > 0) parts.push(`${C.dim}${e.searches} searches${C.reset}`);
171
+ console.log(` ${parts.join(' ')}`);
172
+ }
173
+ console.log();
174
+ });
175
+
176
+ // Tool breakdown
177
+ const topTools = [...toolMap.entries()].sort((a, b) => b[1] - a[1]);
178
+ console.log(`${C.dim}Tool breakdown: ${topTools.map(([t, n]) => `${t}:${n}`).join(' · ')}${C.reset}\n`);
179
+ console.log(`${C.dim}Options: --writes (-w) write-heavy files · --reads (-r) read-heavy files · --days=7 last N days · --project=name filter project · --json JSON output${C.reset}\n`);
180
+ }
181
+
182
+ main().catch(e => { console.error(e.message); process.exit(1); });
package/index.html ADDED
@@ -0,0 +1,237 @@
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.0">
6
+ <title>cc-file-churn — Which files does Claude Code touch the most?</title>
7
+ <meta name="description" content="Find the hotspot files in your Claude Code sessions. Drag in your ~/.claude folder to rank files by Edit/Write/Read frequency.">
8
+ <meta property="og:title" content="cc-file-churn — File activity ranking for Claude Code">
9
+ <meta property="og:description" content="Which files does your AI keep coming back to? Ranks by writes, reads, and searches across all sessions.">
10
+ <meta property="og:url" content="https://yurukusa.github.io/cc-file-churn/">
11
+ <meta name="twitter:card" content="summary">
12
+ <meta name="twitter:site" content="@yurukusa_dev">
13
+ <style>
14
+ * { margin: 0; padding: 0; box-sizing: border-box; }
15
+ :root {
16
+ --bg: #0d1117; --surface: #161b22; --border: #30363d;
17
+ --text: #e6edf3; --muted: #8b949e;
18
+ --green: #56d364; --yellow: #f7c948; --orange: #ffa657;
19
+ --cyan: #58d8f0; --blue: #58a6ff; --red: #ff7b72;
20
+ }
21
+ body {
22
+ background: var(--bg); color: var(--text);
23
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Consolas, monospace;
24
+ min-height: 100vh; display: flex; flex-direction: column;
25
+ align-items: center; padding: 2rem 1rem;
26
+ }
27
+ .header { text-align: center; margin-bottom: 2rem; }
28
+ .header h1 { font-size: 1.8rem; font-weight: 700; color: var(--orange); margin-bottom: 0.5rem; }
29
+ .header p { color: var(--muted); font-size: 0.9rem; }
30
+ .drop-zone {
31
+ width: 100%; max-width: 600px; border: 2px dashed var(--border);
32
+ border-radius: 12px; padding: 3rem 2rem; text-align: center;
33
+ cursor: pointer; transition: border-color 0.2s, background 0.2s;
34
+ margin-bottom: 2rem; background: var(--surface);
35
+ }
36
+ .drop-zone:hover, .drop-zone.drag-over { border-color: var(--orange); background: #1a1a0f; }
37
+ .drop-zone h2 { font-size: 1.1rem; margin-bottom: 0.75rem; }
38
+ .drop-zone p { color: var(--muted); font-size: 0.85rem; line-height: 1.5; }
39
+ .drop-zone .icon { font-size: 2.5rem; margin-bottom: 1rem; }
40
+ .drop-zone input[type="file"] { display: none; }
41
+ .controls {
42
+ width: 100%; max-width: 700px; display: flex; gap: 0.5rem;
43
+ flex-wrap: wrap; margin-bottom: 1.5rem;
44
+ }
45
+ .filter-btn {
46
+ background: var(--surface); border: 1px solid var(--border);
47
+ border-radius: 6px; padding: 0.4rem 0.85rem; font-size: 0.8rem;
48
+ color: var(--muted); cursor: pointer; font-family: inherit;
49
+ transition: border-color 0.15s, color 0.15s;
50
+ }
51
+ .filter-btn.active { border-color: var(--orange); color: var(--orange); }
52
+ .filter-btn:hover { border-color: var(--text); color: var(--text); }
53
+ #results { width: 100%; max-width: 700px; display: none; }
54
+ .meta { font-size: 0.8rem; color: var(--muted); margin-bottom: 1.5rem; text-align: center; }
55
+ .file-row {
56
+ background: var(--surface); border: 1px solid var(--border);
57
+ border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 0.75rem;
58
+ }
59
+ .file-rank { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }
60
+ .rank-num { font-size: 1.1rem; min-width: 1.5rem; }
61
+ .file-path { font-size: 0.82rem; color: var(--cyan); font-family: Consolas, monospace; word-break: break-all; }
62
+ .bar-container { margin-bottom: 0.5rem; }
63
+ .bar-bg { height: 6px; border-radius: 3px; background: #21262d; overflow: hidden; }
64
+ .bar-fill { height: 100%; border-radius: 3px; background: var(--orange); }
65
+ .bar-label { display: flex; justify-content: space-between; font-size: 0.75rem; margin-top: 0.25rem; }
66
+ .counts { display: flex; gap: 1rem; font-size: 0.78rem; flex-wrap: wrap; margin-top: 0.4rem; }
67
+ .count-w { color: var(--orange); } .count-r { color: var(--green); } .count-s { color: var(--muted); }
68
+ .summary-card {
69
+ background: var(--surface); border: 1px solid var(--border);
70
+ border-radius: 8px; padding: 1.25rem; text-align: center; margin-top: 1.5rem;
71
+ }
72
+ .s-title { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 1rem; }
73
+ .s-stats { display: flex; justify-content: center; gap: 2.5rem; flex-wrap: wrap; }
74
+ .s-stat .num { font-size: 1.5rem; font-weight: 700; color: var(--orange); display: block; }
75
+ .s-stat .lbl { font-size: 0.75rem; color: var(--muted); }
76
+ .footer { margin-top: 2rem; text-align: center; font-size: 0.8rem; color: var(--muted); }
77
+ .footer a { color: var(--blue); text-decoration: none; margin: 0 0.5rem; }
78
+ .btn {
79
+ background: var(--orange); color: #0d1117; border: none;
80
+ border-radius: 6px; padding: 0.6rem 1.5rem; font-size: 0.9rem;
81
+ font-weight: 600; cursor: pointer; margin-top: 1rem; font-family: inherit;
82
+ }
83
+ .btn:hover { opacity: 0.85; }
84
+ </style>
85
+ </head>
86
+ <body>
87
+ <div class="header">
88
+ <h1>cc-file-churn</h1>
89
+ <p>Which files does Claude Code touch the most?</p>
90
+ </div>
91
+ <div class="drop-zone" id="dropZone" onclick="document.getElementById('fi').click()">
92
+ <div class="icon">📁</div>
93
+ <h2>Drop your .claude folder here</h2>
94
+ <p>Or click to select it.<br>Your data stays local — nothing is uploaded.</p>
95
+ <input type="file" id="fi" webkitdirectory multiple>
96
+ <button class="btn" onclick="event.stopPropagation(); document.getElementById('fi').click()">
97
+ Select ~/.claude folder
98
+ </button>
99
+ </div>
100
+ <div class="controls" id="controls" style="display:none;">
101
+ <button class="filter-btn active" data-mode="total">All activity</button>
102
+ <button class="filter-btn" data-mode="writes">Most-modified</button>
103
+ <button class="filter-btn" data-mode="reads">Most-referenced</button>
104
+ </div>
105
+ <div id="results">
106
+ <div class="meta" id="metaLine"></div>
107
+ <div id="fileList"></div>
108
+ <div class="summary-card" id="summaryCard"></div>
109
+ <div class="footer">
110
+ <a href="https://github.com/yurukusa/cc-file-churn" target="_blank">GitHub</a> ·
111
+ <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit (106 free tools)</a> ·
112
+ <span>Also: <code>npx cc-file-churn</code></span>
113
+ </div>
114
+ </div>
115
+ <div class="footer" id="footerMain">
116
+ <a href="https://github.com/yurukusa/cc-file-churn" target="_blank">GitHub</a> ·
117
+ <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit</a> ·
118
+ <span>Part of 106 free tools for Claude Code</span>
119
+ </div>
120
+ <script>
121
+ const READ_TOOLS = new Set(['Read']);
122
+ const WRITE_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit']);
123
+ const SEARCH_TOOLS = new Set(['Grep', 'Glob']);
124
+ const ALL_TRACKED = new Set([...READ_TOOLS, ...WRITE_TOOLS, ...SEARCH_TOOLS]);
125
+
126
+ let allFiles = [];
127
+ let currentMode = 'total';
128
+
129
+ const dropZone = document.getElementById('dropZone');
130
+ const fi = document.getElementById('fi');
131
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
132
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
133
+ dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); processFiles(Array.from(e.dataTransfer.files)); });
134
+ fi.addEventListener('change', e => processFiles(Array.from(e.target.files)));
135
+
136
+ document.querySelectorAll('.filter-btn').forEach(btn => {
137
+ btn.addEventListener('click', () => {
138
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
139
+ btn.classList.add('active');
140
+ currentMode = btn.dataset.mode;
141
+ renderFiles(allFiles);
142
+ });
143
+ });
144
+
145
+ async function processFiles(files) {
146
+ const jsonlFiles = files.filter(f => f.name.endsWith('.jsonl') && f.webkitRelativePath.includes('projects'));
147
+ if (!jsonlFiles.length) { alert('No session files found.\nSelect your .claude folder (not a subfolder).'); return; }
148
+ const fileMap = new Map();
149
+ for (const f of jsonlFiles) {
150
+ try {
151
+ const text = await readChunk(f, 4 * 1024 * 1024);
152
+ parseChunk(text, fileMap);
153
+ } catch {}
154
+ }
155
+ allFiles = [...fileMap.entries()].map(([path, c]) => ({
156
+ path, r: c.r, w: c.w, s: c.s, total: c.r + c.w + c.s
157
+ }));
158
+ const totalOps = allFiles.reduce((sum, e) => sum + e.total, 0);
159
+ document.getElementById('metaLine').textContent =
160
+ `${jsonlFiles.length} session files · ${allFiles.length} unique files · ${totalOps} tool calls`;
161
+ dropZone.style.display = 'none';
162
+ document.getElementById('footerMain').style.display = 'none';
163
+ document.getElementById('controls').style.display = 'flex';
164
+ document.getElementById('results').style.display = 'block';
165
+ renderFiles(allFiles);
166
+ }
167
+
168
+ async function readChunk(file, maxBytes) {
169
+ const start = Math.max(0, file.size - maxBytes);
170
+ return await file.slice(start).text();
171
+ }
172
+
173
+ function parseChunk(text, fileMap) {
174
+ for (const line of text.split('\n')) {
175
+ if (!line.trim()) continue;
176
+ let d; try { d = JSON.parse(line); } catch { continue; }
177
+ if (d.type !== 'assistant') continue;
178
+ for (const c of d?.message?.content ?? []) {
179
+ if (c?.type !== 'tool_use' || !ALL_TRACKED.has(c.name)) continue;
180
+ const path = c.input?.file_path ?? c.input?.path ?? '';
181
+ if (!path || typeof path !== 'string') continue;
182
+ const norm = path.replace(/^\/home\/[^/]+/, '~');
183
+ if (!fileMap.has(norm)) fileMap.set(norm, { r: 0, w: 0, s: 0 });
184
+ const e = fileMap.get(norm);
185
+ if (READ_TOOLS.has(c.name)) e.r++;
186
+ else if (WRITE_TOOLS.has(c.name)) e.w++;
187
+ else if (SEARCH_TOOLS.has(c.name)) e.s++;
188
+ }
189
+ }
190
+ }
191
+
192
+ function renderFiles(files) {
193
+ const sorted = [...files].sort((a, b) =>
194
+ currentMode === 'writes' ? b.w - a.w :
195
+ currentMode === 'reads' ? b.r - a.r : b.total - a.total
196
+ ).slice(0, 25);
197
+
198
+ const maxVal = sorted[0]?.[currentMode === 'writes' ? 'w' : currentMode === 'reads' ? 'r' : 'total'] ?? 1;
199
+ const medals = ['🥇', '🥈', '🥉'];
200
+ const list = document.getElementById('fileList');
201
+ list.innerHTML = sorted.map((e, i) => {
202
+ const val = currentMode === 'writes' ? e.w : currentMode === 'reads' ? e.r : e.total;
203
+ const pct = (val / maxVal * 100).toFixed(0);
204
+ const short = e.path.length > 65 ? '…' + e.path.slice(-64) : e.path;
205
+ const countParts = [];
206
+ if (e.w > 0) countParts.push(`<span class="count-w">${e.w} writes</span>`);
207
+ if (e.r > 0) countParts.push(`<span class="count-r">${e.r} reads</span>`);
208
+ if (e.s > 0) countParts.push(`<span class="count-s">${e.s} searches</span>`);
209
+ return `<div class="file-row">
210
+ <div class="file-rank">
211
+ <span class="rank-num">${medals[i] ?? '#' + (i+1)}</span>
212
+ <span style="font-size:1.1rem;font-weight:700;color:var(--orange)">${val}</span>
213
+ </div>
214
+ <div class="file-path">${short}</div>
215
+ <div class="bar-container">
216
+ <div class="bar-bg"><div class="bar-fill" style="width:${pct}%"></div></div>
217
+ <div class="bar-label"><span>${currentMode}</span><span style="color:var(--muted)">${pct}%</span></div>
218
+ </div>
219
+ <div class="counts">${countParts.join('')}</div>
220
+ </div>`;
221
+ }).join('');
222
+
223
+ const totalWrites = files.reduce((s, e) => s + e.w, 0);
224
+ const totalReads = files.reduce((s, e) => s + e.r, 0);
225
+ const topFile = sorted[0];
226
+ document.getElementById('summaryCard').innerHTML = `
227
+ <div class="s-title">Summary</div>
228
+ <div class="s-stats">
229
+ <div class="s-stat"><span class="num">${files.length}</span><span class="lbl">unique files</span></div>
230
+ <div class="s-stat"><span class="num" style="color:var(--orange)">${totalWrites}</span><span class="lbl">total writes</span></div>
231
+ <div class="s-stat"><span class="num" style="color:var(--green)">${totalReads}</span><span class="lbl">total reads</span></div>
232
+ <div class="s-stat"><span class="num">${topFile ? topFile.total : 0}</span><span class="lbl">max activity (#1)</span></div>
233
+ </div>`;
234
+ }
235
+ </script>
236
+ </body>
237
+ </html>
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "cc-file-churn",
3
+ "version": "1.0.0",
4
+ "description": "Which files does Claude Code touch the most? Ranks files by Edit/Write/Read frequency across all sessions.",
5
+ "main": "cli.mjs",
6
+ "bin": {
7
+ "cc-file-churn": "./cli.mjs"
8
+ },
9
+ "scripts": {
10
+ "start": "node cli.mjs"
11
+ },
12
+ "keywords": [
13
+ "claude-code",
14
+ "claude",
15
+ "ai",
16
+ "file-churn",
17
+ "developer-tools",
18
+ "cli",
19
+ "productivity"
20
+ ],
21
+ "author": "yurukusa",
22
+ "license": "MIT",
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=18"
26
+ }
27
+ }