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.
Files changed (3) hide show
  1. package/README.md +59 -0
  2. package/cli.mjs +247 -0
  3. 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
+ }