cc-depth 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 +78 -0
  2. package/cli.mjs +196 -0
  3. package/index.html +293 -0
  4. package/package.json +25 -0
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # cc-depth
2
+
3
+ How many turns per Claude Code session?
4
+
5
+ Shows the distribution of conversation depth — how many back-and-forth exchanges happen in each session.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ npx cc-depth
11
+ ```
12
+
13
+ ```
14
+ cc-depth — Turns per session
15
+
16
+ 1 ███░░░░░░░░░░░░░░░░░░░░░░░░░░░ 18 ( 3%)
17
+ 2–5 ██████████████████░░░░░░░░░░░░ 104 ( 20%)
18
+ 6–15 █████████████░░░░░░░░░░░░░░░░░ 75 ( 14%)
19
+ 16–30 ████████░░░░░░░░░░░░░░░░░░░░░░ 48 ( 9%)
20
+ 31–60 █████████░░░░░░░░░░░░░░░░░░░░░ 54 ( 10%)
21
+ 61–100 █████████░░░░░░░░░░░░░░░░░░░░░ 53 ( 10%)
22
+ 101+ ██████████████████████████████ 174 ( 33%)
23
+
24
+ ─────────────────────────────────────────────────────────
25
+ Median: 38 turns/session
26
+ Mean: 263 turns/session
27
+ Peak: 14,169 turns
28
+ Style: 🔄 Loop Runner (extended sessions or autonomous loop)
29
+
30
+ Analyzed 526 sessions
31
+ ```
32
+
33
+ ## Style Classifications
34
+
35
+ | Style | Median | What it means |
36
+ |-------|--------|---------------|
37
+ | 💬 Quick Prompter | ≤ 3 turns | One-shot queries, fast iterations |
38
+ | ✅ Task Completer | 4–10 turns | Focused task sessions |
39
+ | 🤝 Collaborative Coder | 11–30 turns | Back-and-forth workflow |
40
+ | 🔄 Loop Runner | > 30 turns | Extended sessions or autonomous loop |
41
+
42
+ ## Options
43
+
44
+ ```bash
45
+ npx cc-depth # All sessions
46
+ npx cc-depth --json # JSON output
47
+ npx cc-depth --projects=cc-loop # Filter by project name
48
+ npx cc-depth --help # Show help
49
+ ```
50
+
51
+ ## Browser Version
52
+
53
+ → **[yurukusa.github.io/cc-depth](https://yurukusa.github.io/cc-depth/)**
54
+
55
+ Drag in your `~/.claude` folder. Runs entirely locally.
56
+
57
+ ## What counts as a "turn"?
58
+
59
+ Each user message in a session counts as one turn. This includes:
60
+ - Your direct prompts and questions
61
+ - Continuation messages (in autonomous setups like cc-loop)
62
+ - System-level messages
63
+
64
+ For interactive users, a typical session is 2–30 turns. Autonomous loop setups show much higher counts.
65
+
66
+ ## Part of cc-toolkit
67
+
68
+ cc-depth is tool #49 in [cc-toolkit](https://yurukusa.github.io/cc-toolkit/) — 50 free tools for Claude Code users.
69
+
70
+ Related:
71
+ - [cc-session-length](https://github.com/yurukusa/cc-session-length) — Duration distribution
72
+ - [cc-momentum](https://github.com/yurukusa/cc-momentum) — Week-by-week session trend
73
+ - [cc-gap](https://github.com/yurukusa/cc-gap) — Time between sessions
74
+
75
+ ---
76
+
77
+ **GitHub**: [yurukusa/cc-depth](https://github.com/yurukusa/cc-depth)
78
+ **Try it**: `npx cc-depth`
package/cli.mjs ADDED
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cc-depth — How many turns per Claude Code session?
4
+ * Shows the distribution of conversation depth across your sessions.
5
+ */
6
+
7
+ import { readFileSync, readdirSync, statSync } from 'fs';
8
+ import { join, resolve } from 'path';
9
+ import { homedir } from 'os';
10
+
11
+ const args = process.argv.slice(2);
12
+ const jsonMode = args.includes('--json');
13
+ const showHelp = args.includes('--help') || args.includes('-h');
14
+ const projectsFlag = args.find(a => a.startsWith('--projects='));
15
+ const projectsFilter = projectsFlag ? projectsFlag.split('=')[1] : null;
16
+
17
+ if (showHelp) {
18
+ console.log(`cc-depth — Conversation depth per Claude Code session
19
+
20
+ Usage:
21
+ npx cc-depth # All sessions
22
+ npx cc-depth --json # JSON output
23
+ npx cc-depth --projects=cc-loop # Filter by project name
24
+
25
+ Shows how many turns (user messages) occur per session.
26
+ `);
27
+ process.exit(0);
28
+ }
29
+
30
+ // Find ~/.claude/projects directory
31
+ const claudeDir = join(homedir(), '.claude', 'projects');
32
+
33
+ function scanProjects(dir) {
34
+ const sessions = [];
35
+ let projectDirs;
36
+ try {
37
+ projectDirs = readdirSync(dir);
38
+ } catch {
39
+ return sessions;
40
+ }
41
+
42
+ for (const projDir of projectDirs) {
43
+ if (projectsFilter && !projDir.includes(projectsFilter)) continue;
44
+ const projPath = join(dir, projDir);
45
+ let entries;
46
+ try {
47
+ entries = readdirSync(projPath);
48
+ } catch {
49
+ continue;
50
+ }
51
+
52
+ for (const entry of entries) {
53
+ if (!entry.endsWith('.jsonl')) continue;
54
+ // Skip subagent files
55
+ if (projPath.includes('/subagents/') || entry.includes('subagent')) continue;
56
+
57
+ const filePath = join(projPath, entry);
58
+ // Check subdirs (subagents)
59
+ try {
60
+ const stat = statSync(filePath);
61
+ if (!stat.isFile()) continue;
62
+ } catch {
63
+ continue;
64
+ }
65
+
66
+ // Count "type":"user" occurrences in file
67
+ let content;
68
+ try {
69
+ content = readFileSync(filePath, 'utf8');
70
+ } catch {
71
+ continue;
72
+ }
73
+
74
+ // Fast string counting — "type":"user" at top level
75
+ // Each line is one JSON object; we count lines containing this pattern
76
+ let turns = 0;
77
+ let firstTimestamp = null;
78
+ const lines = content.split('\n');
79
+ for (const line of lines) {
80
+ if (!line.trim()) continue;
81
+ // Count user turns
82
+ if (line.includes('"type":"user"')) {
83
+ turns++;
84
+ }
85
+ // Grab first timestamp for date info
86
+ if (!firstTimestamp && line.includes('"timestamp"')) {
87
+ const m = line.match(/"timestamp":"([^"]+)"/);
88
+ if (m) firstTimestamp = m[1];
89
+ }
90
+ }
91
+
92
+ if (turns > 0) {
93
+ sessions.push({
94
+ id: entry.replace('.jsonl', ''),
95
+ project: projDir,
96
+ turns,
97
+ date: firstTimestamp ? new Date(firstTimestamp) : null,
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ return sessions;
104
+ }
105
+
106
+ const sessions = scanProjects(claudeDir);
107
+
108
+ if (sessions.length === 0) {
109
+ console.error('No session files found. Make sure ~/.claude/projects/ exists.');
110
+ process.exit(1);
111
+ }
112
+
113
+ // Sort by turns ascending for stats
114
+ const turnCounts = sessions.map(s => s.turns).sort((a, b) => a - b);
115
+ const n = turnCounts.length;
116
+ const total = turnCounts.reduce((a, b) => a + b, 0);
117
+ const median = turnCounts[Math.floor(n / 2)];
118
+ const mean = total / n;
119
+ const max = turnCounts[n - 1];
120
+ const min = turnCounts[0];
121
+
122
+ // Peak session
123
+ const peakSession = sessions.reduce((a, b) => a.turns > b.turns ? a : b);
124
+
125
+ // Distribution buckets
126
+ const buckets = [
127
+ { label: '1', min: 1, max: 1, count: 0 },
128
+ { label: '2–5', min: 2, max: 5, count: 0 },
129
+ { label: '6–15', min: 6, max: 15, count: 0 },
130
+ { label: '16–30', min: 16, max: 30, count: 0 },
131
+ { label: '31–60', min: 31, max: 60, count: 0 },
132
+ { label: '61–100',min: 61, max: 100, count: 0 },
133
+ { label: '101+', min: 101, max: Infinity, count: 0 },
134
+ ];
135
+
136
+ for (const t of turnCounts) {
137
+ for (const b of buckets) {
138
+ if (t >= b.min && t <= b.max) {
139
+ b.count++;
140
+ break;
141
+ }
142
+ }
143
+ }
144
+
145
+ // Classification
146
+ function classify(median) {
147
+ if (median <= 3) return { label: '💬 Quick Prompter', desc: 'one-shot queries, fast iterations' };
148
+ if (median <= 10) return { label: '✅ Task Completer', desc: 'focused task sessions' };
149
+ if (median <= 30) return { label: '🤝 Collaborative Coder', desc: 'back-and-forth workflow' };
150
+ return { label: '🔄 Loop Runner', desc: 'extended sessions or autonomous loop' };
151
+ }
152
+
153
+ const style = classify(median);
154
+
155
+ if (jsonMode) {
156
+ console.log(JSON.stringify({
157
+ sessions: n,
158
+ median_turns: median,
159
+ mean_turns: Math.round(mean),
160
+ min_turns: min,
161
+ max_turns: max,
162
+ style: style.label,
163
+ buckets: buckets.map(b => ({ range: b.label, count: b.count, pct: Math.round(b.count / n * 100) })),
164
+ peak: { id: peakSession.id, turns: peakSession.turns, date: peakSession.date },
165
+ }, null, 2));
166
+ process.exit(0);
167
+ }
168
+
169
+ // Bar chart rendering
170
+ const BAR_WIDTH = 30;
171
+ const maxCount = Math.max(...buckets.map(b => b.count));
172
+
173
+ function bar(count) {
174
+ const filled = maxCount > 0 ? Math.round((count / maxCount) * BAR_WIDTH) : 0;
175
+ return '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
176
+ }
177
+
178
+ function rpad(str, len) {
179
+ return str + ' '.repeat(Math.max(0, len - str.length));
180
+ }
181
+
182
+ console.log('cc-depth — Turns per session\n');
183
+
184
+ for (const b of buckets) {
185
+ const label = rpad(b.label, 7);
186
+ const countStr = String(b.count).padStart(4);
187
+ const pct = (b.count / n * 100).toFixed(0).padStart(3);
188
+ console.log(` ${label} ${bar(b.count)} ${countStr} (${pct}%)`);
189
+ }
190
+
191
+ console.log('\n' + '─'.repeat(57));
192
+ console.log(` Median: ${median} turns/session`);
193
+ console.log(` Mean: ${Math.round(mean)} turns/session`);
194
+ console.log(` Peak: ${max.toLocaleString()} turns`);
195
+ console.log(` Style: ${style.label} (${style.desc})`);
196
+ console.log(`\n Analyzed ${n} sessions`);
package/index.html ADDED
@@ -0,0 +1,293 @@
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-depth — Conversation depth per session</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body {
10
+ background: #0d1117;
11
+ color: #c9d1d9;
12
+ font-family: 'SF Mono', 'Consolas', 'Cascadia Code', monospace;
13
+ min-height: 100vh;
14
+ display: flex;
15
+ flex-direction: column;
16
+ align-items: center;
17
+ padding: 40px 20px;
18
+ }
19
+ h1 {
20
+ font-size: 1.5rem;
21
+ color: #f0883e;
22
+ margin-bottom: 6px;
23
+ letter-spacing: -0.5px;
24
+ }
25
+ .subtitle {
26
+ color: #8b949e;
27
+ font-size: 0.875rem;
28
+ margin-bottom: 32px;
29
+ }
30
+ .drop-zone {
31
+ border: 2px dashed #30363d;
32
+ border-radius: 12px;
33
+ padding: 48px 64px;
34
+ text-align: center;
35
+ cursor: pointer;
36
+ transition: all 0.2s;
37
+ max-width: 480px;
38
+ width: 100%;
39
+ margin-bottom: 32px;
40
+ }
41
+ .drop-zone:hover, .drop-zone.drag-over {
42
+ border-color: #f0883e;
43
+ background: rgba(240,136,62,0.05);
44
+ }
45
+ .drop-icon { font-size: 2.5rem; margin-bottom: 12px; }
46
+ .drop-text { color: #8b949e; font-size: 0.875rem; line-height: 1.6; }
47
+ .drop-text strong { color: #c9d1d9; }
48
+ #file-input { display: none; }
49
+
50
+ .result { display: none; max-width: 600px; width: 100%; }
51
+ .result.visible { display: block; }
52
+
53
+ .card {
54
+ background: #161b22;
55
+ border: 1px solid #30363d;
56
+ border-radius: 8px;
57
+ padding: 20px;
58
+ margin-bottom: 16px;
59
+ }
60
+ .card-title {
61
+ color: #8b949e;
62
+ font-size: 0.75rem;
63
+ text-transform: uppercase;
64
+ letter-spacing: 1px;
65
+ margin-bottom: 16px;
66
+ }
67
+
68
+ .stats-grid {
69
+ display: grid;
70
+ grid-template-columns: repeat(2, 1fr);
71
+ gap: 16px;
72
+ margin-bottom: 16px;
73
+ }
74
+ .stat-item { text-align: center; }
75
+ .stat-val { font-size: 1.6rem; color: #f0883e; font-weight: 700; }
76
+ .stat-lbl { font-size: 0.75rem; color: #8b949e; margin-top: 2px; }
77
+
78
+ .style-badge {
79
+ text-align: center;
80
+ padding: 10px;
81
+ background: rgba(240,136,62,0.1);
82
+ border: 1px solid rgba(240,136,62,0.3);
83
+ border-radius: 6px;
84
+ font-size: 1rem;
85
+ color: #f0883e;
86
+ }
87
+ .style-desc { font-size: 0.8rem; color: #8b949e; margin-top: 4px; }
88
+
89
+ .bar-chart { font-size: 0.8rem; }
90
+ .bar-row {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 10px;
94
+ margin-bottom: 6px;
95
+ }
96
+ .bar-label { width: 56px; text-align: right; color: #8b949e; flex-shrink: 0; }
97
+ .bar-track {
98
+ flex: 1;
99
+ height: 14px;
100
+ background: #21262d;
101
+ border-radius: 3px;
102
+ overflow: hidden;
103
+ }
104
+ .bar-fill {
105
+ height: 100%;
106
+ background: #f0883e;
107
+ border-radius: 3px;
108
+ transition: width 0.6s ease;
109
+ }
110
+ .bar-count { width: 64px; color: #c9d1d9; text-align: right; flex-shrink: 0; }
111
+
112
+ .footer { color: #8b949e; font-size: 0.75rem; text-align: center; margin-top: 12px; }
113
+ .footer a { color: #f0883e; text-decoration: none; }
114
+ .footer a:hover { text-decoration: underline; }
115
+
116
+ .reset-btn {
117
+ margin-top: 16px;
118
+ background: none;
119
+ border: 1px solid #30363d;
120
+ color: #8b949e;
121
+ padding: 8px 16px;
122
+ border-radius: 6px;
123
+ cursor: pointer;
124
+ font-family: inherit;
125
+ font-size: 0.8rem;
126
+ display: block;
127
+ width: 100%;
128
+ transition: all 0.2s;
129
+ }
130
+ .reset-btn:hover { border-color: #f0883e; color: #f0883e; }
131
+ </style>
132
+ </head>
133
+ <body>
134
+
135
+ <h1>📊 cc-depth</h1>
136
+ <p class="subtitle">Conversation depth per Claude Code session</p>
137
+
138
+ <div class="drop-zone" id="drop-zone">
139
+ <div class="drop-icon">📁</div>
140
+ <div class="drop-text">
141
+ <strong>Drop your ~/.claude folder here</strong><br>
142
+ or click to select<br>
143
+ <br>
144
+ Reads session files locally.<br>
145
+ Nothing is uploaded.
146
+ </div>
147
+ <input type="file" id="file-input" webkitdirectory multiple accept=".jsonl">
148
+ </div>
149
+
150
+ <div class="result" id="result">
151
+ <div class="card">
152
+ <div class="card-title">Session Depth Summary</div>
153
+ <div class="stats-grid">
154
+ <div class="stat-item">
155
+ <div class="stat-val" id="stat-median">—</div>
156
+ <div class="stat-lbl">median turns</div>
157
+ </div>
158
+ <div class="stat-item">
159
+ <div class="stat-val" id="stat-mean">—</div>
160
+ <div class="stat-lbl">mean turns</div>
161
+ </div>
162
+ <div class="stat-item">
163
+ <div class="stat-val" id="stat-max">—</div>
164
+ <div class="stat-lbl">peak turns</div>
165
+ </div>
166
+ <div class="stat-item">
167
+ <div class="stat-val" id="stat-sessions">—</div>
168
+ <div class="stat-lbl">sessions</div>
169
+ </div>
170
+ </div>
171
+ <div class="style-badge" id="stat-style">—</div>
172
+ <div class="style-desc" id="stat-desc"></div>
173
+ </div>
174
+
175
+ <div class="card">
176
+ <div class="card-title">Distribution</div>
177
+ <div class="bar-chart" id="bar-chart"></div>
178
+ </div>
179
+
180
+ <button class="reset-btn" id="reset-btn">← Analyze another folder</button>
181
+ </div>
182
+
183
+ <div class="footer">
184
+ <a href="https://github.com/yurukusa/cc-depth" target="_blank">cc-depth</a> ·
185
+ Part of <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit</a> · 106 free tools for Claude Code
186
+ </div>
187
+
188
+ <script>
189
+ const dropZone = document.getElementById('drop-zone');
190
+ const fileInput = document.getElementById('file-input');
191
+ const resultEl = document.getElementById('result');
192
+ const resetBtn = document.getElementById('reset-btn');
193
+
194
+ dropZone.addEventListener('click', () => fileInput.click());
195
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
196
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
197
+ dropZone.addEventListener('drop', e => {
198
+ e.preventDefault();
199
+ dropZone.classList.remove('drag-over');
200
+ processFiles(e.dataTransfer.files);
201
+ });
202
+ fileInput.addEventListener('change', () => processFiles(fileInput.files));
203
+ resetBtn.addEventListener('click', () => {
204
+ resultEl.classList.remove('visible');
205
+ dropZone.style.display = '';
206
+ fileInput.value = '';
207
+ });
208
+
209
+ async function processFiles(files) {
210
+ const jsonlFiles = Array.from(files).filter(f => {
211
+ const p = f.webkitRelativePath || f.name;
212
+ return p.endsWith('.jsonl') && !p.includes('/subagents/');
213
+ });
214
+
215
+ if (jsonlFiles.length === 0) {
216
+ alert('No session files found. Drop your ~/.claude folder.');
217
+ return;
218
+ }
219
+
220
+ dropZone.style.display = 'none';
221
+
222
+ const sessions = [];
223
+ const BATCH = 50;
224
+ for (let i = 0; i < jsonlFiles.length; i += BATCH) {
225
+ const batch = jsonlFiles.slice(i, i + BATCH);
226
+ await Promise.all(batch.map(async f => {
227
+ try {
228
+ const text = await f.text();
229
+ let turns = 0;
230
+ for (const line of text.split('\n')) {
231
+ if (line.includes('"type":"user"')) turns++;
232
+ }
233
+ if (turns > 0) sessions.push(turns);
234
+ } catch {}
235
+ }));
236
+ }
237
+
238
+ if (sessions.length === 0) {
239
+ alert('Could not parse session files.');
240
+ return;
241
+ }
242
+
243
+ sessions.sort((a, b) => a - b);
244
+ const n = sessions.length;
245
+ const median = sessions[Math.floor(n / 2)];
246
+ const mean = Math.round(sessions.reduce((a, b) => a + b, 0) / n);
247
+ const max = sessions[n - 1];
248
+
249
+ const classify = m => {
250
+ if (m <= 3) return { label: '💬 Quick Prompter', desc: 'one-shot queries, fast iterations' };
251
+ if (m <= 10) return { label: '✅ Task Completer', desc: 'focused task sessions' };
252
+ if (m <= 30) return { label: '🤝 Collaborative Coder', desc: 'back-and-forth workflow' };
253
+ return { label: '🔄 Loop Runner', desc: 'extended sessions or autonomous loop' };
254
+ };
255
+ const style = classify(median);
256
+
257
+ document.getElementById('stat-median').textContent = median;
258
+ document.getElementById('stat-mean').textContent = mean.toLocaleString();
259
+ document.getElementById('stat-max').textContent = max.toLocaleString();
260
+ document.getElementById('stat-sessions').textContent = n;
261
+ document.getElementById('stat-style').textContent = style.label;
262
+ document.getElementById('stat-desc').textContent = style.desc;
263
+
264
+ const buckets = [
265
+ { label: '1', min: 1, max: 1 },
266
+ { label: '2–5', min: 2, max: 5 },
267
+ { label: '6–15', min: 6, max: 15 },
268
+ { label: '16–30', min: 16, max: 30 },
269
+ { label: '31–60', min: 31, max: 60 },
270
+ { label: '61–100',min: 61, max: 100 },
271
+ { label: '101+', min: 101, max: Infinity },
272
+ ].map(b => ({
273
+ ...b,
274
+ count: sessions.filter(t => t >= b.min && t <= b.max).length,
275
+ }));
276
+
277
+ const maxCount = Math.max(...buckets.map(b => b.count));
278
+ const chart = document.getElementById('bar-chart');
279
+ chart.innerHTML = buckets.map(b => `
280
+ <div class="bar-row">
281
+ <div class="bar-label">${b.label}</div>
282
+ <div class="bar-track">
283
+ <div class="bar-fill" style="width:${maxCount > 0 ? (b.count / maxCount * 100) : 0}%"></div>
284
+ </div>
285
+ <div class="bar-count">${b.count} (${Math.round(b.count / n * 100)}%)</div>
286
+ </div>
287
+ `).join('');
288
+
289
+ resultEl.classList.add('visible');
290
+ }
291
+ </script>
292
+ </body>
293
+ </html>
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "cc-depth",
3
+ "version": "1.0.0",
4
+ "description": "How many turns per Claude Code session? Distribution of conversation depth.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cc-depth": "./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
+ "productivity"
19
+ ],
20
+ "author": "yurukusa",
21
+ "license": "MIT",
22
+ "engines": {
23
+ "node": ">=18"
24
+ }
25
+ }