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.
- package/README.md +54 -0
- package/cli.mjs +200 -0
- package/index.html +486 -0
- 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
|
+
}
|