cc-context-check 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 (4) hide show
  1. package/README.md +54 -0
  2. package/cli.mjs +200 -0
  3. package/index.html +486 -0
  4. package/package.json +27 -0
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # cc-context-check
2
+
3
+ See exactly how full your Claude Code context window is โ€” right from your terminal.
4
+
5
+ ```
6
+ npx cc-context-check
7
+ ```
8
+
9
+ ## What it does
10
+
11
+ Reads token usage directly from your `~/.claude/projects/` session transcripts and shows:
12
+
13
+ - **Context fill %** with a color-coded progress bar
14
+ - **Token counts**: input used (including cache), output, remaining
15
+ - **Smart warnings**: yellow at 70%, red at 85% (time to `/compact`)
16
+ - **Last 5 active sessions** across all your Claude Code projects
17
+
18
+ ## Example output
19
+
20
+ ```
21
+ cc-context-check โ€” Context window usage across sessions
22
+
23
+ Context limit: 200.0k tokens (Claude Sonnet/Opus)
24
+
25
+ ๐ŸŸข ~/projects/my-app [a3f9c12] just now ยท 12.4 MB
26
+ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 40.1% used
27
+ 80.2k input ยท 1.2k output ยท 119.8k remaining
28
+
29
+ ๐ŸŸก ~/ [b7d44e1] 2h ago ยท 5.9 MB
30
+ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘ 71.5% used
31
+ 143.0k input ยท 89 output ยท 57.0k remaining
32
+ โ–ณ Warning: Context is getting full โ€” consider /compact
33
+ ```
34
+
35
+ ## Options
36
+
37
+ ```
38
+ --all, -a Show top 20 sessions instead of 5
39
+ --json JSON output for scripting
40
+ ```
41
+
42
+ ## Why this exists
43
+
44
+ Claude Code's context window is 200k tokens. When it fills up, responses slow down and you lose context of earlier work. `/compact` compresses history โ€” but knowing *when* to compact is guesswork without this tool.
45
+
46
+ cc-context-check reads the actual `input_tokens`, `cache_read_input_tokens`, and `cache_creation_input_tokens` from your session files to give you the real number.
47
+
48
+ ## Part of cc-toolkit
49
+
50
+ One of 79 free tools for Claude Code users โ†’ [cc-toolkit](https://yurukusa.github.io/cc-toolkit/)
51
+
52
+ ## License
53
+
54
+ MIT
package/cli.mjs ADDED
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+
3
+ // cc-context-check โ€” See how full your Claude Code context window is
4
+ // Reads ~/.claude/projects/ session transcripts, extracts token usage from the latest exchange.
5
+ // Zero dependencies. Works with Claude Sonnet/Opus/Haiku.
6
+
7
+ import { readdir, stat, open } from 'node:fs/promises';
8
+ import { join } from 'node:path';
9
+ import { homedir } from 'node:os';
10
+
11
+ // โ”€โ”€ Config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
12
+
13
+ const CONTEXT_LIMIT = 200_000; // Claude Sonnet/Opus context window (tokens)
14
+ const WARN_PCT = 0.70; // yellow warning threshold
15
+ const CRIT_PCT = 0.85; // red critical threshold
16
+ const TAIL_BYTES = 65_536; // read last 64KB to find recent usage data
17
+
18
+ // โ”€โ”€ Color helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
19
+
20
+ const C = {
21
+ reset: '\x1b[0m',
22
+ bold: '\x1b[1m',
23
+ dim: '\x1b[2m',
24
+ red: '\x1b[31m',
25
+ green: '\x1b[32m',
26
+ yellow: '\x1b[33m',
27
+ blue: '\x1b[34m',
28
+ cyan: '\x1b[36m',
29
+ white: '\x1b[37m',
30
+ };
31
+
32
+ function bar(pct, width = 30) {
33
+ const filled = Math.min(Math.round(pct * width), width);
34
+ const color = pct >= CRIT_PCT ? C.red : pct >= WARN_PCT ? C.yellow : C.green;
35
+ return color + 'โ–ˆ'.repeat(filled) + C.dim + 'โ–‘'.repeat(width - filled) + C.reset;
36
+ }
37
+
38
+ function fmt(n) {
39
+ return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
40
+ }
41
+
42
+ // โ”€โ”€ Read last N bytes of a file โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
43
+
44
+ async function readTail(filePath, maxBytes) {
45
+ const fh = await open(filePath, 'r');
46
+ try {
47
+ const { size } = await fh.stat();
48
+ const start = Math.max(0, size - maxBytes);
49
+ const buf = Buffer.alloc(size - start);
50
+ const { bytesRead } = await fh.read(buf, 0, buf.length, start);
51
+ return buf.toString('utf8', 0, bytesRead);
52
+ } finally {
53
+ await fh.close();
54
+ }
55
+ }
56
+
57
+ // โ”€โ”€ Find latest usage from a jsonl chunk โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
58
+
59
+ function extractLatestUsage(chunk) {
60
+ const lines = chunk.split('\n').reverse();
61
+ for (const line of lines) {
62
+ if (!line.trim()) continue;
63
+ let d;
64
+ try { d = JSON.parse(line); } catch { continue; }
65
+ if (d.type !== 'assistant') continue;
66
+ const usage = d?.message?.usage;
67
+ if (!usage) continue;
68
+ const inputTokens = (usage.input_tokens || 0) +
69
+ (usage.cache_read_input_tokens || 0) +
70
+ (usage.cache_creation_input_tokens || 0);
71
+ const outputTokens = usage.output_tokens || 0;
72
+ if (inputTokens > 0) {
73
+ return { inputTokens, outputTokens, raw: usage };
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ // โ”€โ”€ Find all project jsonl files sorted by mtime โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
80
+
81
+ async function findAllJsonlFiles() {
82
+ const base = join(homedir(), '.claude', 'projects');
83
+ let projectDirs;
84
+ try {
85
+ projectDirs = await readdir(base);
86
+ } catch {
87
+ return [];
88
+ }
89
+
90
+ const files = [];
91
+ for (const dir of projectDirs) {
92
+ const dirPath = join(base, dir);
93
+ let entries;
94
+ try { entries = await readdir(dirPath); } catch { continue; }
95
+ for (const entry of entries) {
96
+ if (!entry.endsWith('.jsonl')) continue;
97
+ const filePath = join(dirPath, entry);
98
+ try {
99
+ const s = await stat(filePath);
100
+ files.push({ filePath, mtime: s.mtimeMs, size: s.size, project: dir });
101
+ } catch {}
102
+ }
103
+ }
104
+ files.sort((a, b) => b.mtime - a.mtime);
105
+ return files;
106
+ }
107
+
108
+ // โ”€โ”€ Format timestamp โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
109
+
110
+ function relTime(mtime) {
111
+ const diff = Date.now() - mtime;
112
+ const min = Math.floor(diff / 60_000);
113
+ const hr = Math.floor(min / 60);
114
+ if (hr > 0) return `${hr}h ${min % 60}m ago`;
115
+ if (min > 0) return `${min}m ago`;
116
+ return 'just now';
117
+ }
118
+
119
+ // โ”€โ”€ Main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
120
+
121
+ async function main() {
122
+ const args = process.argv.slice(2);
123
+ const showAll = args.includes('--all') || args.includes('-a');
124
+ const jsonOut = args.includes('--json');
125
+ const topN = showAll ? 20 : 5;
126
+
127
+ const files = await findAllJsonlFiles();
128
+ if (files.length === 0) {
129
+ console.error('No Claude Code session files found in ~/.claude/projects/');
130
+ process.exit(1);
131
+ }
132
+
133
+ const results = [];
134
+ for (const f of files.slice(0, topN)) {
135
+ let usage = null;
136
+ try {
137
+ const chunk = await readTail(f.filePath, TAIL_BYTES);
138
+ usage = extractLatestUsage(chunk);
139
+ } catch {}
140
+ results.push({ ...f, usage });
141
+ }
142
+
143
+ if (jsonOut) {
144
+ console.log(JSON.stringify(results.map(r => ({
145
+ project: r.project,
146
+ file: r.filePath.split('/').pop(),
147
+ mtime: new Date(r.mtime).toISOString(),
148
+ size_mb: (r.size / 1024 / 1024).toFixed(1),
149
+ input_tokens: r.usage?.inputTokens ?? null,
150
+ output_tokens: r.usage?.outputTokens ?? null,
151
+ pct: r.usage ? (r.usage.inputTokens / CONTEXT_LIMIT * 100).toFixed(1) : null,
152
+ })), null, 2));
153
+ return;
154
+ }
155
+
156
+ // โ”€โ”€ Header โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
157
+ console.log(`\n${C.bold}cc-context-check${C.reset} โ€” Context window usage across sessions\n`);
158
+ console.log(`${C.dim}Context limit: ${fmt(CONTEXT_LIMIT)} tokens (Claude Sonnet/Opus)${C.reset}\n`);
159
+
160
+ let anyData = false;
161
+ for (const r of results) {
162
+ const projectShort = r.project.replace(/-home-namakusa-?/, '~/')
163
+ .replace(/^~\/projects\//, '~/').slice(0, 40);
164
+ const sessionId = r.filePath.split('/').pop().slice(0, 8);
165
+ const sizeStr = (r.size / 1024 / 1024).toFixed(1) + ' MB';
166
+
167
+ if (!r.usage) {
168
+ console.log(`${C.dim}${projectShort} [${sessionId}] โ€” no usage data (${relTime(r.mtime)})${C.reset}`);
169
+ continue;
170
+ }
171
+
172
+ anyData = true;
173
+ const { inputTokens, outputTokens } = r.usage;
174
+ const pct = inputTokens / CONTEXT_LIMIT;
175
+ const pctStr = (pct * 100).toFixed(1) + '%';
176
+ const statusIcon = pct >= CRIT_PCT ? '๐Ÿ”ด' : pct >= WARN_PCT ? '๐ŸŸก' : '๐ŸŸข';
177
+ const remaining = CONTEXT_LIMIT - inputTokens;
178
+
179
+ console.log(`${statusIcon} ${C.bold}${projectShort}${C.reset} ${C.dim}[${sessionId}] ${relTime(r.mtime)} ยท ${sizeStr}${C.reset}`);
180
+ console.log(` ${bar(pct)} ${C.bold}${pctStr}${C.reset} used`);
181
+ console.log(` ${C.cyan}${fmt(inputTokens)}${C.reset} input ยท ${C.dim}${fmt(outputTokens)} output ยท ${fmt(remaining)} remaining${C.reset}`);
182
+
183
+ if (pct >= CRIT_PCT) {
184
+ console.log(` ${C.red}โš  Critical: Run /compact soon to avoid context overflow${C.reset}`);
185
+ } else if (pct >= WARN_PCT) {
186
+ console.log(` ${C.yellow}โ–ณ Warning: Context is getting full โ€” consider /compact${C.reset}`);
187
+ }
188
+ console.log();
189
+ }
190
+
191
+ if (!anyData) {
192
+ console.log(`${C.dim}No token usage data found in recent sessions.${C.reset}`);
193
+ console.log(`${C.dim}Token data appears in sessions after at least one AI response.${C.reset}\n`);
194
+ }
195
+
196
+ // โ”€โ”€ Footer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
197
+ console.log(`${C.dim}Options: --all (-a) show top 20 sessions ยท --json JSON output${C.reset}\n`);
198
+ }
199
+
200
+ main().catch(e => { console.error(e.message); process.exit(1); });
package/index.html ADDED
@@ -0,0 +1,486 @@
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-context-check โ€” How full is your Claude Code context window?</title>
7
+ <meta name="description" content="See exactly how full your Claude Code context window is. Drag in your ~/.claude folder for instant analysis.">
8
+ <meta property="og:title" content="cc-context-check โ€” Context window usage for Claude Code">
9
+ <meta property="og:description" content="Reads token usage from your session transcripts. Color-coded progress bars: green, yellow, red. No install needed.">
10
+ <meta property="og:url" content="https://yurukusa.github.io/cc-context-check/">
11
+ <meta name="twitter:card" content="summary">
12
+ <meta name="twitter:site" content="@yurukusa_dev">
13
+ <style>
14
+ * { margin: 0; padding: 0; box-sizing: border-box; }
15
+
16
+ :root {
17
+ --bg: #0d1117;
18
+ --surface: #161b22;
19
+ --border: #30363d;
20
+ --text: #e6edf3;
21
+ --muted: #8b949e;
22
+ --green: #56d364;
23
+ --yellow: #f7c948;
24
+ --red: #ff7b72;
25
+ --cyan: #58d8f0;
26
+ --blue: #58a6ff;
27
+ }
28
+
29
+ body {
30
+ background: var(--bg);
31
+ color: var(--text);
32
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Segoe UI Mono', Consolas, monospace;
33
+ min-height: 100vh;
34
+ display: flex;
35
+ flex-direction: column;
36
+ align-items: center;
37
+ justify-content: flex-start;
38
+ padding: 2rem 1rem;
39
+ }
40
+
41
+ .header {
42
+ text-align: center;
43
+ margin-bottom: 2rem;
44
+ }
45
+
46
+ .header h1 {
47
+ font-size: 1.8rem;
48
+ font-weight: 700;
49
+ color: var(--cyan);
50
+ font-family: Consolas, 'Courier New', monospace;
51
+ margin-bottom: 0.5rem;
52
+ }
53
+
54
+ .header p {
55
+ color: var(--muted);
56
+ font-size: 0.9rem;
57
+ }
58
+
59
+ .drop-zone {
60
+ width: 100%;
61
+ max-width: 600px;
62
+ border: 2px dashed var(--border);
63
+ border-radius: 12px;
64
+ padding: 3rem 2rem;
65
+ text-align: center;
66
+ cursor: pointer;
67
+ transition: border-color 0.2s, background 0.2s;
68
+ margin-bottom: 2rem;
69
+ background: var(--surface);
70
+ }
71
+
72
+ .drop-zone:hover, .drop-zone.drag-over {
73
+ border-color: var(--cyan);
74
+ background: #1a2332;
75
+ }
76
+
77
+ .drop-zone h2 {
78
+ font-size: 1.1rem;
79
+ font-weight: 600;
80
+ margin-bottom: 0.75rem;
81
+ color: var(--text);
82
+ }
83
+
84
+ .drop-zone p {
85
+ color: var(--muted);
86
+ font-size: 0.85rem;
87
+ line-height: 1.5;
88
+ }
89
+
90
+ .drop-zone .icon {
91
+ font-size: 2.5rem;
92
+ margin-bottom: 1rem;
93
+ }
94
+
95
+ .drop-zone input[type="file"] {
96
+ display: none;
97
+ }
98
+
99
+ .btn {
100
+ background: var(--cyan);
101
+ color: #0d1117;
102
+ border: none;
103
+ border-radius: 6px;
104
+ padding: 0.6rem 1.5rem;
105
+ font-size: 0.9rem;
106
+ font-weight: 600;
107
+ cursor: pointer;
108
+ margin-top: 1rem;
109
+ font-family: inherit;
110
+ }
111
+
112
+ .btn:hover { opacity: 0.85; }
113
+
114
+ #results {
115
+ width: 100%;
116
+ max-width: 700px;
117
+ display: none;
118
+ }
119
+
120
+ .results-header {
121
+ font-size: 0.8rem;
122
+ color: var(--muted);
123
+ margin-bottom: 1.5rem;
124
+ text-align: center;
125
+ }
126
+
127
+ .session-card {
128
+ background: var(--surface);
129
+ border: 1px solid var(--border);
130
+ border-radius: 8px;
131
+ padding: 1.25rem 1.5rem;
132
+ margin-bottom: 1rem;
133
+ }
134
+
135
+ .session-meta {
136
+ display: flex;
137
+ justify-content: space-between;
138
+ align-items: flex-start;
139
+ margin-bottom: 0.75rem;
140
+ }
141
+
142
+ .session-project {
143
+ font-weight: 600;
144
+ font-size: 0.95rem;
145
+ color: var(--text);
146
+ }
147
+
148
+ .session-info {
149
+ font-size: 0.75rem;
150
+ color: var(--muted);
151
+ text-align: right;
152
+ }
153
+
154
+ .progress-container {
155
+ margin-bottom: 0.75rem;
156
+ }
157
+
158
+ .progress-bar {
159
+ height: 8px;
160
+ border-radius: 4px;
161
+ background: #21262d;
162
+ overflow: hidden;
163
+ margin-bottom: 0.4rem;
164
+ }
165
+
166
+ .progress-fill {
167
+ height: 100%;
168
+ border-radius: 4px;
169
+ transition: width 0.5s ease;
170
+ }
171
+
172
+ .fill-green { background: var(--green); }
173
+ .fill-yellow { background: var(--yellow); }
174
+ .fill-red { background: var(--red); }
175
+
176
+ .progress-label {
177
+ display: flex;
178
+ justify-content: space-between;
179
+ font-size: 0.8rem;
180
+ }
181
+
182
+ .pct-value {
183
+ font-weight: 700;
184
+ }
185
+
186
+ .token-breakdown {
187
+ font-size: 0.78rem;
188
+ color: var(--muted);
189
+ display: flex;
190
+ gap: 1.2rem;
191
+ flex-wrap: wrap;
192
+ }
193
+
194
+ .token-item span:first-child {
195
+ color: var(--cyan);
196
+ font-weight: 600;
197
+ }
198
+
199
+ .warning-msg {
200
+ margin-top: 0.75rem;
201
+ font-size: 0.8rem;
202
+ padding: 0.4rem 0.75rem;
203
+ border-radius: 4px;
204
+ font-weight: 500;
205
+ }
206
+
207
+ .warning-yellow { background: rgba(247,201,72,0.12); color: var(--yellow); }
208
+ .warning-red { background: rgba(255,123,114,0.12); color: var(--red); }
209
+
210
+ .summary-section {
211
+ background: var(--surface);
212
+ border: 1px solid var(--border);
213
+ border-radius: 8px;
214
+ padding: 1.25rem 1.5rem;
215
+ margin-top: 1.5rem;
216
+ text-align: center;
217
+ }
218
+
219
+ .summary-title {
220
+ font-size: 0.8rem;
221
+ color: var(--muted);
222
+ text-transform: uppercase;
223
+ letter-spacing: 0.08em;
224
+ margin-bottom: 1rem;
225
+ }
226
+
227
+ .summary-stats {
228
+ display: flex;
229
+ justify-content: center;
230
+ gap: 2.5rem;
231
+ flex-wrap: wrap;
232
+ }
233
+
234
+ .summary-stat .num {
235
+ font-size: 1.6rem;
236
+ font-weight: 700;
237
+ color: var(--cyan);
238
+ display: block;
239
+ }
240
+
241
+ .summary-stat .label {
242
+ font-size: 0.75rem;
243
+ color: var(--muted);
244
+ }
245
+
246
+ .footer-links {
247
+ margin-top: 2rem;
248
+ text-align: center;
249
+ font-size: 0.8rem;
250
+ color: var(--muted);
251
+ }
252
+
253
+ .footer-links a {
254
+ color: var(--blue);
255
+ text-decoration: none;
256
+ margin: 0 0.5rem;
257
+ }
258
+
259
+ .status-icon {
260
+ margin-right: 0.4rem;
261
+ font-size: 0.9rem;
262
+ }
263
+
264
+ @media (max-width: 480px) {
265
+ .session-meta { flex-direction: column; gap: 0.25rem; }
266
+ .session-info { text-align: left; }
267
+ .summary-stats { gap: 1.5rem; }
268
+ }
269
+ </style>
270
+ </head>
271
+ <body>
272
+
273
+ <div class="header">
274
+ <h1>cc-context-check</h1>
275
+ <p>See how full your Claude Code context window is</p>
276
+ </div>
277
+
278
+ <div class="drop-zone" id="dropZone" onclick="document.getElementById('folderInput').click()">
279
+ <div class="icon">๐Ÿ“</div>
280
+ <h2>Drop your .claude folder here</h2>
281
+ <p>Or click to select it manually.<br>
282
+ Your data stays local โ€” nothing is uploaded.</p>
283
+ <input type="file" id="folderInput" webkitdirectory multiple>
284
+ <button class="btn" onclick="event.stopPropagation(); document.getElementById('folderInput').click()">
285
+ Select ~/.claude folder
286
+ </button>
287
+ </div>
288
+
289
+ <div id="results">
290
+ <div class="results-header" id="resultsHeader"></div>
291
+ <div id="sessionList"></div>
292
+ <div class="summary-section" id="summarySection"></div>
293
+ <div class="footer-links">
294
+ <a href="https://github.com/yurukusa/cc-context-check" target="_blank">GitHub</a> ยท
295
+ <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit (106 free tools)</a> ยท
296
+ <span>Also: <code>npx cc-context-check</code></span>
297
+ </div>
298
+ </div>
299
+
300
+ <div class="footer-links" id="footerMain">
301
+ <a href="https://github.com/yurukusa/cc-context-check" target="_blank">GitHub</a> ยท
302
+ <a href="https://yurukusa.github.io/cc-toolkit/" target="_blank">cc-toolkit</a> ยท
303
+ <span>Part of 106 free tools for Claude Code</span>
304
+ </div>
305
+
306
+ <script>
307
+ const CONTEXT_LIMIT = 200_000;
308
+ const WARN_PCT = 0.70;
309
+ const CRIT_PCT = 0.85;
310
+
311
+ const dropZone = document.getElementById('dropZone');
312
+ const folderInput = document.getElementById('folderInput');
313
+
314
+ dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); });
315
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
316
+ dropZone.addEventListener('drop', e => {
317
+ e.preventDefault();
318
+ dropZone.classList.remove('drag-over');
319
+ processFiles(Array.from(e.dataTransfer.files));
320
+ });
321
+
322
+ folderInput.addEventListener('change', e => processFiles(Array.from(e.target.files)));
323
+
324
+ function relTime(ms) {
325
+ const diff = Date.now() - ms;
326
+ const min = Math.floor(diff / 60000);
327
+ const hr = Math.floor(min / 60);
328
+ if (hr > 24) return Math.floor(hr / 24) + 'd ago';
329
+ if (hr > 0) return `${hr}h ${min % 60}m ago`;
330
+ if (min > 0) return `${min}m ago`;
331
+ return 'just now';
332
+ }
333
+
334
+ function fmtTokens(n) {
335
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
336
+ return String(n);
337
+ }
338
+
339
+ async function readLastLines(file, maxBytes = 65536) {
340
+ const size = file.size;
341
+ const start = Math.max(0, size - maxBytes);
342
+ const blob = file.slice(start, size);
343
+ return await blob.text();
344
+ }
345
+
346
+ function extractLatestUsage(text) {
347
+ const lines = text.split('\n').reverse();
348
+ for (const line of lines) {
349
+ if (!line.trim()) continue;
350
+ let d;
351
+ try { d = JSON.parse(line); } catch { continue; }
352
+ if (d.type !== 'assistant') continue;
353
+ const usage = d?.message?.usage;
354
+ if (!usage) continue;
355
+ const inputTokens = (usage.input_tokens || 0) +
356
+ (usage.cache_read_input_tokens || 0) +
357
+ (usage.cache_creation_input_tokens || 0);
358
+ if (inputTokens > 0) {
359
+ return { inputTokens, outputTokens: usage.output_tokens || 0 };
360
+ }
361
+ }
362
+ return null;
363
+ }
364
+
365
+ async function processFiles(files) {
366
+ const jsonlFiles = files.filter(f => f.name.endsWith('.jsonl') && f.webkitRelativePath.includes('projects'));
367
+ if (!jsonlFiles.length) {
368
+ alert('No session files found.\nMake sure to select your .claude folder (not a subfolder).');
369
+ return;
370
+ }
371
+
372
+ // Sort by last modified
373
+ jsonlFiles.sort((a, b) => b.lastModified - a.lastModified);
374
+ const top = jsonlFiles.slice(0, 20);
375
+
376
+ const sessions = [];
377
+ for (const f of top) {
378
+ const chunk = await readLastLines(f);
379
+ const usage = extractLatestUsage(chunk);
380
+ const pathParts = f.webkitRelativePath.split('/');
381
+ const projectPart = pathParts.slice(1, -1).join('/') || 'root';
382
+ const project = projectPart
383
+ .replace(/^projects\//, '')
384
+ .replace(/^-home-[^\/]+-?/, '~/')
385
+ .replace(/^~\/projects\//, '~/')
386
+ .slice(0, 45);
387
+ sessions.push({
388
+ project,
389
+ file: f.name.slice(0, 8),
390
+ mtime: f.lastModified,
391
+ size: f.size,
392
+ usage
393
+ });
394
+ }
395
+
396
+ renderResults(sessions);
397
+ }
398
+
399
+ function renderResults(sessions) {
400
+ document.getElementById('dropZone').style.display = 'none';
401
+ document.getElementById('footerMain').style.display = 'none';
402
+ const results = document.getElementById('results');
403
+ results.style.display = 'block';
404
+
405
+ const withData = sessions.filter(s => s.usage);
406
+ document.getElementById('resultsHeader').textContent =
407
+ `Analyzed ${sessions.length} sessions โ€” ${withData.length} with token data`;
408
+
409
+ const list = document.getElementById('sessionList');
410
+ list.innerHTML = '';
411
+
412
+ sessions.forEach(s => {
413
+ if (!s.usage) return;
414
+ const pct = s.usage.inputTokens / CONTEXT_LIMIT;
415
+ const pctPct = (pct * 100).toFixed(1);
416
+ const remaining = CONTEXT_LIMIT - s.usage.inputTokens;
417
+ const colorClass = pct >= CRIT_PCT ? 'fill-red' : pct >= WARN_PCT ? 'fill-yellow' : 'fill-green';
418
+ const icon = pct >= CRIT_PCT ? '๐Ÿ”ด' : pct >= WARN_PCT ? '๐ŸŸก' : '๐ŸŸข';
419
+ const sizeMB = (s.size / 1024 / 1024).toFixed(1);
420
+
421
+ let warningHtml = '';
422
+ if (pct >= CRIT_PCT) {
423
+ warningHtml = `<div class="warning-msg warning-red">โš  Critical: Run /compact soon to avoid context overflow</div>`;
424
+ } else if (pct >= WARN_PCT) {
425
+ warningHtml = `<div class="warning-msg warning-yellow">โ–ณ Warning: Context getting full โ€” consider /compact</div>`;
426
+ }
427
+
428
+ list.innerHTML += `
429
+ <div class="session-card">
430
+ <div class="session-meta">
431
+ <div class="session-project">
432
+ <span class="status-icon">${icon}</span>${s.project}
433
+ </div>
434
+ <div class="session-info">[${s.file}] ${relTime(s.mtime)}<br>${sizeMB} MB</div>
435
+ </div>
436
+ <div class="progress-container">
437
+ <div class="progress-bar">
438
+ <div class="progress-fill ${colorClass}" style="width:${Math.min(pct * 100, 100)}%"></div>
439
+ </div>
440
+ <div class="progress-label">
441
+ <span class="pct-value">${pctPct}% used</span>
442
+ <span style="color:var(--muted)">${fmtTokens(remaining)} remaining</span>
443
+ </div>
444
+ </div>
445
+ <div class="token-breakdown">
446
+ <div class="token-item"><span>${fmtTokens(s.usage.inputTokens)}</span> input tokens</div>
447
+ <div class="token-item"><span>${fmtTokens(s.usage.outputTokens)}</span> output tokens</div>
448
+ <div class="token-item"><span>${fmtTokens(CONTEXT_LIMIT)}</span> limit</div>
449
+ </div>
450
+ ${warningHtml}
451
+ </div>`;
452
+ });
453
+
454
+ // Summary
455
+ const avgPct = withData.length > 0
456
+ ? (withData.reduce((sum, s) => sum + s.usage.inputTokens / CONTEXT_LIMIT, 0) / withData.length * 100).toFixed(0)
457
+ : 0;
458
+ const critCount = withData.filter(s => s.usage.inputTokens / CONTEXT_LIMIT >= CRIT_PCT).length;
459
+ const warnCount = withData.filter(s => s.usage.inputTokens / CONTEXT_LIMIT >= WARN_PCT && s.usage.inputTokens / CONTEXT_LIMIT < CRIT_PCT).length;
460
+ const maxTokens = withData.length > 0 ? Math.max(...withData.map(s => s.usage.inputTokens)) : 0;
461
+
462
+ document.getElementById('summarySection').innerHTML = `
463
+ <div class="summary-title">Summary</div>
464
+ <div class="summary-stats">
465
+ <div class="summary-stat">
466
+ <span class="num">${withData.length}</span>
467
+ <span class="label">sessions analyzed</span>
468
+ </div>
469
+ <div class="summary-stat">
470
+ <span class="num">${avgPct}%</span>
471
+ <span class="label">avg context used</span>
472
+ </div>
473
+ <div class="summary-stat">
474
+ <span class="num" style="color:var(--red)">${critCount}</span>
475
+ <span class="label">need /compact now</span>
476
+ </div>
477
+ <div class="summary-stat">
478
+ <span class="num">${fmtTokens(maxTokens)}</span>
479
+ <span class="label">max tokens used</span>
480
+ </div>
481
+ </div>
482
+ `;
483
+ }
484
+ </script>
485
+ </body>
486
+ </html>
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "cc-context-check",
3
+ "version": "1.0.0",
4
+ "description": "See how full your Claude Code context window is โ€” reads token usage from session transcripts",
5
+ "main": "cli.mjs",
6
+ "bin": {
7
+ "cc-context-check": "./cli.mjs"
8
+ },
9
+ "scripts": {
10
+ "start": "node cli.mjs"
11
+ },
12
+ "keywords": [
13
+ "claude-code",
14
+ "claude",
15
+ "ai",
16
+ "context-window",
17
+ "token-usage",
18
+ "developer-tools",
19
+ "cli"
20
+ ],
21
+ "author": "yurukusa",
22
+ "license": "MIT",
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=18"
26
+ }
27
+ }