claude-contextline 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 +71 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +230 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# claude-contextline
|
|
2
|
+
|
|
3
|
+
A powerline-style statusline for Claude Code showing context window usage.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g claude-contextline
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use with npx:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx claude-contextline
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
Add to your Claude Code settings (`~/.claude/settings.json`):
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"statusLine": {
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": "claude-contextline"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or with npx:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"statusLine": {
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "npx claude-contextline"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Directory** - Shows current project/directory name
|
|
44
|
+
- **Git** - Shows branch name with dirty indicator (●)
|
|
45
|
+
- **Model** - Shows active Claude model
|
|
46
|
+
- **Context** - Shows context window usage percentage
|
|
47
|
+
|
|
48
|
+
## Display
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
myproject main ● ✱ Opus 4.5 ◫ 21%
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Color States
|
|
55
|
+
|
|
56
|
+
- **Normal** (<80%): Sky blue text on dark background
|
|
57
|
+
- **Warning** (≥80%): White text on orange background
|
|
58
|
+
- **Critical** (≥100%): White text on red background
|
|
59
|
+
|
|
60
|
+
## Requirements
|
|
61
|
+
|
|
62
|
+
- Node.js 18+
|
|
63
|
+
- A terminal with powerline font support (for arrow glyphs)
|
|
64
|
+
|
|
65
|
+
## Credits
|
|
66
|
+
|
|
67
|
+
Styling based on [claude-limitline](https://github.com/tylergraydev/claude-limitline).
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/claude-hook.ts
|
|
4
|
+
async function readHookData(timeoutMs = 1e3) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
let data = "";
|
|
7
|
+
const timeout = setTimeout(() => {
|
|
8
|
+
process.stdin.removeAllListeners();
|
|
9
|
+
process.stdin.pause();
|
|
10
|
+
resolve({});
|
|
11
|
+
}, timeoutMs);
|
|
12
|
+
if (process.stdin.isTTY) {
|
|
13
|
+
clearTimeout(timeout);
|
|
14
|
+
resolve({});
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
process.stdin.setEncoding("utf8");
|
|
18
|
+
process.stdin.on("data", (chunk) => {
|
|
19
|
+
data += chunk;
|
|
20
|
+
});
|
|
21
|
+
process.stdin.on("end", () => {
|
|
22
|
+
clearTimeout(timeout);
|
|
23
|
+
try {
|
|
24
|
+
const parsed = data.trim() ? JSON.parse(data) : {};
|
|
25
|
+
resolve(parsed);
|
|
26
|
+
} catch {
|
|
27
|
+
resolve({});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
process.stdin.on("error", () => {
|
|
31
|
+
clearTimeout(timeout);
|
|
32
|
+
resolve({});
|
|
33
|
+
});
|
|
34
|
+
process.stdin.resume();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/utils/environment.ts
|
|
39
|
+
import { execSync } from "child_process";
|
|
40
|
+
import { basename } from "path";
|
|
41
|
+
function getEnvironmentInfo(hookData) {
|
|
42
|
+
const cwd = hookData.workspace?.current_dir || hookData.cwd || process.cwd();
|
|
43
|
+
return {
|
|
44
|
+
directory: getDirectoryName(cwd),
|
|
45
|
+
gitBranch: getGitBranch(cwd),
|
|
46
|
+
gitDirty: isGitDirty(cwd),
|
|
47
|
+
model: getModelName(hookData),
|
|
48
|
+
contextPercent: getContextPercent(hookData)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function getDirectoryName(cwd) {
|
|
52
|
+
const name = basename(cwd);
|
|
53
|
+
return name || "/";
|
|
54
|
+
}
|
|
55
|
+
function getGitBranch(cwd) {
|
|
56
|
+
try {
|
|
57
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
58
|
+
cwd,
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
61
|
+
}).trim();
|
|
62
|
+
if (branch === "HEAD") {
|
|
63
|
+
return execSync("git rev-parse --short HEAD", {
|
|
64
|
+
cwd,
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
67
|
+
}).trim();
|
|
68
|
+
}
|
|
69
|
+
return branch;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function isGitDirty(cwd) {
|
|
75
|
+
try {
|
|
76
|
+
const status = execSync("git status --porcelain", {
|
|
77
|
+
cwd,
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
80
|
+
});
|
|
81
|
+
return status.trim().length > 0;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function getModelName(hookData) {
|
|
87
|
+
const displayName = hookData.model?.display_name || "Claude";
|
|
88
|
+
return displayName.replace(/^Claude\s+/, "");
|
|
89
|
+
}
|
|
90
|
+
function getContextPercent(hookData) {
|
|
91
|
+
const ctx = hookData.context_window;
|
|
92
|
+
if (!ctx?.current_usage || !ctx.context_window_size) {
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
const usage = ctx.current_usage;
|
|
96
|
+
const totalTokens = (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
97
|
+
return Math.round(totalTokens / ctx.context_window_size * 100);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/utils/constants.ts
|
|
101
|
+
var SYMBOLS = {
|
|
102
|
+
arrow: "\uE0B0",
|
|
103
|
+
// Powerline right arrow
|
|
104
|
+
branch: "\uE0A0",
|
|
105
|
+
// Git branch icon
|
|
106
|
+
model: "\u2731",
|
|
107
|
+
// Heavy asterisk ✱
|
|
108
|
+
context: "\u25EB",
|
|
109
|
+
// White square with vertical bisecting line ◫
|
|
110
|
+
dirty: "\u25CF"
|
|
111
|
+
// Dirty indicator
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/themes/index.ts
|
|
115
|
+
var darkTheme = {
|
|
116
|
+
directory: { bg: "#8b4513", fg: "#ffffff" },
|
|
117
|
+
// Brown, white
|
|
118
|
+
git: { bg: "#404040", fg: "#ffffff" },
|
|
119
|
+
// Dark gray, white
|
|
120
|
+
model: { bg: "#2d2d2d", fg: "#ffffff" },
|
|
121
|
+
// Very dark gray, white
|
|
122
|
+
context: { bg: "#2a2a2a", fg: "#87ceeb" },
|
|
123
|
+
// Nearly black, sky blue
|
|
124
|
+
warning: { bg: "#d75f00", fg: "#ffffff" },
|
|
125
|
+
// Orange, white (80%+)
|
|
126
|
+
critical: { bg: "#af0000", fg: "#ffffff" }
|
|
127
|
+
// Red, white (100%+)
|
|
128
|
+
};
|
|
129
|
+
function hexToAnsi256(hex) {
|
|
130
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
131
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
132
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
133
|
+
if (r === g && g === b) {
|
|
134
|
+
if (r < 8) return 16;
|
|
135
|
+
if (r > 248) return 231;
|
|
136
|
+
return Math.round((r - 8) / 247 * 24) + 232;
|
|
137
|
+
}
|
|
138
|
+
const ri = Math.round(r / 255 * 5);
|
|
139
|
+
const gi = Math.round(g / 255 * 5);
|
|
140
|
+
const bi = Math.round(b / 255 * 5);
|
|
141
|
+
return 16 + 36 * ri + 6 * gi + bi;
|
|
142
|
+
}
|
|
143
|
+
var ansi = {
|
|
144
|
+
fg: (hex) => `\x1B[38;5;${hexToAnsi256(hex)}m`,
|
|
145
|
+
bg: (hex) => `\x1B[48;5;${hexToAnsi256(hex)}m`,
|
|
146
|
+
reset: "\x1B[0m"
|
|
147
|
+
};
|
|
148
|
+
function getContextColors(percent) {
|
|
149
|
+
if (percent >= 100) {
|
|
150
|
+
return darkTheme.critical;
|
|
151
|
+
} else if (percent >= 80) {
|
|
152
|
+
return darkTheme.warning;
|
|
153
|
+
}
|
|
154
|
+
return darkTheme.context;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/renderer.ts
|
|
158
|
+
var Renderer = class {
|
|
159
|
+
symbols = SYMBOLS;
|
|
160
|
+
/**
|
|
161
|
+
* Render the complete statusline
|
|
162
|
+
*/
|
|
163
|
+
render(envInfo) {
|
|
164
|
+
const segments = this.buildSegments(envInfo);
|
|
165
|
+
if (segments.length === 0) {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
return this.renderPowerline(segments);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Build segments based on environment info
|
|
172
|
+
*/
|
|
173
|
+
buildSegments(envInfo) {
|
|
174
|
+
const segments = [];
|
|
175
|
+
segments.push({
|
|
176
|
+
text: ` ${envInfo.directory} `,
|
|
177
|
+
colors: darkTheme.directory
|
|
178
|
+
});
|
|
179
|
+
if (envInfo.gitBranch) {
|
|
180
|
+
const dirty = envInfo.gitDirty ? ` ${this.symbols.dirty}` : "";
|
|
181
|
+
segments.push({
|
|
182
|
+
text: ` ${this.symbols.branch} ${envInfo.gitBranch}${dirty} `,
|
|
183
|
+
colors: darkTheme.git
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
segments.push({
|
|
187
|
+
text: ` ${this.symbols.model} ${envInfo.model} `,
|
|
188
|
+
colors: darkTheme.model
|
|
189
|
+
});
|
|
190
|
+
const contextColors = getContextColors(envInfo.contextPercent);
|
|
191
|
+
segments.push({
|
|
192
|
+
text: ` ${this.symbols.context} ${envInfo.contextPercent}% `,
|
|
193
|
+
colors: contextColors
|
|
194
|
+
});
|
|
195
|
+
return segments;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Render segments with powerline arrows
|
|
199
|
+
*/
|
|
200
|
+
renderPowerline(segments) {
|
|
201
|
+
let output = "";
|
|
202
|
+
for (let i = 0; i < segments.length; i++) {
|
|
203
|
+
const seg = segments[i];
|
|
204
|
+
const nextColors = i < segments.length - 1 ? segments[i + 1].colors : null;
|
|
205
|
+
output += ansi.bg(seg.colors.bg) + ansi.fg(seg.colors.fg) + seg.text;
|
|
206
|
+
output += ansi.reset;
|
|
207
|
+
if (nextColors) {
|
|
208
|
+
output += ansi.fg(seg.colors.bg) + ansi.bg(nextColors.bg) + this.symbols.arrow;
|
|
209
|
+
} else {
|
|
210
|
+
output += ansi.fg(seg.colors.bg) + this.symbols.arrow;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
output += ansi.reset;
|
|
214
|
+
return output;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// src/index.ts
|
|
219
|
+
async function main() {
|
|
220
|
+
try {
|
|
221
|
+
const hookData = await readHookData();
|
|
222
|
+
const envInfo = getEnvironmentInfo(hookData);
|
|
223
|
+
const renderer = new Renderer();
|
|
224
|
+
const output = renderer.render(envInfo);
|
|
225
|
+
process.stdout.write(output);
|
|
226
|
+
} catch {
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-contextline",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Powerline statusline for Claude Code showing context window usage",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"claude-contextline": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
18
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"claude",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"statusline",
|
|
28
|
+
"powerline",
|
|
29
|
+
"context-window",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"author": "",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.10.0",
|
|
36
|
+
"tsup": "^8.0.0",
|
|
37
|
+
"typescript": "^5.3.0",
|
|
38
|
+
"vitest": "^2.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|