claude-contextline 2.0.0 → 2.0.1

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.
Files changed (2) hide show
  1. package/dist/index.js +52 -167
  2. package/package.json +1 -1
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
- contextPercent: getContextPercent(hookData)
47
+ usedPercentage: getUsedPercentage(hookData)
49
48
  };
50
49
  }
51
50
  function getDirectoryName(cwd) {
@@ -57,198 +56,84 @@ 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 execSync("git rev-parse --short HEAD", {
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 getContextPercent(hookData) {
74
+ function getUsedPercentage(hookData) {
91
75
  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
- var TEXT_SYMBOLS = {
114
- arrow: "",
115
- branch: "",
116
- model: "",
117
- context: "",
118
- dirty: "*"
119
- };
120
-
121
- // src/themes/index.ts
122
- var darkTheme = {
123
- directory: { bg: "#8b4513", fg: "#ffffff" },
124
- // Brown, white
125
- git: { bg: "#404040", fg: "#ffffff" },
126
- // Dark gray, white
127
- model: { bg: "#2d2d2d", fg: "#ffffff" },
128
- // Very dark gray, white
129
- context: { bg: "#2a2a2a", fg: "#87ceeb" },
130
- // Nearly black, sky blue
131
- warning: { bg: "#d75f00", fg: "#ffffff" },
132
- // Orange, white (80%+)
133
- critical: { bg: "#af0000", fg: "#ffffff" }
134
- // Red, white (100%+)
135
- };
136
- function hexToAnsi256(hex) {
137
- const r = parseInt(hex.slice(1, 3), 16);
138
- const g = parseInt(hex.slice(3, 5), 16);
139
- const b = parseInt(hex.slice(5, 7), 16);
140
- if (r === g && g === b) {
141
- if (r < 8) return 16;
142
- if (r > 248) return 231;
143
- return Math.round((r - 8) / 247 * 24) + 232;
76
+ if (!ctx) return null;
77
+ if (ctx.used_percentage != null) {
78
+ return Math.floor(ctx.used_percentage);
144
79
  }
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
- }
150
- var ansi = {
151
- fg: (hex) => `\x1B[38;5;${hexToAnsi256(hex)}m`,
152
- bg: (hex) => `\x1B[48;5;${hexToAnsi256(hex)}m`,
153
- reset: "\x1B[0m"
154
- };
155
- function getContextColors(percent) {
156
- if (percent >= 100) {
157
- return darkTheme.critical;
158
- } else if (percent >= 80) {
159
- return darkTheme.warning;
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);
160
84
  }
161
- return darkTheme.context;
85
+ return null;
162
86
  }
163
87
 
164
88
  // src/renderer.ts
165
- function detectNerdFontSupport() {
166
- if (process.env.NERD_FONTS === "1") return true;
167
- if (process.env.NERD_FONTS === "0") return false;
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));
178
- }
179
- var Renderer = class {
180
- symbols = detectNerdFontSupport() ? SYMBOLS : TEXT_SYMBOLS;
181
- noArrows;
182
- constructor(options = {}) {
183
- this.noArrows = options.noArrows ?? false;
89
+ var ESC = "\x1B";
90
+ var RESET = `${ESC}[0m`;
91
+ var BLUE = `${ESC}[38;5;69m`;
92
+ var RED = `${ESC}[38;5;196m`;
93
+ var GRAY = `${ESC}[38;5;243m`;
94
+ var BAR_WIDTH = 10;
95
+ var FILLED_CHAR = "\u2588";
96
+ var EMPTY_CHAR = "\u2591";
97
+ function render(envInfo) {
98
+ let out = "";
99
+ let modelCol = 0;
100
+ if (envInfo.usedPercentage != null) {
101
+ const pct = Math.max(0, envInfo.usedPercentage);
102
+ const pctStr = String(pct);
103
+ const nFilled = Math.min(
104
+ Math.floor(pct * BAR_WIDTH / 100),
105
+ BAR_WIDTH
106
+ );
107
+ const nEmpty = BAR_WIDTH - nFilled;
108
+ const filled = FILLED_CHAR.repeat(nFilled);
109
+ const empty = EMPTY_CHAR.repeat(nEmpty);
110
+ out += `${BLUE}[${filled}${GRAY}${empty}${BLUE}] ${pctStr}%`;
111
+ modelCol = 16 + pctStr.length;
184
112
  }
185
- /**
186
- * Render the complete statusline
187
- */
188
- render(envInfo) {
189
- const segments = this.buildSegments(envInfo);
190
- if (segments.length === 0) {
191
- return "";
192
- }
193
- return this.renderPowerline(segments);
113
+ if (out.length > 0) {
114
+ out += " ";
194
115
  }
195
- /**
196
- * Build segments based on environment info
197
- */
198
- buildSegments(envInfo) {
199
- const segments = [];
200
- segments.push({
201
- text: ` ${envInfo.directory} `,
202
- colors: darkTheme.directory
203
- });
204
- if (envInfo.gitBranch) {
205
- const dirty = envInfo.gitDirty ? ` ${this.symbols.dirty}` : "";
206
- segments.push({
207
- text: ` ${this.symbols.branch} ${envInfo.gitBranch}${dirty} `,
208
- colors: darkTheme.git
209
- });
210
- }
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;
116
+ out += `${RED}${envInfo.model}`;
117
+ out += "\n";
118
+ if (envInfo.gitBranch) {
119
+ const branchText = `(${envInfo.gitBranch})`;
120
+ out += `${RED}${branchText}`;
121
+ const gap = Math.max(2, modelCol - branchText.length);
122
+ out += " ".repeat(gap);
123
+ } else {
124
+ out += " ".repeat(modelCol);
221
125
  }
222
- /**
223
- * Render segments with powerline arrows
224
- */
225
- renderPowerline(segments) {
226
- let output = "";
227
- for (let i = 0; i < segments.length; i++) {
228
- const seg = segments[i];
229
- const nextColors = i < segments.length - 1 ? segments[i + 1].colors : null;
230
- output += ansi.bg(seg.colors.bg) + ansi.fg(seg.colors.fg) + seg.text;
231
- output += ansi.reset;
232
- if (this.noArrows) {
233
- } else if (nextColors) {
234
- output += ansi.fg(seg.colors.bg) + ansi.bg(nextColors.bg) + this.symbols.arrow;
235
- } else {
236
- output += ansi.fg(seg.colors.bg) + this.symbols.arrow;
237
- }
238
- }
239
- output += ansi.reset;
240
- return output;
241
- }
242
- };
126
+ out += `${BLUE}${envInfo.directory}`;
127
+ out += RESET;
128
+ return out;
129
+ }
243
130
 
244
131
  // src/index.ts
245
132
  async function main() {
246
133
  try {
247
- const noArrows = process.argv.includes("--no-arrows");
248
134
  const hookData = await readHookData();
249
135
  const envInfo = getEnvironmentInfo(hookData);
250
- const renderer = new Renderer({ noArrows });
251
- const output = renderer.render(envInfo);
136
+ const output = render(envInfo);
252
137
  process.stdout.write(output);
253
138
  } catch {
254
139
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-contextline",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Two-line statusline for Claude Code showing context window usage",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",