claude-contextline 2.0.0 → 2.1.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 +8 -1
- package/dist/index.js +203 -159
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,8 +23,15 @@ Add to your Claude Code settings (`~/.claude/settings.json`):
|
|
|
23
23
|
(main) myproject
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Colors track your active [herdr](https://github.com/) theme, read from
|
|
27
|
+
`~/.config/herdr/config.toml` (respects `$XDG_CONFIG_HOME` and
|
|
28
|
+
`$HERDR_CONFIG_PATH`) on every render — including `[theme.custom]` overrides.
|
|
29
|
+
All of herdr's built-in themes are supported (catppuccin, tokyo-night, dracula,
|
|
30
|
+
nord, gruvbox, solarized, kanagawa, rosé-pine, vesper, …); if no config is found
|
|
31
|
+
it falls back to Catppuccin Mocha.
|
|
32
|
+
|
|
26
33
|
- **Line 1**: Context window battery bar (blue) with usage percentage, model name (red)
|
|
27
|
-
- **Line 2**: Git branch (
|
|
34
|
+
- **Line 2**: Git branch (mauve), working directory (blue) aligned below the model
|
|
28
35
|
|
|
29
36
|
## Requirements
|
|
30
37
|
|
package/dist/index.js
CHANGED
|
@@ -43,9 +43,8 @@ function getEnvironmentInfo(hookData) {
|
|
|
43
43
|
return {
|
|
44
44
|
directory: getDirectoryName(cwd),
|
|
45
45
|
gitBranch: getGitBranch(cwd),
|
|
46
|
-
gitDirty: isGitDirty(cwd),
|
|
47
46
|
model: getModelName(hookData),
|
|
48
|
-
|
|
47
|
+
usedPercentage: getUsedPercentage(hookData)
|
|
49
48
|
};
|
|
50
49
|
}
|
|
51
50
|
function getDirectoryName(cwd) {
|
|
@@ -57,198 +56,243 @@ function getGitBranch(cwd) {
|
|
|
57
56
|
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
58
57
|
cwd,
|
|
59
58
|
encoding: "utf8",
|
|
60
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
59
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
60
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }
|
|
61
61
|
}).trim();
|
|
62
62
|
if (branch === "HEAD") {
|
|
63
|
-
return
|
|
64
|
-
cwd,
|
|
65
|
-
encoding: "utf8",
|
|
66
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
67
|
-
}).trim();
|
|
63
|
+
return null;
|
|
68
64
|
}
|
|
69
65
|
return branch;
|
|
70
66
|
} catch {
|
|
71
67
|
return null;
|
|
72
68
|
}
|
|
73
69
|
}
|
|
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
70
|
function getModelName(hookData) {
|
|
87
71
|
const displayName = hookData.model?.display_name || "Claude";
|
|
88
72
|
return displayName.replace(/^Claude\s+/, "");
|
|
89
73
|
}
|
|
90
|
-
function
|
|
74
|
+
function getUsedPercentage(hookData) {
|
|
91
75
|
const ctx = hookData.context_window;
|
|
92
|
-
if (!ctx
|
|
93
|
-
|
|
76
|
+
if (!ctx) return null;
|
|
77
|
+
if (ctx.used_percentage != null) {
|
|
78
|
+
return Math.floor(ctx.used_percentage);
|
|
79
|
+
}
|
|
80
|
+
if (ctx.current_usage && ctx.context_window_size) {
|
|
81
|
+
const usage = ctx.current_usage;
|
|
82
|
+
const totalTokens = (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
83
|
+
return Math.floor(totalTokens / ctx.context_window_size * 100);
|
|
94
84
|
}
|
|
95
|
-
|
|
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);
|
|
85
|
+
return null;
|
|
98
86
|
}
|
|
99
87
|
|
|
100
|
-
// src/
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
88
|
+
// src/theme.ts
|
|
89
|
+
import { readFileSync } from "fs";
|
|
90
|
+
import { homedir } from "os";
|
|
91
|
+
import { join } from "path";
|
|
92
|
+
var rgb = (r, g, b) => ({ kind: "rgb", r, g, b });
|
|
93
|
+
var ansi = (code) => ({ kind: "ansi", code });
|
|
94
|
+
var RESET_COLOR = { kind: "reset" };
|
|
95
|
+
var PALETTES = {
|
|
96
|
+
catppuccin: { blue: rgb(137, 180, 250), red: rgb(243, 139, 168), mauve: rgb(203, 166, 247), overlay0: rgb(108, 112, 134) },
|
|
97
|
+
"catppuccin-latte": { blue: rgb(30, 102, 245), red: rgb(210, 15, 57), mauve: rgb(136, 57, 239), overlay0: rgb(156, 160, 176) },
|
|
98
|
+
terminal: { blue: ansi(34), red: ansi(91), mauve: ansi(37), overlay0: ansi(37) },
|
|
99
|
+
"tokyo-night": { blue: rgb(122, 162, 247), red: rgb(247, 118, 142), mauve: rgb(187, 154, 247), overlay0: rgb(86, 95, 137) },
|
|
100
|
+
"tokyo-night-day": { blue: rgb(46, 125, 233), red: rgb(245, 42, 101), mauve: rgb(120, 71, 189), overlay0: rgb(137, 144, 179) },
|
|
101
|
+
dracula: { blue: rgb(139, 233, 253), red: rgb(255, 85, 85), mauve: rgb(255, 121, 198), overlay0: rgb(98, 114, 164) },
|
|
102
|
+
nord: { blue: rgb(129, 161, 193), red: rgb(191, 97, 106), mauve: rgb(180, 142, 173), overlay0: rgb(76, 86, 106) },
|
|
103
|
+
gruvbox: { blue: rgb(131, 165, 152), red: rgb(251, 73, 52), mauve: rgb(211, 134, 155), overlay0: rgb(146, 131, 116) },
|
|
104
|
+
"gruvbox-light": { blue: rgb(7, 102, 120), red: rgb(157, 0, 6), mauve: rgb(143, 63, 113), overlay0: rgb(146, 131, 116) },
|
|
105
|
+
"one-dark": { blue: rgb(97, 175, 239), red: rgb(224, 108, 117), mauve: rgb(198, 120, 221), overlay0: rgb(92, 99, 112) },
|
|
106
|
+
"one-light": { blue: rgb(64, 120, 242), red: rgb(228, 86, 73), mauve: rgb(166, 38, 164), overlay0: rgb(160, 161, 167) },
|
|
107
|
+
solarized: { blue: rgb(38, 139, 210), red: rgb(220, 50, 47), mauve: rgb(211, 54, 130), overlay0: rgb(88, 110, 117) },
|
|
108
|
+
"solarized-light": { blue: rgb(38, 139, 210), red: rgb(220, 50, 47), mauve: rgb(211, 54, 130), overlay0: rgb(147, 161, 161) },
|
|
109
|
+
kanagawa: { blue: rgb(126, 156, 216), red: rgb(195, 64, 67), mauve: rgb(149, 127, 184), overlay0: rgb(114, 113, 105) },
|
|
110
|
+
"kanagawa-lotus": { blue: rgb(77, 105, 155), red: rgb(200, 64, 83), mauve: rgb(98, 76, 131), overlay0: rgb(160, 156, 172) },
|
|
111
|
+
"rose-pine": { blue: rgb(49, 116, 143), red: rgb(235, 111, 146), mauve: rgb(196, 167, 231), overlay0: rgb(110, 106, 134) },
|
|
112
|
+
"rose-pine-dawn": { blue: rgb(40, 105, 131), red: rgb(180, 99, 122), mauve: rgb(144, 122, 169), overlay0: rgb(152, 147, 165) },
|
|
113
|
+
vesper: { blue: rgb(176, 176, 176), red: rgb(255, 128, 128), mauve: rgb(255, 209, 168), overlay0: rgb(92, 92, 92) }
|
|
112
114
|
};
|
|
113
|
-
var
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
var ALIASES = {
|
|
116
|
+
"catppuccin-mocha": "catppuccin",
|
|
117
|
+
latte: "catppuccin-latte",
|
|
118
|
+
light: "catppuccin-latte",
|
|
119
|
+
tokyonight: "tokyo-night",
|
|
120
|
+
"tokyo-day": "tokyo-night-day",
|
|
121
|
+
"tokyonight-day": "tokyo-night-day",
|
|
122
|
+
"gruvbox-dark": "gruvbox",
|
|
123
|
+
onedark: "one-dark",
|
|
124
|
+
onelight: "one-light",
|
|
125
|
+
"solarized-dark": "solarized",
|
|
126
|
+
lotus: "kanagawa-lotus",
|
|
127
|
+
rosepine: "rose-pine",
|
|
128
|
+
"rosepine-dawn": "rose-pine-dawn",
|
|
129
|
+
dawn: "rose-pine-dawn"
|
|
119
130
|
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
var DEFAULT_THEME = "catppuccin";
|
|
132
|
+
var NAMED_ANSI = {
|
|
133
|
+
black: 30,
|
|
134
|
+
red: 31,
|
|
135
|
+
green: 32,
|
|
136
|
+
yellow: 33,
|
|
137
|
+
blue: 34,
|
|
138
|
+
magenta: 35,
|
|
139
|
+
purple: 35,
|
|
140
|
+
cyan: 36,
|
|
141
|
+
gray: 37,
|
|
142
|
+
grey: 37,
|
|
143
|
+
white: 97,
|
|
144
|
+
darkgray: 90,
|
|
145
|
+
darkgrey: 90,
|
|
146
|
+
lightred: 91,
|
|
147
|
+
lightgreen: 92,
|
|
148
|
+
lightyellow: 93,
|
|
149
|
+
lightblue: 94,
|
|
150
|
+
lightmagenta: 95,
|
|
151
|
+
lightcyan: 96
|
|
135
152
|
};
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
153
|
+
function fg(color) {
|
|
154
|
+
switch (color.kind) {
|
|
155
|
+
case "rgb":
|
|
156
|
+
return `\x1B[38;2;${color.r};${color.g};${color.b}m`;
|
|
157
|
+
case "ansi":
|
|
158
|
+
return `\x1B[${color.code}m`;
|
|
159
|
+
case "reset":
|
|
160
|
+
return `\x1B[39m`;
|
|
144
161
|
}
|
|
145
|
-
const ri = Math.round(r / 255 * 5);
|
|
146
|
-
const gi = Math.round(g / 255 * 5);
|
|
147
|
-
const bi = Math.round(b / 255 * 5);
|
|
148
|
-
return 16 + 36 * ri + 6 * gi + bi;
|
|
149
162
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
};
|
|
155
|
-
function getContextColors(percent) {
|
|
156
|
-
if (percent >= 100) {
|
|
157
|
-
return darkTheme.critical;
|
|
158
|
-
} else if (percent >= 80) {
|
|
159
|
-
return darkTheme.warning;
|
|
163
|
+
function parseColor(input) {
|
|
164
|
+
const s = input.trim().toLowerCase();
|
|
165
|
+
if (s === "reset" || s === "default" || s === "none" || s === "transparent") {
|
|
166
|
+
return RESET_COLOR;
|
|
160
167
|
}
|
|
161
|
-
|
|
168
|
+
if (s.startsWith("#")) {
|
|
169
|
+
const hex = s.slice(1);
|
|
170
|
+
if (/^[0-9a-f]{6}$/.test(hex)) {
|
|
171
|
+
return rgb(parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16));
|
|
172
|
+
}
|
|
173
|
+
if (/^[0-9a-f]{3}$/.test(hex)) {
|
|
174
|
+
return rgb(parseInt(hex[0], 16) * 17, parseInt(hex[1], 16) * 17, parseInt(hex[2], 16) * 17);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const rgbMatch = s.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
|
178
|
+
if (rgbMatch) {
|
|
179
|
+
return rgb(Number(rgbMatch[1]), Number(rgbMatch[2]), Number(rgbMatch[3]));
|
|
180
|
+
}
|
|
181
|
+
if (s in NAMED_ANSI) {
|
|
182
|
+
return ansi(NAMED_ANSI[s]);
|
|
183
|
+
}
|
|
184
|
+
return ansi(36);
|
|
162
185
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
|
|
169
|
-
const nerdFontTerminals = [
|
|
170
|
-
"warp",
|
|
171
|
-
"iterm",
|
|
172
|
-
"hyper",
|
|
173
|
-
"kitty",
|
|
174
|
-
"alacritty",
|
|
175
|
-
"ghostty"
|
|
176
|
-
];
|
|
177
|
-
return nerdFontTerminals.some((t) => termProgram.includes(t));
|
|
186
|
+
function configPaths() {
|
|
187
|
+
const override = process.env.HERDR_CONFIG_PATH;
|
|
188
|
+
if (override) return [override];
|
|
189
|
+
const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
190
|
+
return [join(base, "herdr", "config.toml"), join(base, "herdr-dev", "config.toml")];
|
|
178
191
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const segments = this.buildSegments(envInfo);
|
|
190
|
-
if (segments.length === 0) {
|
|
191
|
-
return "";
|
|
192
|
+
function parseThemeSection(toml) {
|
|
193
|
+
const result = { custom: {} };
|
|
194
|
+
let section = "";
|
|
195
|
+
for (const rawLine of toml.split(/\r?\n/)) {
|
|
196
|
+
const line = rawLine.trim();
|
|
197
|
+
if (!line || line.startsWith("#")) continue;
|
|
198
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
199
|
+
if (sectionMatch) {
|
|
200
|
+
section = sectionMatch[1].trim();
|
|
201
|
+
continue;
|
|
192
202
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
});
|
|
203
|
+
const eq = line.indexOf("=");
|
|
204
|
+
if (eq === -1) continue;
|
|
205
|
+
const key = line.slice(0, eq).trim();
|
|
206
|
+
let value = line.slice(eq + 1).trim();
|
|
207
|
+
if (value.startsWith('"') || value.startsWith("'")) {
|
|
208
|
+
const quote = value[0];
|
|
209
|
+
const end = value.indexOf(quote, 1);
|
|
210
|
+
if (end !== -1) value = value.slice(1, end);
|
|
211
|
+
} else {
|
|
212
|
+
const hash = value.indexOf("#");
|
|
213
|
+
if (hash !== -1) value = value.slice(0, hash).trim();
|
|
214
|
+
}
|
|
215
|
+
if (section === "theme" && key === "name") {
|
|
216
|
+
result.name = value;
|
|
217
|
+
} else if (section === "theme.custom") {
|
|
218
|
+
result.custom[key] = value;
|
|
210
219
|
}
|
|
211
|
-
segments.push({
|
|
212
|
-
text: ` ${this.symbols.model} ${envInfo.model} `,
|
|
213
|
-
colors: darkTheme.model
|
|
214
|
-
});
|
|
215
|
-
const contextColors = getContextColors(envInfo.contextPercent);
|
|
216
|
-
segments.push({
|
|
217
|
-
text: ` ${this.symbols.context} ${envInfo.contextPercent}% `,
|
|
218
|
-
colors: contextColors
|
|
219
|
-
});
|
|
220
|
-
return segments;
|
|
221
220
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
output += ansi.fg(seg.colors.bg) + this.symbols.arrow;
|
|
237
|
-
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
function resolvePalette(name) {
|
|
224
|
+
const normalized = (name ?? DEFAULT_THEME).toLowerCase().replace(/[ _]/g, "-");
|
|
225
|
+
const canonical = ALIASES[normalized] ?? normalized;
|
|
226
|
+
return PALETTES[canonical] ?? PALETTES[DEFAULT_THEME];
|
|
227
|
+
}
|
|
228
|
+
function loadHerdrTheme() {
|
|
229
|
+
let parsed = { custom: {} };
|
|
230
|
+
for (const path of configPaths()) {
|
|
231
|
+
try {
|
|
232
|
+
parsed = parseThemeSection(readFileSync(path, "utf8"));
|
|
233
|
+
break;
|
|
234
|
+
} catch {
|
|
238
235
|
}
|
|
239
|
-
output += ansi.reset;
|
|
240
|
-
return output;
|
|
241
236
|
}
|
|
242
|
-
};
|
|
237
|
+
const palette = { ...resolvePalette(parsed.name) };
|
|
238
|
+
for (const token of ["blue", "red", "mauve", "overlay0"]) {
|
|
239
|
+
const override = parsed.custom[token];
|
|
240
|
+
if (override) palette[token] = parseColor(override);
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
blue: fg(palette.blue),
|
|
244
|
+
red: fg(palette.red),
|
|
245
|
+
mauve: fg(palette.mauve),
|
|
246
|
+
overlay0: fg(palette.overlay0)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/renderer.ts
|
|
251
|
+
var RESET = "\x1B[0m";
|
|
252
|
+
var BAR_WIDTH = 10;
|
|
253
|
+
var FILLED_CHAR = "\u2588";
|
|
254
|
+
var EMPTY_CHAR = "\u2591";
|
|
255
|
+
function render(envInfo) {
|
|
256
|
+
const { blue: BLUE, red: RED, mauve: MAUVE, overlay0: GRAY } = loadHerdrTheme();
|
|
257
|
+
let out = "";
|
|
258
|
+
let modelCol = 0;
|
|
259
|
+
if (envInfo.usedPercentage != null) {
|
|
260
|
+
const pct = Math.max(0, envInfo.usedPercentage);
|
|
261
|
+
const pctStr = String(pct);
|
|
262
|
+
const nFilled = Math.min(
|
|
263
|
+
Math.floor(pct * BAR_WIDTH / 100),
|
|
264
|
+
BAR_WIDTH
|
|
265
|
+
);
|
|
266
|
+
const nEmpty = BAR_WIDTH - nFilled;
|
|
267
|
+
const filled = FILLED_CHAR.repeat(nFilled);
|
|
268
|
+
const empty = EMPTY_CHAR.repeat(nEmpty);
|
|
269
|
+
out += `${BLUE}[${filled}${GRAY}${empty}${BLUE}] ${pctStr}%`;
|
|
270
|
+
modelCol = 16 + pctStr.length;
|
|
271
|
+
}
|
|
272
|
+
if (out.length > 0) {
|
|
273
|
+
out += " ";
|
|
274
|
+
}
|
|
275
|
+
out += `${RED}${envInfo.model}`;
|
|
276
|
+
out += "\n";
|
|
277
|
+
if (envInfo.gitBranch) {
|
|
278
|
+
const branchText = `(${envInfo.gitBranch})`;
|
|
279
|
+
out += `${MAUVE}${branchText}`;
|
|
280
|
+
const gap = Math.max(2, modelCol - branchText.length);
|
|
281
|
+
out += " ".repeat(gap);
|
|
282
|
+
} else {
|
|
283
|
+
out += " ".repeat(modelCol);
|
|
284
|
+
}
|
|
285
|
+
out += `${BLUE}${envInfo.directory}`;
|
|
286
|
+
out += RESET;
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
243
289
|
|
|
244
290
|
// src/index.ts
|
|
245
291
|
async function main() {
|
|
246
292
|
try {
|
|
247
|
-
const noArrows = process.argv.includes("--no-arrows");
|
|
248
293
|
const hookData = await readHookData();
|
|
249
294
|
const envInfo = getEnvironmentInfo(hookData);
|
|
250
|
-
const
|
|
251
|
-
const output = renderer.render(envInfo);
|
|
295
|
+
const output = render(envInfo);
|
|
252
296
|
process.stdout.write(output);
|
|
253
297
|
} catch {
|
|
254
298
|
process.exit(0);
|