cc-size 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 +58 -0
- package/cli.mjs +211 -0
- package/index.html +235 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# cc-size
|
|
2
|
+
|
|
3
|
+
How much conversation history have you accumulated? Shows total disk usage and growth rate of your Claude Code sessions.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
cc-size — Your Claude Code conversation history
|
|
7
|
+
|
|
8
|
+
Total size: 6.1 GB
|
|
9
|
+
Total files: 755 sessions
|
|
10
|
+
Date range: 2026-01-10 → 2026-03-01
|
|
11
|
+
Daily growth: ~81.8 MB/day (last 30 days)
|
|
12
|
+
Largest file: 1.0 GB
|
|
13
|
+
At this rate: 10 GB by ~2026-04
|
|
14
|
+
|
|
15
|
+
──────────────────────────────────────────────────────
|
|
16
|
+
Monthly growth
|
|
17
|
+
|
|
18
|
+
2026-01 ████████████████████████ 3.7 GB
|
|
19
|
+
2026-02 ██████████████░░░░░░░░░░ 2.2 GB
|
|
20
|
+
2026-03 █░░░░░░░░░░░░░░░░░░░░░░░ 231.1 MB (in progress)
|
|
21
|
+
|
|
22
|
+
──────────────────────────────────────────────────────
|
|
23
|
+
By project (top 8)
|
|
24
|
+
|
|
25
|
+
~/ (home) ████████████████████████ 5.7 GB
|
|
26
|
+
projects-cc-loop █░░░░░░░░░░░░░░░░░░░░░░░ 224.8 MB
|
|
27
|
+
draemorth ░░░░░░░░░░░░░░░░░░░░░░░░ 59.0 MB
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx cc-size # Total history size and growth
|
|
34
|
+
npx cc-size --all # Include subagent session files
|
|
35
|
+
npx cc-size --json # JSON output
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## What it shows
|
|
39
|
+
|
|
40
|
+
- **Total size** — cumulative disk usage of all `.jsonl` session files
|
|
41
|
+
- **Sessions** — number of conversation files
|
|
42
|
+
- **Daily growth** — average bytes added per day over the last 30 days
|
|
43
|
+
- **Largest file** — size of your biggest single session
|
|
44
|
+
- **Growth projection** — estimated date to reach 10 GB at current rate
|
|
45
|
+
- **Monthly chart** — month-by-month storage breakdown
|
|
46
|
+
- **By project** — top 8 projects by storage consumed
|
|
47
|
+
|
|
48
|
+
## Privacy
|
|
49
|
+
|
|
50
|
+
Reads file **metadata only** (size + modification time). No file content is accessed or transmitted. Everything runs locally.
|
|
51
|
+
|
|
52
|
+
## Browser version
|
|
53
|
+
|
|
54
|
+
Drop your `~/.claude` folder into [cc-size on the web](https://yurukusa.github.io/cc-size/) for the same analysis — no install required.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
Part of [cc-toolkit](https://yurukusa.github.io/cc-toolkit/) — 60 free tools for Claude Code
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cc-size — How much conversation history have you accumulated?
|
|
4
|
+
* Shows total disk usage and growth rate of your Claude Code sessions.
|
|
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
|
+
const includeSubagents = args.includes('--all'); // include subagent files
|
|
15
|
+
|
|
16
|
+
if (showHelp) {
|
|
17
|
+
console.log(`cc-size — How much conversation history have you accumulated?
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
npx cc-size # Total history size and growth
|
|
21
|
+
npx cc-size --all # Include subagent session files
|
|
22
|
+
npx cc-size --json # JSON output
|
|
23
|
+
`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const claudeDir = join(homedir(), '.claude', 'projects');
|
|
28
|
+
|
|
29
|
+
function humanSize(bytes) {
|
|
30
|
+
if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
|
|
31
|
+
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
32
|
+
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
33
|
+
return bytes + ' B';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getISOMonth(date) {
|
|
37
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert project directory name to human-readable project label.
|
|
42
|
+
* "-home-namakusa-projects-cc-loop" -> "projects-cc-loop"
|
|
43
|
+
* "-home-namakusa" -> "~/ (home)"
|
|
44
|
+
*/
|
|
45
|
+
function projectName(dirName) {
|
|
46
|
+
// Remove leading "-home-[username]" prefix (with or without trailing dash)
|
|
47
|
+
const stripped = dirName.replace(/^-home-[^-]+/, '').replace(/^-/, '');
|
|
48
|
+
if (!stripped) return '~/ (home)';
|
|
49
|
+
return stripped;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const byProject = {};
|
|
53
|
+
const byMonth = {};
|
|
54
|
+
let totalFiles = 0;
|
|
55
|
+
let totalBytes = 0;
|
|
56
|
+
let oldestDate = null;
|
|
57
|
+
let newestDate = null;
|
|
58
|
+
let largestFile = { size: 0, path: '' };
|
|
59
|
+
|
|
60
|
+
let projectDirs;
|
|
61
|
+
try {
|
|
62
|
+
projectDirs = readdirSync(claudeDir);
|
|
63
|
+
} catch {
|
|
64
|
+
console.error(`Cannot read ${claudeDir}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const projDir of projectDirs) {
|
|
69
|
+
const projPath = join(claudeDir, projDir);
|
|
70
|
+
let stat;
|
|
71
|
+
try {
|
|
72
|
+
stat = statSync(projPath);
|
|
73
|
+
if (!stat.isDirectory()) continue;
|
|
74
|
+
} catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let entries;
|
|
79
|
+
try {
|
|
80
|
+
entries = readdirSync(projPath);
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
if (!entry.endsWith('.jsonl')) continue;
|
|
87
|
+
if (!includeSubagents && projPath.includes('/subagents/')) continue;
|
|
88
|
+
|
|
89
|
+
const filePath = join(projPath, entry);
|
|
90
|
+
let fstat;
|
|
91
|
+
try {
|
|
92
|
+
fstat = statSync(filePath);
|
|
93
|
+
if (!fstat.isFile()) continue;
|
|
94
|
+
} catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const size = fstat.size;
|
|
99
|
+
const mtime = new Date(fstat.mtime);
|
|
100
|
+
const month = getISOMonth(mtime);
|
|
101
|
+
|
|
102
|
+
totalFiles++;
|
|
103
|
+
totalBytes += size;
|
|
104
|
+
|
|
105
|
+
// By project
|
|
106
|
+
if (!byProject[projDir]) byProject[projDir] = { bytes: 0, files: 0, name: projectName(projDir) };
|
|
107
|
+
byProject[projDir].bytes += size;
|
|
108
|
+
byProject[projDir].files++;
|
|
109
|
+
|
|
110
|
+
// By month
|
|
111
|
+
byMonth[month] = (byMonth[month] || 0) + size;
|
|
112
|
+
|
|
113
|
+
// Tracking
|
|
114
|
+
if (!oldestDate || mtime < oldestDate) oldestDate = mtime;
|
|
115
|
+
if (!newestDate || mtime > newestDate) newestDate = mtime;
|
|
116
|
+
if (size > largestFile.size) largestFile = { size, path: filePath.replace(homedir(), '~') };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (totalFiles === 0) {
|
|
121
|
+
console.error('No session files found.');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Calculate growth rate from last 30 days
|
|
126
|
+
const now = new Date();
|
|
127
|
+
const thirtyDaysAgo = new Date(now - 30 * 24 * 60 * 60 * 1000);
|
|
128
|
+
let last30DaysBytes = 0;
|
|
129
|
+
for (const [month, bytes] of Object.entries(byMonth)) {
|
|
130
|
+
const [yr, mo] = month.split('-').map(Number);
|
|
131
|
+
const monthStart = new Date(yr, mo - 1, 1);
|
|
132
|
+
if (monthStart >= thirtyDaysAgo) last30DaysBytes += bytes;
|
|
133
|
+
}
|
|
134
|
+
const dailyGrowth = last30DaysBytes / 30;
|
|
135
|
+
|
|
136
|
+
// Sorted projects by size
|
|
137
|
+
const sortedProjects = Object.entries(byProject).sort((a, b) => b[1].bytes - a[1].bytes);
|
|
138
|
+
const sortedMonths = Object.entries(byMonth).sort((a, b) => a[0].localeCompare(b[0]));
|
|
139
|
+
|
|
140
|
+
if (jsonMode) {
|
|
141
|
+
console.log(JSON.stringify({
|
|
142
|
+
total_bytes: totalBytes,
|
|
143
|
+
total_size: humanSize(totalBytes),
|
|
144
|
+
total_files: totalFiles,
|
|
145
|
+
daily_growth_bytes: Math.round(dailyGrowth),
|
|
146
|
+
daily_growth: humanSize(dailyGrowth),
|
|
147
|
+
oldest_session: oldestDate?.toISOString().slice(0, 10),
|
|
148
|
+
newest_session: newestDate?.toISOString().slice(0, 10),
|
|
149
|
+
largest_file: { path: largestFile.path, size: humanSize(largestFile.size) },
|
|
150
|
+
by_month: Object.fromEntries(sortedMonths.map(([m, b]) => [m, humanSize(b)])),
|
|
151
|
+
by_project: sortedProjects.slice(0, 10).map(([dir, d]) => ({
|
|
152
|
+
project: d.name,
|
|
153
|
+
size: humanSize(d.bytes),
|
|
154
|
+
files: d.files,
|
|
155
|
+
})),
|
|
156
|
+
}, null, 2));
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Display
|
|
161
|
+
const BAR_WIDTH = 24;
|
|
162
|
+
const maxMonthBytes = Math.max(...Object.values(byMonth));
|
|
163
|
+
const maxProjBytes = sortedProjects[0]?.[1].bytes || 1;
|
|
164
|
+
|
|
165
|
+
function bar(bytes, max) {
|
|
166
|
+
const filled = Math.round((bytes / max) * BAR_WIDTH);
|
|
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-size — Your Claude Code conversation history\n');
|
|
175
|
+
|
|
176
|
+
// Stats header
|
|
177
|
+
console.log(` Total size: ${humanSize(totalBytes)}`);
|
|
178
|
+
console.log(` Total files: ${totalFiles.toLocaleString()} sessions`);
|
|
179
|
+
console.log(` Date range: ${oldestDate?.toISOString().slice(0, 10)} → ${newestDate?.toISOString().slice(0, 10)}`);
|
|
180
|
+
console.log(` Daily growth: ~${humanSize(dailyGrowth)}/day (last 30 days)`);
|
|
181
|
+
console.log(` Largest file: ${humanSize(largestFile.size)}`);
|
|
182
|
+
|
|
183
|
+
// Growth projection
|
|
184
|
+
if (dailyGrowth > 0) {
|
|
185
|
+
const daysTo10GB = (10 * 1024 * 1024 * 1024 - totalBytes) / dailyGrowth;
|
|
186
|
+
if (daysTo10GB > 0 && daysTo10GB < 3650) {
|
|
187
|
+
const projDate = new Date(now.getTime() + daysTo10GB * 86400000);
|
|
188
|
+
console.log(` At this rate: 10 GB by ~${projDate.toISOString().slice(0, 7)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log('\n' + '─'.repeat(54));
|
|
193
|
+
console.log(' Monthly growth\n');
|
|
194
|
+
|
|
195
|
+
for (const [month, bytes] of sortedMonths.slice(-12)) {
|
|
196
|
+
const current = month === getISOMonth(now);
|
|
197
|
+
const tag = current ? ' (in progress)' : '';
|
|
198
|
+
const sizeStr = humanSize(bytes).padStart(8);
|
|
199
|
+
console.log(` ${month} ${bar(bytes, maxMonthBytes)} ${sizeStr}${tag}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log('\n' + '─'.repeat(54));
|
|
203
|
+
console.log(' By project (top 8)\n');
|
|
204
|
+
|
|
205
|
+
const maxProjLabel = Math.max(...sortedProjects.slice(0, 8).map(([, d]) => d.name.length));
|
|
206
|
+
|
|
207
|
+
for (const [, data] of sortedProjects.slice(0, 8)) {
|
|
208
|
+
const label = rpad(data.name, maxProjLabel);
|
|
209
|
+
const sizeStr = humanSize(data.bytes).padStart(8);
|
|
210
|
+
console.log(` ${label} ${bar(data.bytes, maxProjBytes)} ${sizeStr}`);
|
|
211
|
+
}
|
package/index.html
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
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-size — Claude Code history size</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: #79c0ff; 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: #79c0ff; background: rgba(121,192,255,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: 600px; 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: 16px; margin-bottom: 16px; }
|
|
44
|
+
@media (min-width: 500px) { .stats-grid { grid-template-columns: repeat(4, 1fr); } }
|
|
45
|
+
.stat-item { text-align: center; }
|
|
46
|
+
.stat-val { font-size: 1.3rem; color: #79c0ff; font-weight: 700; }
|
|
47
|
+
.stat-lbl { font-size: 0.7rem; color: #8b949e; margin-top: 2px; }
|
|
48
|
+
|
|
49
|
+
.growth-badge {
|
|
50
|
+
text-align: center;
|
|
51
|
+
padding: 10px;
|
|
52
|
+
background: rgba(121,192,255,0.1);
|
|
53
|
+
border: 1px solid rgba(121,192,255,0.3);
|
|
54
|
+
border-radius: 6px;
|
|
55
|
+
font-size: 0.9rem;
|
|
56
|
+
color: #79c0ff;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.bar-chart { font-size: 0.78rem; }
|
|
60
|
+
.bar-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
61
|
+
.bar-label { width: 72px; text-align: right; color: #8b949e; flex-shrink: 0; }
|
|
62
|
+
.bar-label-wide { width: 160px; text-align: right; color: #c9d1d9; flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
63
|
+
.bar-track { flex: 1; height: 14px; background: #21262d; border-radius: 3px; overflow: hidden; }
|
|
64
|
+
.bar-fill-month { height: 100%; background: #79c0ff; border-radius: 3px; transition: width 0.5s ease; }
|
|
65
|
+
.bar-fill-proj { height: 100%; background: linear-gradient(90deg, #388bfd, #79c0ff); 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: #79c0ff; color: #79c0ff; }
|
|
73
|
+
.footer { color: #8b949e; font-size: 0.75rem; text-align: center; margin-top: 12px; }
|
|
74
|
+
.footer a { color: #79c0ff; text-decoration: none; }
|
|
75
|
+
.footer a:hover { text-decoration: underline; }
|
|
76
|
+
</style>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<h1>💾 cc-size</h1>
|
|
80
|
+
<p class="subtitle">How much conversation history have you accumulated?</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">History Summary</div>
|
|
96
|
+
<div class="stats-grid">
|
|
97
|
+
<div class="stat-item"><div class="stat-val" id="stat-total">—</div><div class="stat-lbl">total size</div></div>
|
|
98
|
+
<div class="stat-item"><div class="stat-val" id="stat-files">—</div><div class="stat-lbl">sessions</div></div>
|
|
99
|
+
<div class="stat-item"><div class="stat-val" id="stat-growth">—</div><div class="stat-lbl">per day (30d)</div></div>
|
|
100
|
+
<div class="stat-item"><div class="stat-val" id="stat-largest">—</div><div class="stat-lbl">largest file</div></div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="growth-badge" id="growth-badge">—</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="card">
|
|
106
|
+
<div class="section-title">Monthly growth</div>
|
|
107
|
+
<div class="bar-chart" id="month-chart"></div>
|
|
108
|
+
|
|
109
|
+
<div class="section-title">By project (top 8)</div>
|
|
110
|
+
<div class="bar-chart" id="proj-chart"></div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<button class="reset-btn" id="reset-btn">← Analyze another folder</button>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="footer">
|
|
117
|
+
<a href="https://github.com/yurukusa/cc-size" target="_blank">cc-size</a> ·
|
|
118
|
+
Part of <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit</a> · 106 free tools for Claude Code
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<script>
|
|
122
|
+
const dropZone = document.getElementById('drop-zone');
|
|
123
|
+
const fileInput = document.getElementById('file-input');
|
|
124
|
+
const resultEl = document.getElementById('result');
|
|
125
|
+
const resetBtn = document.getElementById('reset-btn');
|
|
126
|
+
|
|
127
|
+
dropZone.addEventListener('click', () => fileInput.click());
|
|
128
|
+
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
|
|
129
|
+
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
|
|
130
|
+
dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); processFiles(e.dataTransfer.files); });
|
|
131
|
+
fileInput.addEventListener('change', () => processFiles(fileInput.files));
|
|
132
|
+
resetBtn.addEventListener('click', () => { resultEl.classList.remove('visible'); dropZone.style.display = ''; fileInput.value = ''; });
|
|
133
|
+
|
|
134
|
+
function humanSize(bytes) {
|
|
135
|
+
if (bytes >= 1024**3) return (bytes/1024**3).toFixed(1) + ' GB';
|
|
136
|
+
if (bytes >= 1024**2) return (bytes/1024**2).toFixed(1) + ' MB';
|
|
137
|
+
if (bytes >= 1024) return (bytes/1024).toFixed(1) + ' KB';
|
|
138
|
+
return bytes + ' B';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function projectName(webkitPath) {
|
|
142
|
+
// Extract project dir from path like ".claude/projects/PROJ/file.jsonl"
|
|
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 getMonth(file) {
|
|
154
|
+
const d = new Date(file.lastModified);
|
|
155
|
+
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function processFiles(files) {
|
|
159
|
+
const jsonlFiles = Array.from(files).filter(f => {
|
|
160
|
+
const p = f.webkitRelativePath || f.name;
|
|
161
|
+
return p.endsWith('.jsonl') && !p.includes('/subagents/');
|
|
162
|
+
});
|
|
163
|
+
if (!jsonlFiles.length) { alert('No session files found.'); return; }
|
|
164
|
+
dropZone.style.display = 'none';
|
|
165
|
+
|
|
166
|
+
let totalBytes = 0, totalFiles = 0;
|
|
167
|
+
let largestSize = 0;
|
|
168
|
+
const byMonth = {}, byProj = {};
|
|
169
|
+
const now = new Date();
|
|
170
|
+
const thirtyAgo = now - 30*86400000;
|
|
171
|
+
|
|
172
|
+
for (const f of jsonlFiles) {
|
|
173
|
+
const size = f.size;
|
|
174
|
+
totalBytes += size;
|
|
175
|
+
totalFiles++;
|
|
176
|
+
if (size > largestSize) largestSize = size;
|
|
177
|
+
|
|
178
|
+
const month = getMonth(f);
|
|
179
|
+
byMonth[month] = (byMonth[month]||0) + size;
|
|
180
|
+
|
|
181
|
+
const proj = projectName(f.webkitRelativePath || f.name);
|
|
182
|
+
if (!byProj[proj]) byProj[proj] = 0;
|
|
183
|
+
byProj[proj] += size;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 30-day growth
|
|
187
|
+
let last30 = 0;
|
|
188
|
+
for (const [month, bytes] of Object.entries(byMonth)) {
|
|
189
|
+
const [yr, mo] = month.split('-').map(Number);
|
|
190
|
+
if (new Date(yr, mo-1, 1) >= new Date(thirtyAgo)) last30 += bytes;
|
|
191
|
+
}
|
|
192
|
+
const dailyGrowth = last30 / 30;
|
|
193
|
+
|
|
194
|
+
document.getElementById('stat-total').textContent = humanSize(totalBytes);
|
|
195
|
+
document.getElementById('stat-files').textContent = totalFiles;
|
|
196
|
+
document.getElementById('stat-growth').textContent = humanSize(dailyGrowth);
|
|
197
|
+
document.getElementById('stat-largest').textContent = humanSize(largestSize);
|
|
198
|
+
|
|
199
|
+
// Growth projection
|
|
200
|
+
const daysTo10GB = (10*1024**3 - totalBytes) / dailyGrowth;
|
|
201
|
+
if (daysTo10GB > 0 && daysTo10GB < 3650) {
|
|
202
|
+
const d = new Date(now.getTime() + daysTo10GB*86400000);
|
|
203
|
+
document.getElementById('growth-badge').textContent = `At this rate → 10 GB by ${d.toISOString().slice(0,7)}`;
|
|
204
|
+
} else {
|
|
205
|
+
document.getElementById('growth-badge').textContent = `${totalFiles} session files · ${humanSize(totalBytes)} total`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Monthly chart
|
|
209
|
+
const months = Object.entries(byMonth).sort((a,b)=>a[0].localeCompare(b[0])).slice(-12);
|
|
210
|
+
const maxM = Math.max(...months.map(([,b])=>b));
|
|
211
|
+
const curMonth = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}`;
|
|
212
|
+
document.getElementById('month-chart').innerHTML = months.map(([m,b]) =>
|
|
213
|
+
`<div class="bar-row">
|
|
214
|
+
<div class="bar-label">${m}</div>
|
|
215
|
+
<div class="bar-track"><div class="bar-fill-month" style="width:${(b/maxM*100).toFixed(1)}%"></div></div>
|
|
216
|
+
<div class="bar-count">${humanSize(b)}${m===curMonth?' *':''}</div>
|
|
217
|
+
</div>`
|
|
218
|
+
).join('');
|
|
219
|
+
|
|
220
|
+
// Project chart
|
|
221
|
+
const projs = Object.entries(byProj).sort((a,b)=>b[1]-a[1]).slice(0,8);
|
|
222
|
+
const maxP = projs[0]?.[1]||1;
|
|
223
|
+
document.getElementById('proj-chart').innerHTML = projs.map(([name,bytes]) =>
|
|
224
|
+
`<div class="bar-row">
|
|
225
|
+
<div class="bar-label-wide">${name}</div>
|
|
226
|
+
<div class="bar-track"><div class="bar-fill-proj" style="width:${(bytes/maxP*100).toFixed(1)}%"></div></div>
|
|
227
|
+
<div class="bar-count">${humanSize(bytes)}</div>
|
|
228
|
+
</div>`
|
|
229
|
+
).join('');
|
|
230
|
+
|
|
231
|
+
resultEl.classList.add('visible');
|
|
232
|
+
}
|
|
233
|
+
</script>
|
|
234
|
+
</body>
|
|
235
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-size",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "How much conversation history have you accumulated? Total disk usage and growth rate of your Claude Code sessions.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc-size": "./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
|
+
}
|