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 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
@@ -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
+ }