cc-output 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 +247 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# cc-output
|
|
2
|
+
|
|
3
|
+
See how much text Claude Code generated for you.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx cc-output
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
cc-output — Claude Code Output Volume
|
|
11
|
+
══════════════════════════════════════════
|
|
12
|
+
|
|
13
|
+
▸ Overview
|
|
14
|
+
Output tokens: 20,468,061
|
|
15
|
+
≈ Words: 15,351,046 (×0.75/token)
|
|
16
|
+
≈ Pages: 61,404 (÷250 words)
|
|
17
|
+
≈ Novels: 192 (÷80,000 words)
|
|
18
|
+
Sessions: 3,877
|
|
19
|
+
Cache hit rate: 96.3%
|
|
20
|
+
|
|
21
|
+
▸ Output by project
|
|
22
|
+
~/ (home) ██████████████████ 12.9M 62.8%
|
|
23
|
+
projects-cc-loop ██████████░░░░░░░░ 7.4M 35.9%
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
▸ Monthly output
|
|
27
|
+
2026-02 ██████████████████ 16.9M
|
|
28
|
+
2026-01 ███░░░░░░░░░░░░░░░ 2.7M
|
|
29
|
+
|
|
30
|
+
▸ Most verbose sessions
|
|
31
|
+
5.1M projects/cc-loop/2fa937ff-a7c6-...
|
|
32
|
+
1.1M projects/-home-namakusa/56d7f7ab-...
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Browser version
|
|
36
|
+
|
|
37
|
+
Drag and drop your `~/.claude` folder at:
|
|
38
|
+
**https://yurukusa.github.io/cc-output/**
|
|
39
|
+
|
|
40
|
+
Nothing is uploaded. All analysis runs locally in your browser.
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx cc-output # interactive output
|
|
46
|
+
npx cc-output --json # JSON output for scripting
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## What it measures
|
|
50
|
+
|
|
51
|
+
- **Output tokens** — every token Claude generated in response to you
|
|
52
|
+
- **Words / Pages / Novels** — human-readable equivalents (0.75 words/token, 250 words/page, 80,000 words/novel)
|
|
53
|
+
- **By project** — which projects got the most output
|
|
54
|
+
- **Monthly trend** — output volume over time
|
|
55
|
+
- **Top sessions** — your most verbose conversations
|
|
56
|
+
|
|
57
|
+
## Part of cc-toolkit
|
|
58
|
+
|
|
59
|
+
One of [60 free tools](https://yurukusa.github.io/cc-toolkit/) for Claude Code users.
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// cc-output — See how much text Claude Code generated for you.
|
|
4
|
+
// Zero dependencies. Reads ~/.claude/projects/ session transcripts.
|
|
5
|
+
|
|
6
|
+
import { readdir, stat, open } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { createInterface } from 'node:readline';
|
|
10
|
+
import { createReadStream } from 'node:fs';
|
|
11
|
+
|
|
12
|
+
const CONCURRENCY = 8;
|
|
13
|
+
|
|
14
|
+
// Approximations
|
|
15
|
+
const WORDS_PER_TOKEN = 0.75;
|
|
16
|
+
const WORDS_PER_PAGE = 250;
|
|
17
|
+
const WORDS_PER_NOVEL = 80000;
|
|
18
|
+
|
|
19
|
+
const C = {
|
|
20
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
21
|
+
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
22
|
+
cyan: '\x1b[36m', blue: '\x1b[34m',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function bar(pct, width = 22) {
|
|
26
|
+
const filled = Math.round(pct * width);
|
|
27
|
+
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function projectName(dir) {
|
|
31
|
+
const stripped = dir.replace(/^-home-[^-]+/, '').replace(/^-/, '');
|
|
32
|
+
return stripped || '~/ (home)';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fmtTokens(n) {
|
|
36
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
37
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
|
|
38
|
+
return String(n);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function analyzeFile(filePath) {
|
|
42
|
+
let outputTokens = 0;
|
|
43
|
+
let inputTokens = 0;
|
|
44
|
+
let cacheReadTokens = 0;
|
|
45
|
+
let cacheCreationTokens = 0;
|
|
46
|
+
let firstTimestamp = null;
|
|
47
|
+
|
|
48
|
+
const rl = createInterface({
|
|
49
|
+
input: createReadStream(filePath),
|
|
50
|
+
crlfDelay: Infinity,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
for await (const line of rl) {
|
|
54
|
+
if (!line || !line.includes('"usage"')) continue;
|
|
55
|
+
let data;
|
|
56
|
+
try { data = JSON.parse(line); } catch { continue; }
|
|
57
|
+
|
|
58
|
+
// Extract timestamp
|
|
59
|
+
if (!firstTimestamp) {
|
|
60
|
+
const ts = data.timestamp || data.ts;
|
|
61
|
+
if (ts) firstTimestamp = ts;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const msg = data.message || data;
|
|
65
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
66
|
+
const usage = msg.usage;
|
|
67
|
+
if (!usage || typeof usage !== 'object') continue;
|
|
68
|
+
|
|
69
|
+
if (typeof usage.output_tokens === 'number') outputTokens += usage.output_tokens;
|
|
70
|
+
if (typeof usage.input_tokens === 'number') inputTokens += usage.input_tokens;
|
|
71
|
+
if (typeof usage.cache_read_input_tokens === 'number') cacheReadTokens += usage.cache_read_input_tokens;
|
|
72
|
+
if (typeof usage.cache_creation_input_tokens === 'number') cacheCreationTokens += usage.cache_creation_input_tokens;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { outputTokens, inputTokens, cacheReadTokens, cacheCreationTokens, firstTimestamp };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function scan() {
|
|
79
|
+
const projectsDir = join(homedir(), '.claude', 'projects');
|
|
80
|
+
let projectDirs;
|
|
81
|
+
try { projectDirs = await readdir(projectsDir); } catch { return null; }
|
|
82
|
+
|
|
83
|
+
const tasks = [];
|
|
84
|
+
for (const pd of projectDirs) {
|
|
85
|
+
const pp = join(projectsDir, pd);
|
|
86
|
+
const ps = await stat(pp).catch(() => null);
|
|
87
|
+
if (!ps?.isDirectory()) continue;
|
|
88
|
+
const files = await readdir(pp).catch(() => []);
|
|
89
|
+
for (const f of files) {
|
|
90
|
+
if (f.endsWith('.jsonl')) {
|
|
91
|
+
tasks.push({ path: join(pp, f), project: projectName(pd) });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Subagent files
|
|
95
|
+
for (const f of files) {
|
|
96
|
+
const sp = join(pp, f, 'subagents');
|
|
97
|
+
const ss = await stat(sp).catch(() => null);
|
|
98
|
+
if (!ss?.isDirectory()) continue;
|
|
99
|
+
const sfs = await readdir(sp).catch(() => []);
|
|
100
|
+
for (const sf of sfs) {
|
|
101
|
+
if (sf.endsWith('.jsonl')) {
|
|
102
|
+
tasks.push({ path: join(sp, sf), project: projectName(pd) });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let totalOutput = 0, totalInput = 0, totalCacheRead = 0, totalCacheCreate = 0;
|
|
109
|
+
let sessionsWithOutput = 0;
|
|
110
|
+
const byProject = {}; // project -> outputTokens
|
|
111
|
+
const byMonth = {}; // YYYY-MM -> outputTokens
|
|
112
|
+
const topSessions = []; // { outputTokens, path }
|
|
113
|
+
|
|
114
|
+
// Process in batches
|
|
115
|
+
for (let i = 0; i < tasks.length; i += CONCURRENCY) {
|
|
116
|
+
const batch = tasks.slice(i, i + CONCURRENCY);
|
|
117
|
+
const results = await Promise.all(batch.map(async t => {
|
|
118
|
+
const st = await stat(t.path).catch(() => null);
|
|
119
|
+
if (!st || st.size < 100) return null;
|
|
120
|
+
const r = await analyzeFile(t.path).catch(() => null);
|
|
121
|
+
return r ? { ...r, project: t.project, path: t.path } : null;
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
for (const r of results) {
|
|
125
|
+
if (!r || r.outputTokens === 0) continue;
|
|
126
|
+
totalOutput += r.outputTokens;
|
|
127
|
+
totalInput += r.inputTokens;
|
|
128
|
+
totalCacheRead += r.cacheReadTokens;
|
|
129
|
+
totalCacheCreate += r.cacheCreationTokens;
|
|
130
|
+
sessionsWithOutput++;
|
|
131
|
+
|
|
132
|
+
if (!byProject[r.project]) byProject[r.project] = 0;
|
|
133
|
+
byProject[r.project] += r.outputTokens;
|
|
134
|
+
|
|
135
|
+
if (r.firstTimestamp) {
|
|
136
|
+
const month = r.firstTimestamp.slice(0, 7);
|
|
137
|
+
if (!byMonth[month]) byMonth[month] = 0;
|
|
138
|
+
byMonth[month] += r.outputTokens;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
topSessions.push({ tokens: r.outputTokens, path: r.path });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Sort top sessions
|
|
146
|
+
topSessions.sort((a, b) => b.tokens - a.tokens);
|
|
147
|
+
|
|
148
|
+
return { totalOutput, totalInput, totalCacheRead, totalCacheCreate, sessionsWithOutput, byProject, byMonth, topSessions };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const jsonMode = process.argv.includes('--json');
|
|
152
|
+
if (!jsonMode) process.stdout.write(` ${C.dim}Counting tokens...${C.reset}\r`);
|
|
153
|
+
|
|
154
|
+
const data = await scan();
|
|
155
|
+
if (!data) {
|
|
156
|
+
console.error('Could not read ~/.claude/projects/');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { totalOutput, totalInput, totalCacheRead, totalCacheCreate, sessionsWithOutput, byProject, byMonth, topSessions } = data;
|
|
161
|
+
|
|
162
|
+
const words = Math.round(totalOutput * WORDS_PER_TOKEN);
|
|
163
|
+
const pages = Math.round(words / WORDS_PER_PAGE);
|
|
164
|
+
const novels = Math.round(words / WORDS_PER_NOVEL);
|
|
165
|
+
const cacheHitRate = (totalInput + totalCacheRead + totalCacheCreate) > 0
|
|
166
|
+
? totalCacheRead / (totalInput + totalCacheRead + totalCacheCreate)
|
|
167
|
+
: 0;
|
|
168
|
+
|
|
169
|
+
if (jsonMode) {
|
|
170
|
+
const byProjectSorted = Object.entries(byProject)
|
|
171
|
+
.sort((a, b) => b[1] - a[1])
|
|
172
|
+
.map(([name, tokens]) => ({ name, tokens }));
|
|
173
|
+
const byMonthSorted = Object.entries(byMonth)
|
|
174
|
+
.sort((a, b) => b[0].localeCompare(a[0]))
|
|
175
|
+
.map(([month, tokens]) => ({ month, tokens }));
|
|
176
|
+
console.log(JSON.stringify({
|
|
177
|
+
version: '1.0.0',
|
|
178
|
+
totalOutputTokens: totalOutput,
|
|
179
|
+
totalInputTokens: totalInput,
|
|
180
|
+
cacheReadTokens: totalCacheRead,
|
|
181
|
+
cacheHitRate: +cacheHitRate.toFixed(3),
|
|
182
|
+
sessionsWithOutput,
|
|
183
|
+
equivalents: { words, pages, novels },
|
|
184
|
+
byProject: byProjectSorted,
|
|
185
|
+
byMonth: byMonthSorted,
|
|
186
|
+
topSessions: topSessions.slice(0, 10).map(s => ({ tokens: s.tokens, path: s.path.split('/').slice(-3).join('/') })),
|
|
187
|
+
}, null, 2));
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Display ──────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
console.log(`\n ${C.bold}${C.cyan}cc-output — Claude Code Output Volume${C.reset}`);
|
|
194
|
+
console.log(` ${'═'.repeat(42)}`);
|
|
195
|
+
|
|
196
|
+
console.log(`\n ${C.bold}▸ Overview${C.reset}`);
|
|
197
|
+
console.log(` Output tokens: ${C.bold}${totalOutput.toLocaleString()}${C.reset}`);
|
|
198
|
+
console.log(` ≈ Words: ${C.bold}${C.green}${words.toLocaleString()}${C.reset}${C.dim} (×0.75/token)${C.reset}`);
|
|
199
|
+
console.log(` ≈ Pages: ${C.bold}${C.green}${pages.toLocaleString()}${C.reset}${C.dim} (÷250 words)${C.reset}`);
|
|
200
|
+
console.log(` ≈ Novels: ${C.bold}${C.yellow}${novels.toLocaleString()}${C.reset}${C.dim} (÷80,000 words)${C.reset}`);
|
|
201
|
+
console.log(` Sessions: ${C.dim}${sessionsWithOutput.toLocaleString()}${C.reset}`);
|
|
202
|
+
console.log(` Cache hit rate: ${C.dim}${(cacheHitRate * 100).toFixed(1)}%${C.reset}`);
|
|
203
|
+
|
|
204
|
+
// By project
|
|
205
|
+
const projList = Object.entries(byProject)
|
|
206
|
+
.sort((a, b) => b[1] - a[1])
|
|
207
|
+
.slice(0, 8);
|
|
208
|
+
|
|
209
|
+
if (projList.length > 0) {
|
|
210
|
+
console.log(`\n ${C.bold}▸ Output by project${C.reset}`);
|
|
211
|
+
const maxProj = projList[0][1];
|
|
212
|
+
for (const [name, tokens] of projList) {
|
|
213
|
+
const pct = totalOutput > 0 ? tokens / totalOutput : 0;
|
|
214
|
+
const b = bar(tokens / maxProj, 18);
|
|
215
|
+
console.log(` ${C.cyan}${name.slice(0, 24).padEnd(24)}${C.reset} ${C.dim}${b}${C.reset} ${fmtTokens(tokens).padStart(6)} ${C.dim}${(pct * 100).toFixed(1)}%${C.reset}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Monthly breakdown
|
|
220
|
+
const monthList = Object.entries(byMonth)
|
|
221
|
+
.sort((a, b) => b[0].localeCompare(a[0]))
|
|
222
|
+
.slice(0, 6);
|
|
223
|
+
|
|
224
|
+
if (monthList.length > 0) {
|
|
225
|
+
console.log(`\n ${C.bold}▸ Monthly output${C.reset}`);
|
|
226
|
+
const maxMonth = Math.max(...monthList.map(([, v]) => v));
|
|
227
|
+
for (const [month, tokens] of monthList) {
|
|
228
|
+
const b = bar(tokens / maxMonth, 18);
|
|
229
|
+
console.log(` ${C.dim}${month}${C.reset} ${b} ${C.bold}${fmtTokens(tokens)}${C.reset}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Top sessions
|
|
234
|
+
const topN = topSessions.slice(0, 6);
|
|
235
|
+
if (topN.length > 0) {
|
|
236
|
+
console.log(`\n ${C.bold}▸ Most verbose sessions${C.reset}`);
|
|
237
|
+
for (const s of topN) {
|
|
238
|
+
const label = s.path.split('/').slice(-3).join('/').slice(0, 55);
|
|
239
|
+
console.log(` ${C.yellow}${fmtTokens(s.tokens).padStart(6)}${C.reset} ${C.dim}${label}${C.reset}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log();
|
|
244
|
+
console.log(` ${C.dim}─── Share ───${C.reset}`);
|
|
245
|
+
console.log(` ${C.dim}Claude Code generated ${novels} novels worth of text for me (${totalOutput.toLocaleString()} output tokens)`);
|
|
246
|
+
console.log(` #ClaudeCode${C.reset}`);
|
|
247
|
+
console.log();
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-output",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "See how much text Claude Code generated for you. Output tokens, words, pages, and novel equivalents across all sessions.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"cc-output": "cli.mjs"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"claude-code",
|
|
11
|
+
"claude",
|
|
12
|
+
"ai",
|
|
13
|
+
"tokens",
|
|
14
|
+
"output",
|
|
15
|
+
"analytics",
|
|
16
|
+
"productivity",
|
|
17
|
+
"stats"
|
|
18
|
+
],
|
|
19
|
+
"author": "yurukusa",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/yurukusa/cc-output.git"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"cli.mjs",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
]
|
|
33
|
+
}
|