cc-compact 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 +65 -0
- package/cli.mjs +274 -0
- package/index.html +299 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# cc-compact
|
|
2
|
+
|
|
3
|
+
How often does Claude Code hit the context limit? Shows compaction frequency, pre-compaction token counts, and trigger type breakdown.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
cc-compact — Claude Code context compaction stats
|
|
7
|
+
|
|
8
|
+
Total sessions: 755
|
|
9
|
+
Sessions compacted: 137 (18.1% of sessions)
|
|
10
|
+
Total compactions: 912
|
|
11
|
+
Avg per compacted: 6.7 compactions/session
|
|
12
|
+
Trigger: 883 auto / 29 manual
|
|
13
|
+
|
|
14
|
+
Pre-compaction context size:
|
|
15
|
+
Min: 71.1K tokens
|
|
16
|
+
Avg: 163.9K tokens
|
|
17
|
+
Median: 167.3K tokens
|
|
18
|
+
Max: 190.3K tokens
|
|
19
|
+
|
|
20
|
+
────────────────────────────────────────────────────────
|
|
21
|
+
Context size at compaction
|
|
22
|
+
|
|
23
|
+
<100K ░░░░░░░░░░░░░░░░░░░░░░░░ 9 (1.0%)
|
|
24
|
+
100–150K ░░░░░░░░░░░░░░░░░░░░░░░░ 9 (1.0%)
|
|
25
|
+
150–175K ████████████████████████ 870 (95.4%)
|
|
26
|
+
175K+ █░░░░░░░░░░░░░░░░░░░░░░░ 24 (2.6%)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx cc-compact # Compaction frequency and token stats
|
|
33
|
+
npx cc-compact --json # JSON output
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## What it shows
|
|
37
|
+
|
|
38
|
+
- **Compaction rate** — percentage of sessions that hit the context limit
|
|
39
|
+
- **Total compactions** — how many times Claude Code auto-compacted
|
|
40
|
+
- **Trigger breakdown** — auto (limit hit) vs manual (/compact command)
|
|
41
|
+
- **Pre-compaction size** — context size at the moment of compaction
|
|
42
|
+
- **Size distribution** — where in the token range compactions happen
|
|
43
|
+
- **Monthly trend** — compaction frequency over time
|
|
44
|
+
- **By project** — which projects burn through context fastest
|
|
45
|
+
|
|
46
|
+
## What compaction means
|
|
47
|
+
|
|
48
|
+
When Claude Code's context window fills up, it automatically compacts (summarizes) the conversation to free up space. This is called an "auto" compaction.
|
|
49
|
+
|
|
50
|
+
- **Auto compaction** (96.8% of cases): hit the limit, context summarized automatically
|
|
51
|
+
- **Manual compaction** (3.2% of cases): user typed `/compact` before hitting the limit
|
|
52
|
+
|
|
53
|
+
95.4% of auto-compactions happen between 150K–175K tokens — just below the typical 200K context limit.
|
|
54
|
+
|
|
55
|
+
## Privacy
|
|
56
|
+
|
|
57
|
+
Reads session files looking for compaction event markers. No content is transmitted. Everything runs locally.
|
|
58
|
+
|
|
59
|
+
## Browser version
|
|
60
|
+
|
|
61
|
+
Drop your `~/.claude` folder into [cc-compact on the web](https://yurukusa.github.io/cc-compact/) — no install required.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
Part of [cc-toolkit](https://yurukusa.github.io/cc-toolkit/) — 87 free tools for Claude Code
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cc-compact — How often does Claude Code compact your context?
|
|
4
|
+
* Shows compaction frequency, pre-compaction token counts, and trigger types.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readdirSync, statSync, createReadStream } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { createInterface } from 'readline';
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const jsonMode = args.includes('--json');
|
|
14
|
+
const showHelp = args.includes('--help') || args.includes('-h');
|
|
15
|
+
|
|
16
|
+
if (showHelp) {
|
|
17
|
+
console.log(`cc-compact — How often does Claude Code compact your context?
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
npx cc-compact # Compaction frequency and token statistics
|
|
21
|
+
npx cc-compact --json # JSON output
|
|
22
|
+
`);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const claudeDir = join(homedir(), '.claude', 'projects');
|
|
27
|
+
|
|
28
|
+
function getISOMonth(date) {
|
|
29
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function projectName(dirName) {
|
|
33
|
+
const stripped = dirName.replace(/^-home-[^-]+/, '').replace(/^-/, '');
|
|
34
|
+
return stripped || '~/ (home)';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function humanTok(n) {
|
|
38
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
39
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
|
40
|
+
return n.toString();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Accumulators
|
|
44
|
+
let totalSessions = 0;
|
|
45
|
+
let sessionsWithCompaction = 0;
|
|
46
|
+
let totalCompactions = 0;
|
|
47
|
+
let autoCompactions = 0;
|
|
48
|
+
let manualCompactions = 0;
|
|
49
|
+
const preTokens = []; // all pre-compaction token counts
|
|
50
|
+
const byMonth = {}; // { month: { compactions, sessions, preTokenSum } }
|
|
51
|
+
const byProject = {}; // { projDir: { compactions, sessions, preTokenSum, name } }
|
|
52
|
+
|
|
53
|
+
let projectDirs;
|
|
54
|
+
try {
|
|
55
|
+
projectDirs = readdirSync(claudeDir);
|
|
56
|
+
} catch {
|
|
57
|
+
console.error(`Cannot read ${claudeDir}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function processFile(filePath, projDir) {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });
|
|
64
|
+
const mtime = new Date(statSync(filePath).mtime);
|
|
65
|
+
const month = getISOMonth(mtime);
|
|
66
|
+
const projLabel = projectName(projDir);
|
|
67
|
+
|
|
68
|
+
let fileCompactions = 0;
|
|
69
|
+
let filePreTokens = [];
|
|
70
|
+
|
|
71
|
+
rl.on('line', (line) => {
|
|
72
|
+
if (!line.includes('"compact_boundary"')) return;
|
|
73
|
+
|
|
74
|
+
const mPre = line.match(/"preTokens":(\d+)/);
|
|
75
|
+
const mTrigger = line.match(/"trigger":"([^"]+)"/);
|
|
76
|
+
|
|
77
|
+
const pre = mPre ? parseInt(mPre[1], 10) : 0;
|
|
78
|
+
const trigger = mTrigger ? mTrigger[1] : 'auto';
|
|
79
|
+
|
|
80
|
+
fileCompactions++;
|
|
81
|
+
if (trigger === 'manual') manualCompactions++;
|
|
82
|
+
else autoCompactions++;
|
|
83
|
+
if (pre > 0) {
|
|
84
|
+
preTokens.push(pre);
|
|
85
|
+
filePreTokens.push(pre);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
rl.on('close', () => {
|
|
90
|
+
totalSessions++;
|
|
91
|
+
if (fileCompactions > 0) {
|
|
92
|
+
sessionsWithCompaction++;
|
|
93
|
+
totalCompactions += fileCompactions;
|
|
94
|
+
|
|
95
|
+
if (!byMonth[month]) byMonth[month] = { compactions: 0, sessions: 0, preTokenSum: 0, preTokenCount: 0 };
|
|
96
|
+
byMonth[month].compactions += fileCompactions;
|
|
97
|
+
byMonth[month].sessions++;
|
|
98
|
+
byMonth[month].preTokenSum += filePreTokens.reduce((a, b) => a + b, 0);
|
|
99
|
+
byMonth[month].preTokenCount += filePreTokens.length;
|
|
100
|
+
|
|
101
|
+
if (!byProject[projDir]) byProject[projDir] = { compactions: 0, sessions: 0, preTokenSum: 0, preTokenCount: 0, name: projLabel };
|
|
102
|
+
byProject[projDir].compactions += fileCompactions;
|
|
103
|
+
byProject[projDir].sessions++;
|
|
104
|
+
byProject[projDir].preTokenSum += filePreTokens.reduce((a, b) => a + b, 0);
|
|
105
|
+
byProject[projDir].preTokenCount += filePreTokens.length;
|
|
106
|
+
} else {
|
|
107
|
+
if (!byMonth[month]) byMonth[month] = { compactions: 0, sessions: 0, preTokenSum: 0, preTokenCount: 0 };
|
|
108
|
+
if (!byProject[projDir]) byProject[projDir] = { compactions: 0, sessions: 0, preTokenSum: 0, preTokenCount: 0, name: projLabel };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
resolve();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Collect files
|
|
117
|
+
const filesToProcess = [];
|
|
118
|
+
for (const projDir of projectDirs) {
|
|
119
|
+
const projPath = join(claudeDir, projDir);
|
|
120
|
+
let stat;
|
|
121
|
+
try {
|
|
122
|
+
stat = statSync(projPath);
|
|
123
|
+
if (!stat.isDirectory()) continue;
|
|
124
|
+
} catch { continue; }
|
|
125
|
+
|
|
126
|
+
let entries;
|
|
127
|
+
try { entries = readdirSync(projPath); } catch { continue; }
|
|
128
|
+
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
if (!entry.endsWith('.jsonl')) continue;
|
|
131
|
+
if (entry.includes('subagent')) continue;
|
|
132
|
+
const filePath = join(projPath, entry);
|
|
133
|
+
try {
|
|
134
|
+
if (!statSync(filePath).isFile()) continue;
|
|
135
|
+
} catch { continue; }
|
|
136
|
+
filesToProcess.push({ filePath, projDir });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!filesToProcess.length) {
|
|
141
|
+
console.error('No session files found.');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!jsonMode) process.stderr.write(`Scanning ${filesToProcess.length} sessions...\r`);
|
|
146
|
+
|
|
147
|
+
const CONCURRENCY = 16;
|
|
148
|
+
for (let i = 0; i < filesToProcess.length; i += CONCURRENCY) {
|
|
149
|
+
const batch = filesToProcess.slice(i, i + CONCURRENCY);
|
|
150
|
+
await Promise.all(batch.map(({ filePath, projDir }) => processFile(filePath, projDir)));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!jsonMode) process.stderr.write(' '.repeat(40) + '\r');
|
|
154
|
+
|
|
155
|
+
// Compute stats
|
|
156
|
+
preTokens.sort((a, b) => a - b);
|
|
157
|
+
const medianPre = preTokens.length ? preTokens[Math.floor(preTokens.length / 2)] : 0;
|
|
158
|
+
const avgPre = preTokens.length ? Math.round(preTokens.reduce((a, b) => a + b, 0) / preTokens.length) : 0;
|
|
159
|
+
const maxPre = preTokens.length ? preTokens[preTokens.length - 1] : 0;
|
|
160
|
+
const minPre = preTokens.length ? preTokens[0] : 0;
|
|
161
|
+
|
|
162
|
+
// Distribution buckets: < 100K, 100K–150K, 150K–175K, 175K+
|
|
163
|
+
const buckets = {
|
|
164
|
+
'<100K': preTokens.filter(t => t < 100000).length,
|
|
165
|
+
'100–150K': preTokens.filter(t => t >= 100000 && t < 150000).length,
|
|
166
|
+
'150–175K': preTokens.filter(t => t >= 150000 && t < 175000).length,
|
|
167
|
+
'175K+': preTokens.filter(t => t >= 175000).length,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (jsonMode) {
|
|
171
|
+
const sortedMonths = Object.entries(byMonth).sort((a, b) => a[0].localeCompare(b[0]));
|
|
172
|
+
const sortedProjects = Object.entries(byProject)
|
|
173
|
+
.filter(([, d]) => d.compactions > 0)
|
|
174
|
+
.sort((a, b) => b[1].compactions - a[1].compactions);
|
|
175
|
+
console.log(JSON.stringify({
|
|
176
|
+
total_sessions: totalSessions,
|
|
177
|
+
sessions_with_compaction: sessionsWithCompaction,
|
|
178
|
+
compaction_rate_pct: Math.round(sessionsWithCompaction / totalSessions * 1000) / 10,
|
|
179
|
+
total_compactions: totalCompactions,
|
|
180
|
+
auto_compactions: autoCompactions,
|
|
181
|
+
manual_compactions: manualCompactions,
|
|
182
|
+
pre_tokens: {
|
|
183
|
+
min: minPre,
|
|
184
|
+
median: medianPre,
|
|
185
|
+
avg: avgPre,
|
|
186
|
+
max: maxPre,
|
|
187
|
+
},
|
|
188
|
+
token_distribution: buckets,
|
|
189
|
+
by_month: Object.fromEntries(sortedMonths.map(([m, d]) => [m, {
|
|
190
|
+
compactions: d.compactions,
|
|
191
|
+
sessions_compacted: d.sessions,
|
|
192
|
+
avg_pre_tokens: d.preTokenCount > 0 ? Math.round(d.preTokenSum / d.preTokenCount) : 0,
|
|
193
|
+
}])),
|
|
194
|
+
by_project: sortedProjects.slice(0, 10).map(([, d]) => ({
|
|
195
|
+
project: d.name,
|
|
196
|
+
compactions: d.compactions,
|
|
197
|
+
sessions_compacted: d.sessions,
|
|
198
|
+
avg_pre_tokens: d.preTokenCount > 0 ? Math.round(d.preTokenSum / d.preTokenCount) : 0,
|
|
199
|
+
})),
|
|
200
|
+
}, null, 2));
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Terminal display
|
|
205
|
+
const BAR_WIDTH = 24;
|
|
206
|
+
const now = new Date();
|
|
207
|
+
const curMonth = getISOMonth(now);
|
|
208
|
+
|
|
209
|
+
function rpad(str, len) {
|
|
210
|
+
return str + ' '.repeat(Math.max(0, len - str.length));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function countBar(n, max) {
|
|
214
|
+
const filled = max > 0 ? Math.round((n / max) * BAR_WIDTH) : 0;
|
|
215
|
+
return '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const compactionRatePct = (sessionsWithCompaction / totalSessions * 100).toFixed(1);
|
|
219
|
+
const avgPerCompacted = sessionsWithCompaction > 0 ? (totalCompactions / sessionsWithCompaction).toFixed(1) : '0';
|
|
220
|
+
|
|
221
|
+
console.log('cc-compact — Claude Code context compaction stats\n');
|
|
222
|
+
|
|
223
|
+
console.log(` Total sessions: ${totalSessions.toLocaleString()}`);
|
|
224
|
+
console.log(` Sessions compacted: ${sessionsWithCompaction.toLocaleString()} (${compactionRatePct}% of sessions)`);
|
|
225
|
+
console.log(` Total compactions: ${totalCompactions.toLocaleString()}`);
|
|
226
|
+
console.log(` Avg per compacted: ${avgPerCompacted} compactions/session`);
|
|
227
|
+
console.log(` Trigger: ${autoCompactions} auto / ${manualCompactions} manual`);
|
|
228
|
+
|
|
229
|
+
console.log();
|
|
230
|
+
console.log(' Pre-compaction context size:');
|
|
231
|
+
console.log(` Min: ${humanTok(minPre)} tokens`);
|
|
232
|
+
console.log(` Avg: ${humanTok(avgPre)} tokens`);
|
|
233
|
+
console.log(` Median: ${humanTok(medianPre)} tokens`);
|
|
234
|
+
console.log(` Max: ${humanTok(maxPre)} tokens`);
|
|
235
|
+
|
|
236
|
+
// Distribution of pre-compaction sizes
|
|
237
|
+
console.log('\n' + '─'.repeat(56));
|
|
238
|
+
console.log(' Context size at compaction\n');
|
|
239
|
+
const maxBucket = Math.max(...Object.values(buckets));
|
|
240
|
+
for (const [label, count] of Object.entries(buckets)) {
|
|
241
|
+
const pct = preTokens.length > 0 ? (count / preTokens.length * 100).toFixed(1) : '0.0';
|
|
242
|
+
console.log(` ${label.padEnd(10)} ${countBar(count, maxBucket)} ${String(count).padStart(4)} (${pct}%)`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Monthly chart
|
|
246
|
+
const sortedMonths = Object.entries(byMonth)
|
|
247
|
+
.filter(([, d]) => d.compactions > 0)
|
|
248
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
249
|
+
.slice(-12);
|
|
250
|
+
if (sortedMonths.length) {
|
|
251
|
+
console.log('\n' + '─'.repeat(56));
|
|
252
|
+
console.log(' Monthly compactions\n');
|
|
253
|
+
const maxMonthCount = Math.max(...sortedMonths.map(([, d]) => d.compactions));
|
|
254
|
+
for (const [month, data] of sortedMonths) {
|
|
255
|
+
const tag = month === curMonth ? ' (in progress)' : '';
|
|
256
|
+
console.log(` ${month} ${countBar(data.compactions, maxMonthCount)} ${String(data.compactions).padStart(4)}${tag}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Project chart (top 8)
|
|
261
|
+
const sortedProjects = Object.entries(byProject)
|
|
262
|
+
.filter(([, d]) => d.compactions > 0)
|
|
263
|
+
.sort((a, b) => b[1].compactions - a[1].compactions)
|
|
264
|
+
.slice(0, 8);
|
|
265
|
+
if (sortedProjects.length) {
|
|
266
|
+
console.log('\n' + '─'.repeat(56));
|
|
267
|
+
console.log(' By project (top 8)\n');
|
|
268
|
+
const maxProjCount = sortedProjects[0][1].compactions;
|
|
269
|
+
const maxLabel = Math.max(...sortedProjects.map(([, d]) => d.name.length));
|
|
270
|
+
for (const [, data] of sortedProjects) {
|
|
271
|
+
const label = rpad(data.name, maxLabel);
|
|
272
|
+
console.log(` ${label} ${countBar(data.compactions, maxProjCount)} ${String(data.compactions).padStart(4)}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
package/index.html
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
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-compact — Claude Code compaction stats</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: #f7c948; 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: #f7c948; background: rgba(247,201,72,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
|
+
.progress { display: none; max-width: 480px; width: 100%; text-align: center; margin-bottom: 24px; }
|
|
39
|
+
.progress.visible { display: block; }
|
|
40
|
+
.progress-bar-track { height: 6px; background: #21262d; border-radius: 3px; overflow: hidden; margin: 12px 0; }
|
|
41
|
+
.progress-bar-fill { height: 100%; background: #f7c948; border-radius: 3px; transition: width 0.1s; }
|
|
42
|
+
.progress-label { color: #8b949e; font-size: 0.78rem; }
|
|
43
|
+
|
|
44
|
+
.result { display: none; max-width: 620px; width: 100%; }
|
|
45
|
+
.result.visible { display: block; }
|
|
46
|
+
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; margin-bottom: 16px; }
|
|
47
|
+
.card-title { color: #8b949e; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px; }
|
|
48
|
+
|
|
49
|
+
.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 16px; }
|
|
50
|
+
@media (min-width: 500px) { .stats-grid { grid-template-columns: repeat(3, 1fr); } }
|
|
51
|
+
.stat-item { text-align: center; }
|
|
52
|
+
.stat-val { font-size: 1.2rem; color: #f7c948; font-weight: 700; }
|
|
53
|
+
.stat-lbl { font-size: 0.68rem; color: #8b949e; margin-top: 2px; }
|
|
54
|
+
|
|
55
|
+
.trigger-row { display: flex; justify-content: center; gap: 32px; padding: 12px; background: #21262d; border-radius: 6px; margin-top: 12px; }
|
|
56
|
+
.trigger-item { text-align: center; }
|
|
57
|
+
.trigger-val { font-size: 1.1rem; font-weight: 700; }
|
|
58
|
+
.trigger-auto { color: #ff7b72; }
|
|
59
|
+
.trigger-manual { color: #58a6ff; }
|
|
60
|
+
.trigger-lbl { font-size: 0.68rem; color: #8b949e; margin-top: 2px; }
|
|
61
|
+
|
|
62
|
+
.tok-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; margin-top: 12px; }
|
|
63
|
+
@media (min-width: 500px) { .tok-stats { grid-template-columns: repeat(4, 1fr); } }
|
|
64
|
+
.tok-item { text-align: center; padding: 8px; background: #21262d; border-radius: 4px; }
|
|
65
|
+
.tok-val { font-size: 0.9rem; color: #f7c948; font-weight: 600; }
|
|
66
|
+
.tok-lbl { font-size: 0.65rem; color: #8b949e; margin-top: 2px; }
|
|
67
|
+
|
|
68
|
+
.bar-chart { font-size: 0.78rem; }
|
|
69
|
+
.bar-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
70
|
+
.bar-label { width: 72px; text-align: right; color: #8b949e; flex-shrink: 0; }
|
|
71
|
+
.bar-label-wide { width: 150px; text-align: right; color: #c9d1d9; flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
72
|
+
.bar-track { flex: 1; height: 14px; background: #21262d; border-radius: 3px; overflow: hidden; }
|
|
73
|
+
.bar-fill { height: 100%; background: #f7c948; border-radius: 3px; transition: width 0.5s ease; }
|
|
74
|
+
.bar-count { width: 56px; color: #8b949e; text-align: right; flex-shrink: 0; font-size: 0.72rem; }
|
|
75
|
+
|
|
76
|
+
.section-title { color: #8b949e; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; margin-top: 16px; }
|
|
77
|
+
.section-title:first-child { margin-top: 0; }
|
|
78
|
+
|
|
79
|
+
.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; }
|
|
80
|
+
.reset-btn:hover { border-color: #f7c948; color: #f7c948; }
|
|
81
|
+
.footer { color: #8b949e; font-size: 0.75rem; text-align: center; margin-top: 12px; }
|
|
82
|
+
.footer a { color: #f7c948; text-decoration: none; }
|
|
83
|
+
.footer a:hover { text-decoration: underline; }
|
|
84
|
+
</style>
|
|
85
|
+
</head>
|
|
86
|
+
<body>
|
|
87
|
+
<h1>🗜️ cc-compact</h1>
|
|
88
|
+
<p class="subtitle">How often does Claude Code hit the context limit?</p>
|
|
89
|
+
|
|
90
|
+
<div class="drop-zone" id="drop-zone">
|
|
91
|
+
<div style="font-size:2.5rem;margin-bottom:12px;">📁</div>
|
|
92
|
+
<div class="drop-text">
|
|
93
|
+
<strong>Drop your ~/.claude folder here</strong><br>
|
|
94
|
+
or click to select<br><br>
|
|
95
|
+
Reads session files for compaction events.<br>
|
|
96
|
+
Nothing is uploaded. Processing is fast.
|
|
97
|
+
</div>
|
|
98
|
+
<input type="file" id="file-input" webkitdirectory multiple accept=".jsonl">
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="progress" id="progress">
|
|
102
|
+
<div class="progress-label" id="progress-label">Processing sessions...</div>
|
|
103
|
+
<div class="progress-bar-track"><div class="progress-bar-fill" id="progress-fill" style="width:0%"></div></div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="result" id="result">
|
|
107
|
+
<div class="card">
|
|
108
|
+
<div class="card-title">Compaction Summary</div>
|
|
109
|
+
<div class="stats-grid">
|
|
110
|
+
<div class="stat-item"><div class="stat-val" id="stat-sessions">—</div><div class="stat-lbl">sessions</div></div>
|
|
111
|
+
<div class="stat-item"><div class="stat-val" id="stat-rate">—%</div><div class="stat-lbl">hit context limit</div></div>
|
|
112
|
+
<div class="stat-item"><div class="stat-val" id="stat-total">—</div><div class="stat-lbl">total compactions</div></div>
|
|
113
|
+
<div class="stat-item"><div class="stat-val" id="stat-avg">—</div><div class="stat-lbl">per compacted session</div></div>
|
|
114
|
+
<div class="stat-item"><div class="stat-val" id="stat-median">—</div><div class="stat-lbl">median context size</div></div>
|
|
115
|
+
<div class="stat-item"><div class="stat-val" id="stat-max">—</div><div class="stat-lbl">max context size</div></div>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="trigger-row">
|
|
118
|
+
<div class="trigger-item"><div class="trigger-val trigger-auto" id="trig-auto">—</div><div class="trigger-lbl">🔴 auto (limit hit)</div></div>
|
|
119
|
+
<div class="trigger-item"><div class="trigger-val trigger-manual" id="trig-manual">—</div><div class="trigger-lbl">🔵 manual (/compact)</div></div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div class="card">
|
|
124
|
+
<div class="section-title">Context size at compaction</div>
|
|
125
|
+
<div class="bar-chart" id="bucket-chart"></div>
|
|
126
|
+
<div class="section-title">Monthly compactions</div>
|
|
127
|
+
<div class="bar-chart" id="month-chart"></div>
|
|
128
|
+
<div class="section-title">By project (top 8)</div>
|
|
129
|
+
<div class="bar-chart" id="proj-chart"></div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<button class="reset-btn" id="reset-btn">← Analyze another folder</button>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div class="footer">
|
|
136
|
+
<a href="https://github.com/yurukusa/cc-compact" target="_blank">cc-compact</a> ·
|
|
137
|
+
Part of <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit</a> · 106 free tools for Claude Code
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<script>
|
|
141
|
+
const dropZone = document.getElementById('drop-zone');
|
|
142
|
+
const fileInput = document.getElementById('file-input');
|
|
143
|
+
const resultEl = document.getElementById('result');
|
|
144
|
+
const progressEl = document.getElementById('progress');
|
|
145
|
+
const progressFill = document.getElementById('progress-fill');
|
|
146
|
+
const progressLabel = document.getElementById('progress-label');
|
|
147
|
+
const resetBtn = document.getElementById('reset-btn');
|
|
148
|
+
|
|
149
|
+
dropZone.addEventListener('click', () => fileInput.click());
|
|
150
|
+
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
|
|
151
|
+
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
|
|
152
|
+
dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); processFiles(e.dataTransfer.files); });
|
|
153
|
+
fileInput.addEventListener('change', () => processFiles(fileInput.files));
|
|
154
|
+
resetBtn.addEventListener('click', () => { resultEl.classList.remove('visible'); progressEl.classList.remove('visible'); dropZone.style.display = ''; fileInput.value = ''; });
|
|
155
|
+
|
|
156
|
+
function humanTok(n) {
|
|
157
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
158
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
|
159
|
+
return n.toString();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function projectName(webkitPath) {
|
|
163
|
+
const parts = webkitPath.split('/');
|
|
164
|
+
const projIdx = parts.indexOf('projects');
|
|
165
|
+
if (projIdx >= 0 && parts[projIdx + 1]) {
|
|
166
|
+
const dir = parts[projIdx + 1];
|
|
167
|
+
const stripped = dir.replace(/^-home-[^-]+/, '').replace(/^-/, '');
|
|
168
|
+
return stripped || '~/ (home)';
|
|
169
|
+
}
|
|
170
|
+
return 'unknown';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getMonth(file) {
|
|
174
|
+
const d = new Date(file.lastModified);
|
|
175
|
+
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function parseFile(file) {
|
|
179
|
+
const text = await file.text();
|
|
180
|
+
const lines = text.split('\n');
|
|
181
|
+
let compactions = 0, auto = 0, manual = 0;
|
|
182
|
+
const filePreTokens = [];
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
if (!line.includes('"compact_boundary"')) continue;
|
|
185
|
+
const mPre = line.match(/"preTokens":(\d+)/);
|
|
186
|
+
const mTrig = line.match(/"trigger":"([^"]+)"/);
|
|
187
|
+
const pre = mPre ? parseInt(mPre[1], 10) : 0;
|
|
188
|
+
const trig = mTrig ? mTrig[1] : 'auto';
|
|
189
|
+
compactions++;
|
|
190
|
+
if (trig === 'manual') manual++;
|
|
191
|
+
else auto++;
|
|
192
|
+
if (pre > 0) filePreTokens.push(pre);
|
|
193
|
+
}
|
|
194
|
+
return { compactions, auto, manual, preTokens: filePreTokens };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function processFiles(files) {
|
|
198
|
+
const jsonlFiles = Array.from(files).filter(f => {
|
|
199
|
+
const p = f.webkitRelativePath || f.name;
|
|
200
|
+
return p.endsWith('.jsonl') && !p.includes('/subagents/');
|
|
201
|
+
});
|
|
202
|
+
if (!jsonlFiles.length) { alert('No session files found.'); return; }
|
|
203
|
+
|
|
204
|
+
dropZone.style.display = 'none';
|
|
205
|
+
progressEl.classList.add('visible');
|
|
206
|
+
|
|
207
|
+
let totalSessions = 0, sessionsCompacted = 0, totalCompactions = 0, totalAuto = 0, totalManual = 0;
|
|
208
|
+
const allPreTokens = [];
|
|
209
|
+
const byMonth = {}, byProj = {};
|
|
210
|
+
|
|
211
|
+
for (let i = 0; i < jsonlFiles.length; i++) {
|
|
212
|
+
const f = jsonlFiles[i];
|
|
213
|
+
progressLabel.textContent = `Processing ${i + 1} / ${jsonlFiles.length}...`;
|
|
214
|
+
progressFill.style.width = ((i + 1) / jsonlFiles.length * 100) + '%';
|
|
215
|
+
|
|
216
|
+
let data;
|
|
217
|
+
try { data = await parseFile(f); } catch { continue; }
|
|
218
|
+
|
|
219
|
+
totalSessions++;
|
|
220
|
+
if (data.compactions > 0) {
|
|
221
|
+
sessionsCompacted++;
|
|
222
|
+
totalCompactions += data.compactions;
|
|
223
|
+
totalAuto += data.auto;
|
|
224
|
+
totalManual += data.manual;
|
|
225
|
+
allPreTokens.push(...data.preTokens);
|
|
226
|
+
|
|
227
|
+
const month = getMonth(f);
|
|
228
|
+
if (!byMonth[month]) byMonth[month] = 0;
|
|
229
|
+
byMonth[month] += data.compactions;
|
|
230
|
+
|
|
231
|
+
const proj = projectName(f.webkitRelativePath || f.name);
|
|
232
|
+
if (!byProj[proj]) byProj[proj] = 0;
|
|
233
|
+
byProj[proj] += data.compactions;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (i % 20 === 0) await new Promise(r => setTimeout(r, 0));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
progressEl.classList.remove('visible');
|
|
240
|
+
|
|
241
|
+
allPreTokens.sort((a, b) => a - b);
|
|
242
|
+
const median = allPreTokens.length ? allPreTokens[Math.floor(allPreTokens.length / 2)] : 0;
|
|
243
|
+
const max = allPreTokens.length ? allPreTokens[allPreTokens.length - 1] : 0;
|
|
244
|
+
const rate = totalSessions > 0 ? (sessionsCompacted / totalSessions * 100) : 0;
|
|
245
|
+
const avgPerCompacted = sessionsCompacted > 0 ? (totalCompactions / sessionsCompacted).toFixed(1) : '0';
|
|
246
|
+
|
|
247
|
+
document.getElementById('stat-sessions').textContent = totalSessions.toLocaleString();
|
|
248
|
+
document.getElementById('stat-rate').textContent = rate.toFixed(1) + '%';
|
|
249
|
+
document.getElementById('stat-total').textContent = totalCompactions.toLocaleString();
|
|
250
|
+
document.getElementById('stat-avg').textContent = avgPerCompacted;
|
|
251
|
+
document.getElementById('stat-median').textContent = humanTok(median);
|
|
252
|
+
document.getElementById('stat-max').textContent = humanTok(max);
|
|
253
|
+
document.getElementById('trig-auto').textContent = totalAuto.toLocaleString();
|
|
254
|
+
document.getElementById('trig-manual').textContent = totalManual.toLocaleString();
|
|
255
|
+
|
|
256
|
+
// Bucket chart
|
|
257
|
+
const buckets = [
|
|
258
|
+
['<100K', allPreTokens.filter(t => t < 100000).length],
|
|
259
|
+
['100–150K', allPreTokens.filter(t => t >= 100000 && t < 150000).length],
|
|
260
|
+
['150–175K', allPreTokens.filter(t => t >= 150000 && t < 175000).length],
|
|
261
|
+
['175K+', allPreTokens.filter(t => t >= 175000).length],
|
|
262
|
+
];
|
|
263
|
+
const maxBucket = Math.max(...buckets.map(([,n])=>n), 1);
|
|
264
|
+
document.getElementById('bucket-chart').innerHTML = buckets.map(([label, count]) => {
|
|
265
|
+
const pct = allPreTokens.length ? (count / allPreTokens.length * 100).toFixed(1) : '0.0';
|
|
266
|
+
return `<div class="bar-row">
|
|
267
|
+
<div class="bar-label">${label}</div>
|
|
268
|
+
<div class="bar-track"><div class="bar-fill" style="width:${(count/maxBucket*100).toFixed(1)}%"></div></div>
|
|
269
|
+
<div class="bar-count">${count} (${pct}%)</div>
|
|
270
|
+
</div>`;
|
|
271
|
+
}).join('');
|
|
272
|
+
|
|
273
|
+
// Monthly chart
|
|
274
|
+
const months = Object.entries(byMonth).sort((a,b) => a[0].localeCompare(b[0])).slice(-12);
|
|
275
|
+
const maxMonth = Math.max(...months.map(([,n])=>n), 1);
|
|
276
|
+
document.getElementById('month-chart').innerHTML = months.map(([m, n]) =>
|
|
277
|
+
`<div class="bar-row">
|
|
278
|
+
<div class="bar-label">${m}</div>
|
|
279
|
+
<div class="bar-track"><div class="bar-fill" style="width:${(n/maxMonth*100).toFixed(1)}%"></div></div>
|
|
280
|
+
<div class="bar-count">${n}</div>
|
|
281
|
+
</div>`
|
|
282
|
+
).join('');
|
|
283
|
+
|
|
284
|
+
// Project chart
|
|
285
|
+
const projs = Object.entries(byProj).sort((a,b) => b[1]-a[1]).slice(0, 8);
|
|
286
|
+
const maxProj = projs[0]?.[1] || 1;
|
|
287
|
+
document.getElementById('proj-chart').innerHTML = projs.map(([name, n]) =>
|
|
288
|
+
`<div class="bar-row">
|
|
289
|
+
<div class="bar-label-wide">${name}</div>
|
|
290
|
+
<div class="bar-track"><div class="bar-fill" style="width:${(n/maxProj*100).toFixed(1)}%"></div></div>
|
|
291
|
+
<div class="bar-count">${n}</div>
|
|
292
|
+
</div>`
|
|
293
|
+
).join('');
|
|
294
|
+
|
|
295
|
+
resultEl.classList.add('visible');
|
|
296
|
+
}
|
|
297
|
+
</script>
|
|
298
|
+
</body>
|
|
299
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-compact",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "How often does Claude Code compact your context? Compaction frequency, pre-compaction token counts, and trigger types.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc-compact": "./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
|
+
"context"
|
|
19
|
+
],
|
|
20
|
+
"author": "yurukusa",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
}
|
|
25
|
+
}
|