claude-context-meter 1.0.0 → 1.0.2
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 +29 -21
- package/context_meter.js +106 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,23 +3,30 @@
|
|
|
3
3
|
A Claude Code plugin that adds a persistent context window usage meter to the status line. After every API response you'll see something like:
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
Context: 24k (18%)
|
|
6
|
+
Context: 24k (18%) · [GitHub/my-project/main]
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Color-coded by pressure level:
|
|
10
10
|
|
|
11
|
-
|
|
|
12
|
-
|
|
13
|
-
| Green | < 70,000
|
|
14
|
-
| Yellow | 70,000
|
|
15
|
-
| Red | > 100,000
|
|
11
|
+
| Zone | Token range | Context numbers | VCS segment |
|
|
12
|
+
|--------|------------------|-----------------|---------------------|
|
|
13
|
+
| Green | < 70,000 | Green | Gray (clean) / Cyan (dirty) |
|
|
14
|
+
| Yellow | 70,000–100,000 | Yellow | Gray (clean) / Cyan (dirty) |
|
|
15
|
+
| Red | > 100,000 | Red | Gray (clean) / Cyan (dirty) |
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
In the red zone a suggestion is appended:
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
```
|
|
20
|
+
Context: 112k (56%) · Consider /compact or /clear · [GitHub/my-project/main]
|
|
21
|
+
```
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
**VCS segment** — if you're inside a git repository, the current platform, repo, and branch are shown at the end of the line. The segment turns cyan with a `*` when there are uncommitted changes:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Context: 24k (18%) · [GitHub/my-project/main*]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The status line updates automatically — no polling or background process required.
|
|
23
30
|
|
|
24
31
|
## Installation
|
|
25
32
|
|
|
@@ -27,35 +34,36 @@ The status line updates automatically — no polling or background process requi
|
|
|
27
34
|
npx claude-context-meter install
|
|
28
35
|
```
|
|
29
36
|
|
|
30
|
-
This copies the
|
|
37
|
+
This copies the plugin to `~/.claude/plugins/context-meter/` and updates `~/.claude/settings.json` automatically. Open a new Claude Code session to activate it.
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
## Uninstalling
|
|
39
|
+
## Uninstallation
|
|
35
40
|
|
|
36
41
|
```sh
|
|
37
42
|
npx claude-context-meter uninstall
|
|
38
43
|
```
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
This removes the plugin files and cleans up the `statusLine` entry from `~/.claude/settings.json`. Open a new Claude Code session to complete the removal.
|
|
46
|
+
|
|
47
|
+
## Prerequisites
|
|
48
|
+
|
|
49
|
+
- **Claude Code** (any version that supports the `statusLine` setting)
|
|
50
|
+
- **Node.js** ≥ 18 — already installed as part of Claude Code; no additional installation required
|
|
41
51
|
|
|
42
52
|
## Verifying the installation
|
|
43
53
|
|
|
44
|
-
After opening a new Claude Code session you should see a status line
|
|
54
|
+
After opening a new Claude Code session you should see a status line that reads:
|
|
45
55
|
|
|
46
56
|
```
|
|
47
|
-
Context: 0 (0%)
|
|
57
|
+
Context: 0 (0%) · [GitHub/my-project/main]
|
|
48
58
|
```
|
|
49
59
|
|
|
50
|
-
Send any message to Claude. After the API response the token count and percentage will update
|
|
60
|
+
Send any message to Claude. After the API response the token count and percentage will update. If the status line does not appear, check that:
|
|
51
61
|
|
|
52
|
-
- `~/.claude/settings.json` contains the correct `statusLine` entry (run `npx claude-context-meter install` to set it)
|
|
53
62
|
- You opened a **new** Claude Code session after running the installer
|
|
63
|
+
- `~/.claude/settings.json` contains a `statusLine` entry pointing to the plugin
|
|
54
64
|
|
|
55
65
|
## Running tests
|
|
56
66
|
|
|
57
67
|
```sh
|
|
58
68
|
node --test tests/test_context_meter.js
|
|
59
69
|
```
|
|
60
|
-
|
|
61
|
-
All 26 tests pass with no external dependencies — Node.js standard library only.
|
package/context_meter.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
4
6
|
const GREEN = '\x1b[32m';
|
|
5
7
|
const YELLOW = '\x1b[33m';
|
|
6
8
|
const RED = '\x1b[31m';
|
|
9
|
+
const GRAY = '\x1b[2m';
|
|
10
|
+
const CYAN = '\x1b[36m';
|
|
7
11
|
const RESET = '\x1b[0m';
|
|
8
12
|
|
|
9
13
|
const GREEN_THRESHOLD = 70_000;
|
|
10
14
|
const YELLOW_THRESHOLD = 100_000;
|
|
11
|
-
const RED_ZONE_MSG = ' · Consider /compact or /clear';
|
|
12
15
|
|
|
13
16
|
const ANSI = { green: GREEN, yellow: YELLOW, red: RED };
|
|
14
17
|
|
|
18
|
+
const KNOWN_PLATFORMS = {
|
|
19
|
+
'github.com': 'GitHub',
|
|
20
|
+
'bitbucket.org': 'Bitbucket',
|
|
21
|
+
'gitlab.com': 'GitLab',
|
|
22
|
+
};
|
|
23
|
+
|
|
15
24
|
function formatTokens(count) {
|
|
16
25
|
if (count >= 1_000_000) return `${Math.round(count / 1_000_000)}M`;
|
|
17
26
|
if (count >= 1_000) return `${Math.round(count / 1_000)}k`;
|
|
@@ -36,13 +45,103 @@ function parseInput(data) {
|
|
|
36
45
|
}
|
|
37
46
|
}
|
|
38
47
|
|
|
39
|
-
function
|
|
48
|
+
function parseRemoteUrl(url) {
|
|
49
|
+
let host = null;
|
|
50
|
+
let repo = null;
|
|
51
|
+
|
|
52
|
+
const sshMatch = url.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
|
|
53
|
+
if (sshMatch) {
|
|
54
|
+
host = sshMatch[1];
|
|
55
|
+
repo = sshMatch[2].split('/').pop() || null;
|
|
56
|
+
} else {
|
|
57
|
+
try {
|
|
58
|
+
const u = new URL(url);
|
|
59
|
+
host = u.hostname || null;
|
|
60
|
+
const segments = u.pathname.replace(/^\//, '').replace(/\.git$/, '').split('/');
|
|
61
|
+
repo = segments.pop() || null;
|
|
62
|
+
} catch {
|
|
63
|
+
// unparseable remote URL
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const platform = host ? (KNOWN_PLATFORMS[host] || host) : null;
|
|
68
|
+
return { platform, repo };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function detectVcs(cwd = process.cwd(), exec = execSync) {
|
|
72
|
+
const run = cmd => exec(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
73
|
+
|
|
74
|
+
let branch;
|
|
75
|
+
try {
|
|
76
|
+
branch = run('git branch --show-current');
|
|
77
|
+
} catch {
|
|
78
|
+
return { type: 'none' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let platform = null;
|
|
82
|
+
let repo = null;
|
|
83
|
+
try {
|
|
84
|
+
({ platform, repo } = parseRemoteUrl(run('git remote get-url origin')));
|
|
85
|
+
} catch {
|
|
86
|
+
// no remote configured
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let dirty = false;
|
|
90
|
+
try {
|
|
91
|
+
dirty = run('git status --porcelain').length > 0;
|
|
92
|
+
} catch {
|
|
93
|
+
// default to clean if git status fails
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!branch) {
|
|
97
|
+
let hash = 'HEAD';
|
|
98
|
+
try { hash = run('git rev-parse --short HEAD'); } catch { /* use fallback */ }
|
|
99
|
+
return { type: 'detached', platform, repo, hash, dirty };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { type: 'branch', platform, repo, branch, dirty };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatVcs(vcsState) {
|
|
106
|
+
if (vcsState.type === 'none') {
|
|
107
|
+
return `${GRAY} · [No version control]${RESET}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { dirty } = vcsState;
|
|
111
|
+
const color = dirty ? CYAN : GRAY;
|
|
112
|
+
const star = dirty ? '*' : '';
|
|
113
|
+
|
|
114
|
+
if (vcsState.type === 'detached') {
|
|
115
|
+
const bracketParts = [vcsState.platform, vcsState.repo].filter(Boolean);
|
|
116
|
+
const bracket = bracketParts.length ? `[${bracketParts.join('/')}] ` : '';
|
|
117
|
+
const content = `${bracket}(HEAD detached at ${vcsState.hash})${star}`;
|
|
118
|
+
return dirty
|
|
119
|
+
? `${GRAY} · ${RESET}${color}${content}${RESET}`
|
|
120
|
+
: `${GRAY} · ${content}${RESET}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const parts = [vcsState.platform, vcsState.repo, vcsState.branch].filter(Boolean);
|
|
124
|
+
const content = `[${parts.join('/')}${star}]`;
|
|
125
|
+
return dirty
|
|
126
|
+
? `${GRAY} · ${RESET}${color}${content}${RESET}`
|
|
127
|
+
: `${GRAY} · ${content}${RESET}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function render(tokens, pct, vcsState = { type: 'none' }) {
|
|
40
131
|
const formatted = formatTokens(tokens);
|
|
41
132
|
const pctInt = Math.round(pct);
|
|
42
133
|
const category = classify(tokens);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
134
|
+
const zoneColor = ANSI[category];
|
|
135
|
+
|
|
136
|
+
let line = `${GRAY}Context: ${RESET}${zoneColor}${formatted} (${pctInt}%)${RESET}`;
|
|
137
|
+
|
|
138
|
+
if (category === 'red') {
|
|
139
|
+
line += `${GRAY} · Consider /compact or /clear${RESET}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
line += formatVcs(vcsState);
|
|
143
|
+
|
|
144
|
+
return line;
|
|
46
145
|
}
|
|
47
146
|
|
|
48
147
|
if (require.main === module) {
|
|
@@ -51,8 +150,8 @@ if (require.main === module) {
|
|
|
51
150
|
process.stdin.on('data', chunk => { data += chunk; });
|
|
52
151
|
process.stdin.on('end', () => {
|
|
53
152
|
const [tokens, pct] = parseInput(data);
|
|
54
|
-
console.log(render(tokens, pct));
|
|
153
|
+
console.log(render(tokens, pct, detectVcs()));
|
|
55
154
|
});
|
|
56
155
|
}
|
|
57
156
|
|
|
58
|
-
module.exports = { formatTokens, classify, parseInput, render };
|
|
157
|
+
module.exports = { formatTokens, classify, parseInput, detectVcs, formatVcs, render };
|