cc-subagent 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 +59 -0
- package/cli.mjs +203 -0
- package/index.html +249 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# cc-subagent
|
|
2
|
+
|
|
3
|
+
How many subagents does your Claude Code spawn? Shows subagent adoption rate, total count, peak sessions, and per-project breakdown.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
cc-subagent — Claude Code subagent usage
|
|
7
|
+
|
|
8
|
+
Main sessions: 755
|
|
9
|
+
Sessions w/ subagents: 324 (42.9% of sessions)
|
|
10
|
+
Total subagent sessions: 3,677
|
|
11
|
+
Avg per spawning session: 11.3
|
|
12
|
+
Avg per all sessions: 4.9
|
|
13
|
+
Total subagent data: 1003.0 MB
|
|
14
|
+
Peak in one session: 284 subagents
|
|
15
|
+
|
|
16
|
+
────────────────────────────────────────────────────────
|
|
17
|
+
Subagents per session (spawning sessions only)
|
|
18
|
+
|
|
19
|
+
1 ███████████░░░░░░░░░░░░░ 64 (19.8%)
|
|
20
|
+
2-5 ████████████████████████ 142 (43.8%)
|
|
21
|
+
6-10 ████████░░░░░░░░░░░░░░░░ 48 (14.8%)
|
|
22
|
+
11-30 ███████░░░░░░░░░░░░░░░░░ 43 (13.3%)
|
|
23
|
+
31-100 ████░░░░░░░░░░░░░░░░░░░░ 21 (6.5%)
|
|
24
|
+
100+ █░░░░░░░░░░░░░░░░░░░░░░░ 6 (1.9%)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx cc-subagent # Subagent count and adoption stats
|
|
31
|
+
npx cc-subagent --json # JSON output
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What it shows
|
|
35
|
+
|
|
36
|
+
- **Adoption rate** — what fraction of your sessions spawned at least one subagent
|
|
37
|
+
- **Total count** — cumulative subagent sessions across your history
|
|
38
|
+
- **Distribution** — single subagent vs teams of 100+
|
|
39
|
+
- **Peak session** — the session that spawned the most subagents
|
|
40
|
+
- **Subagent data size** — total disk usage of subagent session files
|
|
41
|
+
- **By project** — which projects rely on subagents most
|
|
42
|
+
|
|
43
|
+
## About subagents
|
|
44
|
+
|
|
45
|
+
When Claude Code uses the `Agent` tool, it spawns a subagent — a child Claude session that handles a specific subtask. Subagent session files are stored at `~/.claude/projects/PROJ/SESSION_ID/subagents/agent-XXXXX.jsonl`.
|
|
46
|
+
|
|
47
|
+
Most tools filter out subagent files by default. cc-subagent specifically analyzes what's usually hidden.
|
|
48
|
+
|
|
49
|
+
## Privacy
|
|
50
|
+
|
|
51
|
+
Reads file metadata only (names, sizes). No file content is accessed or transmitted. Everything runs locally.
|
|
52
|
+
|
|
53
|
+
## Browser version
|
|
54
|
+
|
|
55
|
+
Drop your `~/.claude` folder into [cc-subagent on the web](https://yurukusa.github.io/cc-subagent/) — no install required.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
Part of [cc-toolkit](https://yurukusa.github.io/cc-toolkit/) — 60 free tools for Claude Code
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cc-subagent — How many subagents does your Claude Code spawn?
|
|
4
|
+
* Analyzes subagent usage: sessions, count, size, and per-project breakdown.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readdirSync, statSync } from 'fs';
|
|
8
|
+
import { join } 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
|
+
|
|
15
|
+
if (showHelp) {
|
|
16
|
+
console.log(`cc-subagent — Subagent usage in your Claude Code sessions
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
npx cc-subagent # Subagent count, size, and project breakdown
|
|
20
|
+
npx cc-subagent --json # JSON output
|
|
21
|
+
`);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const claudeDir = join(homedir(), '.claude', 'projects');
|
|
26
|
+
|
|
27
|
+
function humanSize(bytes) {
|
|
28
|
+
if (bytes >= 1024 ** 3) return (bytes / 1024 ** 3).toFixed(1) + ' GB';
|
|
29
|
+
if (bytes >= 1024 ** 2) return (bytes / 1024 ** 2).toFixed(1) + ' MB';
|
|
30
|
+
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
31
|
+
return bytes + ' B';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function projectName(dirName) {
|
|
35
|
+
const stripped = dirName.replace(/^-home-[^-]+/, '').replace(/^-/, '');
|
|
36
|
+
return stripped || '~/ (home)';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let projectDirs;
|
|
40
|
+
try {
|
|
41
|
+
projectDirs = readdirSync(claudeDir);
|
|
42
|
+
} catch {
|
|
43
|
+
console.error(`Cannot read ${claudeDir}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Totals
|
|
48
|
+
let totalMainSessions = 0;
|
|
49
|
+
let sessionsWithSubagents = 0;
|
|
50
|
+
let totalSubagentSessions = 0;
|
|
51
|
+
let totalSubagentBytes = 0;
|
|
52
|
+
let maxSubagentsInSession = 0;
|
|
53
|
+
let maxSubagentsSession = '';
|
|
54
|
+
|
|
55
|
+
const byProject = {}; // { projDir: { subagentSessions, parentSessions, bytes, name } }
|
|
56
|
+
const distribution = { 1: 0, '2-5': 0, '6-10': 0, '11-30': 0, '31-100': 0, '100+': 0 };
|
|
57
|
+
|
|
58
|
+
for (const projDir of projectDirs) {
|
|
59
|
+
const projPath = join(claudeDir, projDir);
|
|
60
|
+
let pstat;
|
|
61
|
+
try {
|
|
62
|
+
pstat = statSync(projPath);
|
|
63
|
+
if (!pstat.isDirectory()) continue;
|
|
64
|
+
} catch { continue; }
|
|
65
|
+
|
|
66
|
+
const projLabel = projectName(projDir);
|
|
67
|
+
if (!byProject[projDir]) byProject[projDir] = { subagentSessions: 0, parentSessions: 0, bytes: 0, name: projLabel };
|
|
68
|
+
|
|
69
|
+
// Each entry in projPath could be a session file OR a session dir (when it has subagents)
|
|
70
|
+
let entries;
|
|
71
|
+
try { entries = readdirSync(projPath); } catch { continue; }
|
|
72
|
+
|
|
73
|
+
// Count main sessions (*.jsonl files directly in projPath)
|
|
74
|
+
const mainSessions = entries.filter(e => e.endsWith('.jsonl'));
|
|
75
|
+
totalMainSessions += mainSessions.length;
|
|
76
|
+
byProject[projDir].parentSessions += mainSessions.length;
|
|
77
|
+
|
|
78
|
+
// Check subdirectories for session dirs with subagents
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (entry.endsWith('.jsonl')) continue; // skip session files
|
|
81
|
+
const sessionDir = join(projPath, entry);
|
|
82
|
+
let sdstat;
|
|
83
|
+
try {
|
|
84
|
+
sdstat = statSync(sessionDir);
|
|
85
|
+
if (!sdstat.isDirectory()) continue;
|
|
86
|
+
} catch { continue; }
|
|
87
|
+
|
|
88
|
+
const subagentsDir = join(sessionDir, 'subagents');
|
|
89
|
+
let subEntries;
|
|
90
|
+
try {
|
|
91
|
+
subEntries = readdirSync(subagentsDir);
|
|
92
|
+
} catch { continue; }
|
|
93
|
+
|
|
94
|
+
const subFiles = subEntries.filter(e => e.endsWith('.jsonl'));
|
|
95
|
+
if (!subFiles.length) continue;
|
|
96
|
+
|
|
97
|
+
// This session has subagents
|
|
98
|
+
sessionsWithSubagents++;
|
|
99
|
+
const sessionSubCount = subFiles.length;
|
|
100
|
+
totalSubagentSessions += sessionSubCount;
|
|
101
|
+
|
|
102
|
+
// Distribution bucket
|
|
103
|
+
if (sessionSubCount === 1) distribution[1]++;
|
|
104
|
+
else if (sessionSubCount <= 5) distribution['2-5']++;
|
|
105
|
+
else if (sessionSubCount <= 10) distribution['6-10']++;
|
|
106
|
+
else if (sessionSubCount <= 30) distribution['11-30']++;
|
|
107
|
+
else if (sessionSubCount <= 100) distribution['31-100']++;
|
|
108
|
+
else distribution['100+']++;
|
|
109
|
+
|
|
110
|
+
// Max
|
|
111
|
+
if (sessionSubCount > maxSubagentsInSession) {
|
|
112
|
+
maxSubagentsInSession = sessionSubCount;
|
|
113
|
+
maxSubagentsSession = `${projLabel}/${entry.slice(0, 8)}...`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Size of subagent files
|
|
117
|
+
let sessionSubBytes = 0;
|
|
118
|
+
for (const sf of subFiles) {
|
|
119
|
+
try {
|
|
120
|
+
sessionSubBytes += statSync(join(subagentsDir, sf)).size;
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
totalSubagentBytes += sessionSubBytes;
|
|
124
|
+
byProject[projDir].subagentSessions += sessionSubCount;
|
|
125
|
+
byProject[projDir].bytes += sessionSubBytes;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (totalMainSessions === 0) {
|
|
130
|
+
console.error('No session files found.');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const adoptionRate = (sessionsWithSubagents / totalMainSessions * 100).toFixed(1);
|
|
135
|
+
const avgPerSpawner = sessionsWithSubagents > 0 ? (totalSubagentSessions / sessionsWithSubagents).toFixed(1) : '0';
|
|
136
|
+
const avgPerAll = (totalSubagentSessions / totalMainSessions).toFixed(1);
|
|
137
|
+
|
|
138
|
+
const sortedProjects = Object.entries(byProject)
|
|
139
|
+
.filter(([, d]) => d.subagentSessions > 0)
|
|
140
|
+
.sort((a, b) => b[1].subagentSessions - a[1].subagentSessions);
|
|
141
|
+
|
|
142
|
+
if (jsonMode) {
|
|
143
|
+
console.log(JSON.stringify({
|
|
144
|
+
main_sessions: totalMainSessions,
|
|
145
|
+
sessions_with_subagents: sessionsWithSubagents,
|
|
146
|
+
adoption_rate_pct: parseFloat(adoptionRate),
|
|
147
|
+
total_subagent_sessions: totalSubagentSessions,
|
|
148
|
+
avg_per_spawning_session: parseFloat(avgPerSpawner),
|
|
149
|
+
avg_per_all_sessions: parseFloat(avgPerAll),
|
|
150
|
+
total_subagent_size: humanSize(totalSubagentBytes),
|
|
151
|
+
max_in_single_session: maxSubagentsInSession,
|
|
152
|
+
distribution,
|
|
153
|
+
by_project: sortedProjects.slice(0, 10).map(([, d]) => ({
|
|
154
|
+
project: d.name,
|
|
155
|
+
subagent_sessions: d.subagentSessions,
|
|
156
|
+
size: humanSize(d.bytes),
|
|
157
|
+
})),
|
|
158
|
+
}, null, 2));
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Terminal display
|
|
163
|
+
const BAR_WIDTH = 24;
|
|
164
|
+
|
|
165
|
+
function countBar(n, max) {
|
|
166
|
+
const filled = max > 0 ? Math.round((n / max) * BAR_WIDTH) : 0;
|
|
167
|
+
return '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function rpad(str, len) {
|
|
171
|
+
return str + ' '.repeat(Math.max(0, len - str.length));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log('cc-subagent — Claude Code subagent usage\n');
|
|
175
|
+
|
|
176
|
+
console.log(` Main sessions: ${totalMainSessions.toLocaleString()}`);
|
|
177
|
+
console.log(` Sessions w/ subagents:${sessionsWithSubagents.toLocaleString().padStart(5)} (${adoptionRate}% of sessions)`);
|
|
178
|
+
console.log(` Total subagent sessions: ${totalSubagentSessions.toLocaleString()}`);
|
|
179
|
+
console.log(` Avg per spawning session: ${avgPerSpawner}`);
|
|
180
|
+
console.log(` Avg per all sessions: ${avgPerAll}`);
|
|
181
|
+
console.log(` Total subagent data: ${humanSize(totalSubagentBytes)}`);
|
|
182
|
+
console.log(` Peak in one session: ${maxSubagentsInSession} subagents`);
|
|
183
|
+
|
|
184
|
+
// Distribution
|
|
185
|
+
console.log('\n' + '─'.repeat(56));
|
|
186
|
+
console.log(' Subagents per session (spawning sessions only)\n');
|
|
187
|
+
const maxDist = Math.max(...Object.values(distribution));
|
|
188
|
+
for (const [label, count] of Object.entries(distribution)) {
|
|
189
|
+
const pct = sessionsWithSubagents > 0 ? (count / sessionsWithSubagents * 100).toFixed(1) : '0.0';
|
|
190
|
+
console.log(` ${label.padEnd(8)} ${countBar(count, maxDist)} ${String(count).padStart(4)} (${pct}%)`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Project breakdown
|
|
194
|
+
if (sortedProjects.length) {
|
|
195
|
+
console.log('\n' + '─'.repeat(56));
|
|
196
|
+
console.log(' By project (top 8)\n');
|
|
197
|
+
const maxProj = sortedProjects[0][1].subagentSessions;
|
|
198
|
+
const maxLabel = Math.max(...sortedProjects.slice(0, 8).map(([, d]) => d.name.length));
|
|
199
|
+
for (const [, data] of sortedProjects.slice(0, 8)) {
|
|
200
|
+
const label = rpad(data.name, maxLabel);
|
|
201
|
+
console.log(` ${label} ${countBar(data.subagentSessions, maxProj)} ${String(data.subagentSessions).padStart(4)} ${humanSize(data.bytes)}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
package/index.html
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
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-subagent — Claude Code subagent usage</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 { font-size: 1.5rem; color: #ff7b72; margin-bottom: 6px; }
|
|
20
|
+
.subtitle { color: #8b949e; font-size: 0.875rem; margin-bottom: 32px; }
|
|
21
|
+
|
|
22
|
+
.drop-zone {
|
|
23
|
+
border: 2px dashed #30363d;
|
|
24
|
+
border-radius: 12px;
|
|
25
|
+
padding: 48px 64px;
|
|
26
|
+
text-align: center;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
transition: all 0.2s;
|
|
29
|
+
max-width: 480px;
|
|
30
|
+
width: 100%;
|
|
31
|
+
margin-bottom: 32px;
|
|
32
|
+
}
|
|
33
|
+
.drop-zone:hover, .drop-zone.drag-over { border-color: #ff7b72; background: rgba(255,123,114,0.05); }
|
|
34
|
+
.drop-text { color: #8b949e; font-size: 0.875rem; line-height: 1.6; }
|
|
35
|
+
.drop-text strong { color: #c9d1d9; }
|
|
36
|
+
#file-input { display: none; }
|
|
37
|
+
|
|
38
|
+
.result { display: none; max-width: 620px; width: 100%; }
|
|
39
|
+
.result.visible { display: block; }
|
|
40
|
+
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; margin-bottom: 16px; }
|
|
41
|
+
.card-title { color: #8b949e; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px; }
|
|
42
|
+
|
|
43
|
+
.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
|
44
|
+
@media (min-width: 500px) { .stats-grid { grid-template-columns: repeat(3, 1fr); } }
|
|
45
|
+
.stat-item { text-align: center; }
|
|
46
|
+
.stat-val { font-size: 1.2rem; color: #ff7b72; font-weight: 700; }
|
|
47
|
+
.stat-lbl { font-size: 0.68rem; color: #8b949e; margin-top: 2px; }
|
|
48
|
+
|
|
49
|
+
.adoption-badge {
|
|
50
|
+
text-align: center;
|
|
51
|
+
padding: 12px;
|
|
52
|
+
background: rgba(255,123,114,0.1);
|
|
53
|
+
border: 1px solid rgba(255,123,114,0.3);
|
|
54
|
+
border-radius: 6px;
|
|
55
|
+
margin-top: 16px;
|
|
56
|
+
font-size: 0.9rem;
|
|
57
|
+
color: #ff7b72;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.bar-chart { font-size: 0.78rem; }
|
|
61
|
+
.bar-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
62
|
+
.bar-label { width: 64px; text-align: right; color: #8b949e; flex-shrink: 0; }
|
|
63
|
+
.bar-label-wide { width: 160px; text-align: right; color: #c9d1d9; flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
64
|
+
.bar-track { flex: 1; height: 14px; background: #21262d; border-radius: 3px; overflow: hidden; }
|
|
65
|
+
.bar-fill { height: 100%; background: #ff7b72; border-radius: 3px; transition: width 0.5s ease; }
|
|
66
|
+
.bar-count { width: 64px; color: #8b949e; text-align: right; flex-shrink: 0; font-size: 0.72rem; }
|
|
67
|
+
|
|
68
|
+
.section-title { color: #8b949e; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; margin-top: 16px; }
|
|
69
|
+
.section-title:first-child { margin-top: 0; }
|
|
70
|
+
|
|
71
|
+
.reset-btn { margin-top: 16px; background: none; border: 1px solid #30363d; color: #8b949e; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-family: inherit; font-size: 0.8rem; display: block; width: 100%; transition: all 0.2s; }
|
|
72
|
+
.reset-btn:hover { border-color: #ff7b72; color: #ff7b72; }
|
|
73
|
+
.footer { color: #8b949e; font-size: 0.75rem; text-align: center; margin-top: 12px; }
|
|
74
|
+
.footer a { color: #ff7b72; text-decoration: none; }
|
|
75
|
+
.footer a:hover { text-decoration: underline; }
|
|
76
|
+
</style>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<h1>🤖 cc-subagent</h1>
|
|
80
|
+
<p class="subtitle">How many subagents does your Claude Code spawn?</p>
|
|
81
|
+
|
|
82
|
+
<div class="drop-zone" id="drop-zone">
|
|
83
|
+
<div style="font-size:2.5rem;margin-bottom:12px;">📁</div>
|
|
84
|
+
<div class="drop-text">
|
|
85
|
+
<strong>Drop your ~/.claude folder here</strong><br>
|
|
86
|
+
or click to select<br><br>
|
|
87
|
+
Reads file metadata only (no content).<br>
|
|
88
|
+
Nothing is uploaded.
|
|
89
|
+
</div>
|
|
90
|
+
<input type="file" id="file-input" webkitdirectory multiple accept=".jsonl">
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="result" id="result">
|
|
94
|
+
<div class="card">
|
|
95
|
+
<div class="card-title">Subagent Summary</div>
|
|
96
|
+
<div class="stats-grid">
|
|
97
|
+
<div class="stat-item"><div class="stat-val" id="stat-main">—</div><div class="stat-lbl">main sessions</div></div>
|
|
98
|
+
<div class="stat-item"><div class="stat-val" id="stat-spawners">—</div><div class="stat-lbl">spawned subagents</div></div>
|
|
99
|
+
<div class="stat-item"><div class="stat-val" id="stat-total">—</div><div class="stat-lbl">total subagents</div></div>
|
|
100
|
+
<div class="stat-item"><div class="stat-val" id="stat-avg">—</div><div class="stat-lbl">avg per spawner</div></div>
|
|
101
|
+
<div class="stat-item"><div class="stat-val" id="stat-size">—</div><div class="stat-lbl">subagent data</div></div>
|
|
102
|
+
<div class="stat-item"><div class="stat-val" id="stat-peak">—</div><div class="stat-lbl">peak (1 session)</div></div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="adoption-badge" id="adoption-badge">—</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="card">
|
|
108
|
+
<div class="section-title">Subagents per session (spawning sessions)</div>
|
|
109
|
+
<div class="bar-chart" id="dist-chart"></div>
|
|
110
|
+
<div class="section-title">By project (top 8)</div>
|
|
111
|
+
<div class="bar-chart" id="proj-chart"></div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<button class="reset-btn" id="reset-btn">← Analyze another folder</button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="footer">
|
|
118
|
+
<a href="https://github.com/yurukusa/cc-subagent" target="_blank">cc-subagent</a> ·
|
|
119
|
+
Part of <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit</a> · 106 free tools for Claude Code
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<script>
|
|
123
|
+
const dropZone = document.getElementById('drop-zone');
|
|
124
|
+
const fileInput = document.getElementById('file-input');
|
|
125
|
+
const resultEl = document.getElementById('result');
|
|
126
|
+
const resetBtn = document.getElementById('reset-btn');
|
|
127
|
+
|
|
128
|
+
dropZone.addEventListener('click', () => fileInput.click());
|
|
129
|
+
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
|
|
130
|
+
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
|
|
131
|
+
dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); processFiles(e.dataTransfer.files); });
|
|
132
|
+
fileInput.addEventListener('change', () => processFiles(fileInput.files));
|
|
133
|
+
resetBtn.addEventListener('click', () => { resultEl.classList.remove('visible'); dropZone.style.display = ''; fileInput.value = ''; });
|
|
134
|
+
|
|
135
|
+
function humanSize(bytes) {
|
|
136
|
+
if (bytes >= 1024**3) return (bytes/1024**3).toFixed(1) + ' GB';
|
|
137
|
+
if (bytes >= 1024**2) return (bytes/1024**2).toFixed(1) + ' MB';
|
|
138
|
+
if (bytes >= 1024) return (bytes/1024).toFixed(1) + ' KB';
|
|
139
|
+
return bytes + ' B';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function projectName(webkitPath) {
|
|
143
|
+
const parts = webkitPath.split('/');
|
|
144
|
+
const projIdx = parts.indexOf('projects');
|
|
145
|
+
if (projIdx >= 0 && parts[projIdx + 1]) {
|
|
146
|
+
const dir = parts[projIdx + 1];
|
|
147
|
+
const stripped = dir.replace(/^-home-[^-]+/, '').replace(/^-/, '');
|
|
148
|
+
return stripped || '~/ (home)';
|
|
149
|
+
}
|
|
150
|
+
return 'unknown';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function processFiles(files) {
|
|
154
|
+
const allFiles = Array.from(files);
|
|
155
|
+
|
|
156
|
+
// Separate main sessions and subagent files
|
|
157
|
+
const mainFiles = allFiles.filter(f => {
|
|
158
|
+
const p = f.webkitRelativePath || f.name;
|
|
159
|
+
return p.endsWith('.jsonl') && !p.includes('/subagents/');
|
|
160
|
+
});
|
|
161
|
+
const subFiles = allFiles.filter(f => {
|
|
162
|
+
const p = f.webkitRelativePath || f.name;
|
|
163
|
+
return p.endsWith('.jsonl') && p.includes('/subagents/');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!mainFiles.length && !subFiles.length) { alert('No session files found.'); return; }
|
|
167
|
+
dropZone.style.display = 'none';
|
|
168
|
+
|
|
169
|
+
// Group subagent files by parent session path
|
|
170
|
+
const byParent = {}; // parentPath → count + bytes
|
|
171
|
+
let totalSubBytes = 0;
|
|
172
|
+
for (const f of subFiles) {
|
|
173
|
+
const p = f.webkitRelativePath || f.name;
|
|
174
|
+
const parentPath = p.split('/subagents/')[0];
|
|
175
|
+
if (!byParent[parentPath]) byParent[parentPath] = { count: 0, bytes: 0 };
|
|
176
|
+
byParent[parentPath].count++;
|
|
177
|
+
byParent[parentPath].bytes += f.size;
|
|
178
|
+
totalSubBytes += f.size;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const totalMain = mainFiles.length;
|
|
182
|
+
const spawnerCount = Object.keys(byParent).length;
|
|
183
|
+
const totalSubCount = subFiles.length;
|
|
184
|
+
const adoptionRate = totalMain > 0 ? (spawnerCount / totalMain * 100) : 0;
|
|
185
|
+
const avgPerSpawner = spawnerCount > 0 ? (totalSubCount / spawnerCount).toFixed(1) : '0';
|
|
186
|
+
|
|
187
|
+
let peakCount = 0;
|
|
188
|
+
for (const d of Object.values(byParent)) {
|
|
189
|
+
if (d.count > peakCount) peakCount = d.count;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Distribution
|
|
193
|
+
const dist = { 1: 0, '2-5': 0, '6-10': 0, '11-30': 0, '31-100': 0, '100+': 0 };
|
|
194
|
+
for (const d of Object.values(byParent)) {
|
|
195
|
+
const n = d.count;
|
|
196
|
+
if (n === 1) dist[1]++;
|
|
197
|
+
else if (n <= 5) dist['2-5']++;
|
|
198
|
+
else if (n <= 10) dist['6-10']++;
|
|
199
|
+
else if (n <= 30) dist['11-30']++;
|
|
200
|
+
else if (n <= 100) dist['31-100']++;
|
|
201
|
+
else dist['100+']++;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// By project (using subagent file paths)
|
|
205
|
+
const byProj = {};
|
|
206
|
+
for (const f of subFiles) {
|
|
207
|
+
const proj = projectName(f.webkitRelativePath || f.name);
|
|
208
|
+
if (!byProj[proj]) byProj[proj] = { count: 0, bytes: 0 };
|
|
209
|
+
byProj[proj].count++;
|
|
210
|
+
byProj[proj].bytes += f.size;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Render
|
|
214
|
+
document.getElementById('stat-main').textContent = totalMain.toLocaleString();
|
|
215
|
+
document.getElementById('stat-spawners').textContent = spawnerCount.toLocaleString();
|
|
216
|
+
document.getElementById('stat-total').textContent = totalSubCount.toLocaleString();
|
|
217
|
+
document.getElementById('stat-avg').textContent = avgPerSpawner;
|
|
218
|
+
document.getElementById('stat-size').textContent = humanSize(totalSubBytes);
|
|
219
|
+
document.getElementById('stat-peak').textContent = peakCount;
|
|
220
|
+
document.getElementById('adoption-badge').textContent =
|
|
221
|
+
`${adoptionRate.toFixed(1)}% of sessions spawned subagents · ${(totalSubCount / (totalMain || 1)).toFixed(1)} subagents per session on average`;
|
|
222
|
+
|
|
223
|
+
// Distribution chart
|
|
224
|
+
const maxDist = Math.max(...Object.values(dist), 1);
|
|
225
|
+
document.getElementById('dist-chart').innerHTML = Object.entries(dist).map(([label, count]) => {
|
|
226
|
+
const pct = spawnerCount > 0 ? (count / spawnerCount * 100).toFixed(1) : '0.0';
|
|
227
|
+
return `<div class="bar-row">
|
|
228
|
+
<div class="bar-label">${label}</div>
|
|
229
|
+
<div class="bar-track"><div class="bar-fill" style="width:${(count/maxDist*100).toFixed(1)}%"></div></div>
|
|
230
|
+
<div class="bar-count">${count} (${pct}%)</div>
|
|
231
|
+
</div>`;
|
|
232
|
+
}).join('');
|
|
233
|
+
|
|
234
|
+
// Project chart
|
|
235
|
+
const projs = Object.entries(byProj).sort((a,b) => b[1].count - a[1].count).slice(0, 8);
|
|
236
|
+
const maxProj = projs[0]?.[1].count || 1;
|
|
237
|
+
document.getElementById('proj-chart').innerHTML = projs.map(([name, d]) =>
|
|
238
|
+
`<div class="bar-row">
|
|
239
|
+
<div class="bar-label-wide">${name}</div>
|
|
240
|
+
<div class="bar-track"><div class="bar-fill" style="width:${(d.count/maxProj*100).toFixed(1)}%"></div></div>
|
|
241
|
+
<div class="bar-count">${d.count} / ${humanSize(d.bytes)}</div>
|
|
242
|
+
</div>`
|
|
243
|
+
).join('');
|
|
244
|
+
|
|
245
|
+
resultEl.classList.add('visible');
|
|
246
|
+
}
|
|
247
|
+
</script>
|
|
248
|
+
</body>
|
|
249
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-subagent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "How many subagents does your Claude Code spawn? Session count, adoption rate, and per-project breakdown.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc-subagent": "./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
|
+
"agents"
|
|
19
|
+
],
|
|
20
|
+
"author": "yurukusa",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
}
|
|
25
|
+
}
|