claude-limitline 1.0.1 → 1.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.
Files changed (3) hide show
  1. package/README.md +73 -63
  2. package/dist/index.js +343 -96
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,26 +1,36 @@
1
1
  # claude-limitline
2
2
 
3
- A statusline for Claude Code showing real-time usage limits and weekly tracking.
3
+ A powerline-style statusline for Claude Code showing real-time usage limits, git info, and model details.
4
4
 
5
5
  ![License](https://img.shields.io/badge/license-MIT-blue.svg)
6
6
  ![Node](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)
7
7
  ![TypeScript](https://img.shields.io/badge/typescript-5.3-blue.svg)
8
8
 
9
+ ![Theme Preview](imgs/themes-preview.png)
10
+
9
11
  ## Features
10
12
 
13
+ - **Powerline Style** - Beautiful segmented display with smooth transitions
11
14
  - **5-Hour Block Limit** - Shows current usage percentage with time remaining until reset
12
15
  - **7-Day Rolling Limit** - Tracks weekly usage with progress indicator
13
- - **Real-time Tracking** - Uses Anthropic's OAuth usage API for accurate usage data
14
- - **Progress Bar Display** - Visual progress bars for quick status checks
16
+ - **Repository Name** - Displays current project/directory name
17
+ - **Git Branch** - Shows current branch with dirty indicator (●)
18
+ - **Claude Model** - Displays the active model (Opus 4.5, Sonnet 4, etc.)
19
+ - **Multiple Themes** - Dark, light, nord, gruvbox, tokyo-night, and rose-pine
20
+ - **Real-time Tracking** - Uses Anthropic's OAuth usage API for accurate data
15
21
  - **Cross-Platform** - Works on Windows, macOS, and Linux
16
- - **Zero Runtime Dependencies** - Lightweight and fast
17
- - **Multiple Themes** - Dark, light, nord, and gruvbox themes included
22
+
23
+ ## Example Output
24
+
25
+ ```
26
+ claude-limitline main ● Opus 4.5 12% (3h20m) 45% (wk 85%)
27
+ ```
18
28
 
19
29
  ## Prerequisites
20
30
 
21
31
  - **Node.js** 18.0.0 or higher
22
32
  - **Claude Code** CLI installed and authenticated (for OAuth token)
23
- - **Nerd Font** (optional, for powerline symbols)
33
+ - **Nerd Font** (recommended, for powerline symbols)
24
34
 
25
35
  ## Installation
26
36
 
@@ -40,16 +50,6 @@ npm run build
40
50
  npm link
41
51
  ```
42
52
 
43
- ### Using Docker
44
-
45
- ```bash
46
- # Build the image
47
- docker build -t claude-limitline .
48
-
49
- # Run (mount your .claude directory for OAuth token access)
50
- docker run --rm -v ~/.claude:/root/.claude claude-limitline
51
- ```
52
-
53
53
  ## Quick Start
54
54
 
55
55
  The easiest way to use claude-limitline is to add it directly to your Claude Code settings.
@@ -83,9 +83,7 @@ Here's a complete example with other common settings:
83
83
  }
84
84
  ```
85
85
 
86
- ### Alternative: Global Install
87
-
88
- If you prefer a global installation (slightly faster startup):
86
+ ### Global Install (faster startup)
89
87
 
90
88
  ```bash
91
89
  npm install -g claude-limitline
@@ -107,12 +105,8 @@ Then update your settings:
107
105
  Run standalone to verify it's working:
108
106
 
109
107
  ```bash
110
- npx claude-limitline
111
- ```
112
-
113
- You should see output like:
114
- ```
115
- ⏳ ████████░░ 45% (2h 30m left) | 📅 ██████░░░░ 62% (wk 43%)
108
+ # Simulate Claude Code hook data
109
+ echo '{"model":{"id":"claude-opus-4-5-20251101"}}' | npx claude-limitline
116
110
  ```
117
111
 
118
112
  ## Configuration
@@ -122,18 +116,27 @@ Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitl
122
116
  ```json
123
117
  {
124
118
  "display": {
125
- "style": "minimal",
119
+ "style": "powerline",
126
120
  "useNerdFonts": true
127
121
  },
122
+ "directory": {
123
+ "enabled": true
124
+ },
125
+ "git": {
126
+ "enabled": true
127
+ },
128
+ "model": {
129
+ "enabled": true
130
+ },
128
131
  "block": {
129
132
  "enabled": true,
130
- "displayStyle": "bar",
133
+ "displayStyle": "text",
131
134
  "barWidth": 10,
132
135
  "showTimeRemaining": true
133
136
  },
134
137
  "weekly": {
135
138
  "enabled": true,
136
- "displayStyle": "bar",
139
+ "displayStyle": "text",
137
140
  "barWidth": 10,
138
141
  "showWeekProgress": true
139
142
  },
@@ -149,13 +152,16 @@ Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitl
149
152
 
150
153
  | Option | Description | Default |
151
154
  |--------|-------------|---------|
152
- | `display.useNerdFonts` | Use Nerd Font symbols (⏳📅) vs ASCII | `true` |
155
+ | `display.useNerdFonts` | Use Nerd Font symbols for powerline | `true` |
156
+ | `directory.enabled` | Show repository/directory name | `true` |
157
+ | `git.enabled` | Show git branch with dirty indicator | `true` |
158
+ | `model.enabled` | Show Claude model name | `true` |
153
159
  | `block.enabled` | Show 5-hour block usage | `true` |
154
- | `block.displayStyle` | `"bar"` or `"text"` | `"bar"` |
160
+ | `block.displayStyle` | `"bar"` or `"text"` | `"text"` |
155
161
  | `block.barWidth` | Width of progress bar in characters | `10` |
156
162
  | `block.showTimeRemaining` | Show time until block resets | `true` |
157
163
  | `weekly.enabled` | Show 7-day rolling usage | `true` |
158
- | `weekly.displayStyle` | `"bar"` or `"text"` | `"bar"` |
164
+ | `weekly.displayStyle` | `"bar"` or `"text"` | `"text"` |
159
165
  | `weekly.barWidth` | Width of progress bar in characters | `10` |
160
166
  | `weekly.showWeekProgress` | Show week progress percentage | `true` |
161
167
  | `budget.pollInterval` | Minutes between API calls | `15` |
@@ -164,14 +170,33 @@ Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitl
164
170
 
165
171
  ### Available Themes
166
172
 
167
- - `dark` - Default dark theme
168
- - `light` - Light background theme
173
+ - `dark` - Default dark theme with warm browns and cool cyans
174
+ - `light` - Light background theme with vibrant colors
169
175
  - `nord` - Nord color palette
170
176
  - `gruvbox` - Gruvbox color palette
177
+ - `tokyo-night` - Tokyo Night color palette
178
+ - `rose-pine` - Rosé Pine color palette
179
+
180
+ ## Segments
181
+
182
+ The statusline displays the following segments (all configurable):
183
+
184
+ | Segment | Description | Color (dark theme) |
185
+ |---------|-------------|-------------------|
186
+ | **Directory** | Current repo/project name | Brown/Orange |
187
+ | **Git** | Branch name + dirty indicator (●) | Dark Gray |
188
+ | **Model** | Claude model (Opus 4.5, Sonnet 4, etc.) | Dark Gray |
189
+ | **Block** | 5-hour usage % + time remaining | Cyan (warning: Orange, critical: Red) |
190
+ | **Weekly** | 7-day usage % + week progress | Green |
171
191
 
172
192
  ## How It Works
173
193
 
174
- claude-limitline retrieves your Claude usage data from Anthropic's OAuth usage API. It reads your OAuth token from:
194
+ claude-limitline retrieves data from two sources:
195
+
196
+ 1. **Hook Data (stdin)** - Claude Code passes JSON with model info, workspace, and session data
197
+ 2. **Usage API** - Fetches usage limits from Anthropic's OAuth usage endpoint
198
+
199
+ ### OAuth Token Location
175
200
 
176
201
  | Platform | Location |
177
202
  |----------|----------|
@@ -179,15 +204,6 @@ claude-limitline retrieves your Claude usage data from Anthropic's OAuth usage A
179
204
  | **macOS** | Keychain or `~/.claude/.credentials.json` |
180
205
  | **Linux** | secret-tool (GNOME Keyring) or `~/.claude/.credentials.json` |
181
206
 
182
- The usage data is cached locally to respect API rate limits. The cache duration is configurable via `budget.pollInterval` (default: 15 minutes).
183
-
184
- ### API Response
185
-
186
- The tool queries Anthropic's usage endpoint which returns:
187
-
188
- - **5-hour block**: Usage percentage and reset time for the rolling 5-hour window
189
- - **7-day rolling**: Usage percentage and reset time for the rolling 7-day window
190
-
191
207
  ## Development
192
208
 
193
209
  ### Setup
@@ -210,16 +226,13 @@ npm run build
210
226
  npm run dev
211
227
  ```
212
228
 
213
- ### Type Checking
214
-
215
- ```bash
216
- npm run typecheck
217
- ```
218
-
219
229
  ### Run Locally
220
230
 
221
231
  ```bash
222
232
  node dist/index.js
233
+
234
+ # With simulated hook data
235
+ echo '{"model":{"id":"claude-opus-4-5-20251101"}}' | node dist/index.js
223
236
  ```
224
237
 
225
238
  ## Debug Mode
@@ -241,25 +254,22 @@ Debug output is written to stderr so it won't interfere with the status line out
241
254
 
242
255
  ## Troubleshooting
243
256
 
244
- ### "No data" or empty output
245
-
246
- 1. **Check OAuth token**: Make sure you're logged into Claude Code (`claude --login`)
247
- 2. **Check credentials file**: Verify `~/.claude/.credentials.json` exists and contains `claudeAiOauth.accessToken`
248
- 3. **Enable debug mode**: Run with `CLAUDE_LIMITLINE_DEBUG=true` to see detailed logs
249
-
250
- ### Token not found
251
-
252
- The OAuth token is stored by Claude Code when you authenticate. Try:
257
+ ### Model not showing
253
258
 
259
+ The model is passed via stdin from Claude Code. If running standalone, pipe in hook data:
254
260
  ```bash
255
- # Re-authenticate with Claude Code
256
- claude --login
261
+ echo '{"model":{"id":"claude-opus-4-5-20251101"}}' | claude-limitline
257
262
  ```
258
263
 
259
- ### API returns errors
264
+ ### "No data" or empty output
265
+
266
+ 1. **Check OAuth token**: Make sure you're logged into Claude Code (`claude --login`)
267
+ 2. **Check credentials file**: Verify `~/.claude/.credentials.json` exists
268
+ 3. **Enable debug mode**: Run with `CLAUDE_LIMITLINE_DEBUG=true`
269
+
270
+ ### Git branch not showing
260
271
 
261
- - Ensure your Claude subscription is active
262
- - Check if you've exceeded API rate limits (try increasing `pollInterval`)
272
+ Make sure you're in a git repository. The git segment only appears when a `.git` directory is found.
263
273
 
264
274
  ## Contributing
265
275
 
package/dist/index.js CHANGED
@@ -17,18 +17,27 @@ function debug(...args) {
17
17
  // src/config/types.ts
18
18
  var DEFAULT_CONFIG = {
19
19
  display: {
20
- style: "minimal",
20
+ style: "powerline",
21
21
  useNerdFonts: true
22
22
  },
23
+ directory: {
24
+ enabled: true
25
+ },
26
+ git: {
27
+ enabled: true
28
+ },
29
+ model: {
30
+ enabled: true
31
+ },
23
32
  block: {
24
33
  enabled: true,
25
- displayStyle: "bar",
34
+ displayStyle: "text",
26
35
  barWidth: 10,
27
36
  showTimeRemaining: true
28
37
  },
29
38
  weekly: {
30
39
  enabled: true,
31
- displayStyle: "bar",
40
+ displayStyle: "text",
32
41
  barWidth: 10,
33
42
  showWeekProgress: true
34
43
  },
@@ -436,61 +445,88 @@ var TEXT_SYMBOLS = {
436
445
  var RESET_CODE = "\x1B[0m";
437
446
 
438
447
  // src/themes/index.ts
439
- var color = (n) => `\x1B[38;5;${n}m`;
440
- var bgColor = (n) => `\x1B[48;5;${n}m`;
441
- var themes = {
442
- dark: {
443
- blockBg: bgColor(236),
444
- blockFg: color(252),
445
- weeklyBg: bgColor(236),
446
- weeklyFg: color(252),
447
- warningBg: bgColor(172),
448
- warningFg: color(232),
449
- criticalBg: bgColor(160),
450
- criticalFg: color(255),
451
- progressFull: color(76),
452
- progressEmpty: color(240),
453
- separatorFg: color(244)
454
- },
455
- light: {
456
- blockBg: bgColor(254),
457
- blockFg: color(236),
458
- weeklyBg: bgColor(254),
459
- weeklyFg: color(236),
460
- warningBg: bgColor(214),
461
- warningFg: color(232),
462
- criticalBg: bgColor(196),
463
- criticalFg: color(255),
464
- progressFull: color(34),
465
- progressEmpty: color(250),
466
- separatorFg: color(244)
467
- },
468
- nord: {
469
- blockBg: bgColor(236),
470
- blockFg: color(110),
471
- weeklyBg: bgColor(236),
472
- weeklyFg: color(110),
473
- warningBg: bgColor(179),
474
- warningFg: color(232),
475
- criticalBg: bgColor(131),
476
- criticalFg: color(255),
477
- progressFull: color(108),
478
- progressEmpty: color(239),
479
- separatorFg: color(60)
480
- },
481
- gruvbox: {
482
- blockBg: bgColor(237),
483
- blockFg: color(223),
484
- weeklyBg: bgColor(237),
485
- weeklyFg: color(223),
486
- warningBg: bgColor(214),
487
- warningFg: color(235),
488
- criticalBg: bgColor(167),
489
- criticalFg: color(235),
490
- progressFull: color(142),
491
- progressEmpty: color(239),
492
- separatorFg: color(246)
448
+ function hexToAnsi256(hex) {
449
+ const r = parseInt(hex.slice(1, 3), 16);
450
+ const g = parseInt(hex.slice(3, 5), 16);
451
+ const b = parseInt(hex.slice(5, 7), 16);
452
+ if (r === g && g === b) {
453
+ if (r < 8) return 16;
454
+ if (r > 248) return 231;
455
+ return Math.round((r - 8) / 247 * 24) + 232;
493
456
  }
457
+ const ri = Math.round(r / 255 * 5);
458
+ const gi = Math.round(g / 255 * 5);
459
+ const bi = Math.round(b / 255 * 5);
460
+ return 16 + 36 * ri + 6 * gi + bi;
461
+ }
462
+ var ansi = {
463
+ fg: (hex) => `\x1B[38;5;${hexToAnsi256(hex)}m`,
464
+ bg: (hex) => `\x1B[48;5;${hexToAnsi256(hex)}m`,
465
+ fgRaw: (n) => `\x1B[38;5;${n}m`,
466
+ bgRaw: (n) => `\x1B[48;5;${n}m`,
467
+ reset: "\x1B[0m"
468
+ };
469
+ var darkTheme = {
470
+ directory: { bg: "#8b4513", fg: "#ffffff" },
471
+ git: { bg: "#404040", fg: "#ffffff" },
472
+ model: { bg: "#2d2d2d", fg: "#ffffff" },
473
+ block: { bg: "#2a2a2a", fg: "#87ceeb" },
474
+ weekly: { bg: "#1a1a1a", fg: "#98fb98" },
475
+ warning: { bg: "#d75f00", fg: "#ffffff" },
476
+ critical: { bg: "#af0000", fg: "#ffffff" }
477
+ };
478
+ var lightTheme = {
479
+ directory: { bg: "#ff6b47", fg: "#ffffff" },
480
+ git: { bg: "#4fb3d9", fg: "#ffffff" },
481
+ model: { bg: "#87ceeb", fg: "#000000" },
482
+ block: { bg: "#6366f1", fg: "#ffffff" },
483
+ weekly: { bg: "#10b981", fg: "#ffffff" },
484
+ warning: { bg: "#f59e0b", fg: "#000000" },
485
+ critical: { bg: "#ef4444", fg: "#ffffff" }
486
+ };
487
+ var nordTheme = {
488
+ directory: { bg: "#434c5e", fg: "#d8dee9" },
489
+ git: { bg: "#3b4252", fg: "#a3be8c" },
490
+ model: { bg: "#4c566a", fg: "#81a1c1" },
491
+ block: { bg: "#3b4252", fg: "#81a1c1" },
492
+ weekly: { bg: "#2e3440", fg: "#8fbcbb" },
493
+ warning: { bg: "#d08770", fg: "#2e3440" },
494
+ critical: { bg: "#bf616a", fg: "#eceff4" }
495
+ };
496
+ var gruvboxTheme = {
497
+ directory: { bg: "#504945", fg: "#ebdbb2" },
498
+ git: { bg: "#3c3836", fg: "#b8bb26" },
499
+ model: { bg: "#665c54", fg: "#83a598" },
500
+ block: { bg: "#3c3836", fg: "#83a598" },
501
+ weekly: { bg: "#282828", fg: "#fabd2f" },
502
+ warning: { bg: "#d79921", fg: "#282828" },
503
+ critical: { bg: "#cc241d", fg: "#ebdbb2" }
504
+ };
505
+ var tokyoNightTheme = {
506
+ directory: { bg: "#2f334d", fg: "#82aaff" },
507
+ git: { bg: "#1e2030", fg: "#c3e88d" },
508
+ model: { bg: "#191b29", fg: "#fca7ea" },
509
+ block: { bg: "#2d3748", fg: "#7aa2f7" },
510
+ weekly: { bg: "#1a202c", fg: "#4fd6be" },
511
+ warning: { bg: "#e0af68", fg: "#1a1b26" },
512
+ critical: { bg: "#f7768e", fg: "#1a1b26" }
513
+ };
514
+ var rosePineTheme = {
515
+ directory: { bg: "#26233a", fg: "#c4a7e7" },
516
+ git: { bg: "#1f1d2e", fg: "#9ccfd8" },
517
+ model: { bg: "#191724", fg: "#ebbcba" },
518
+ block: { bg: "#2a273f", fg: "#eb6f92" },
519
+ weekly: { bg: "#232136", fg: "#9ccfd8" },
520
+ warning: { bg: "#f6c177", fg: "#191724" },
521
+ critical: { bg: "#eb6f92", fg: "#191724" }
522
+ };
523
+ var themes = {
524
+ dark: darkTheme,
525
+ light: lightTheme,
526
+ nord: nordTheme,
527
+ gruvbox: gruvboxTheme,
528
+ "tokyo-night": tokyoNightTheme,
529
+ "rose-pine": rosePineTheme
494
530
  };
495
531
  function getTheme(name) {
496
532
  return themes[name] || themes.dark;
@@ -501,113 +537,324 @@ var Renderer = class {
501
537
  config;
502
538
  theme;
503
539
  symbols;
540
+ usePowerline;
504
541
  constructor(config) {
505
542
  this.config = config;
506
543
  this.theme = getTheme(config.theme || "dark");
507
544
  const useNerd = config.display?.useNerdFonts ?? true;
508
545
  const symbolSet = useNerd ? SYMBOLS : TEXT_SYMBOLS;
546
+ this.usePowerline = useNerd;
509
547
  this.symbols = {
510
548
  block: symbolSet.block_cost,
511
549
  weekly: symbolSet.weekly_cost,
550
+ rightArrow: symbolSet.right,
512
551
  separator: symbolSet.separator,
552
+ branch: symbolSet.branch,
553
+ model: "\uF0E7",
554
+ // Lightning bolt for model
513
555
  progressFull: symbolSet.progress_full,
514
556
  progressEmpty: symbolSet.progress_empty
515
557
  };
516
558
  }
517
- formatProgressBar(percent, width, colors) {
559
+ formatProgressBar(percent, width) {
518
560
  const filled = Math.round(percent / 100 * width);
519
561
  const empty = width - filled;
520
- const filledBar = colors.progressFull + this.symbols.progressFull.repeat(filled);
521
- const emptyBar = colors.progressEmpty + this.symbols.progressEmpty.repeat(empty);
522
- return filledBar + emptyBar + RESET_CODE;
562
+ return this.symbols.progressFull.repeat(filled) + this.symbols.progressEmpty.repeat(empty);
523
563
  }
524
564
  formatTimeRemaining(minutes) {
525
565
  if (minutes >= 60) {
526
566
  const hours = Math.floor(minutes / 60);
527
567
  const mins = minutes % 60;
528
- return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
568
+ return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
529
569
  }
530
570
  return `${minutes}m`;
531
571
  }
532
- getColorForPercent(percent, colors) {
572
+ getColorsForPercent(percent, baseColors) {
533
573
  const threshold = this.config.budget?.warningThreshold ?? 80;
534
574
  if (percent >= 100) {
535
- return { bg: colors.criticalBg, fg: colors.criticalFg };
575
+ return this.theme.critical;
536
576
  } else if (percent >= threshold) {
537
- return { bg: colors.warningBg, fg: colors.warningFg };
577
+ return this.theme.warning;
578
+ }
579
+ return baseColors;
580
+ }
581
+ renderPowerline(segments) {
582
+ if (segments.length === 0) return "";
583
+ let output = "";
584
+ for (let i = 0; i < segments.length; i++) {
585
+ const seg = segments[i];
586
+ const nextColors = i < segments.length - 1 ? segments[i + 1].colors : null;
587
+ output += ansi.bg(seg.colors.bg) + ansi.fg(seg.colors.fg) + seg.text;
588
+ output += RESET_CODE;
589
+ if (nextColors) {
590
+ output += ansi.fg(seg.colors.bg) + ansi.bg(nextColors.bg) + this.symbols.rightArrow;
591
+ } else {
592
+ output += ansi.fg(seg.colors.bg) + this.symbols.rightArrow;
593
+ }
594
+ }
595
+ output += RESET_CODE;
596
+ return output;
597
+ }
598
+ renderFallback(segments) {
599
+ return segments.map((seg) => ansi.bg(seg.colors.bg) + ansi.fg(seg.colors.fg) + seg.text + RESET_CODE).join(` ${this.symbols.separator} `);
600
+ }
601
+ renderDirectory(envInfo) {
602
+ if (!this.config.directory?.enabled || !envInfo.directory) {
603
+ return null;
538
604
  }
539
- return { bg: colors.blockBg, fg: colors.blockFg };
605
+ return {
606
+ text: ` ${envInfo.directory} `,
607
+ colors: this.theme.directory
608
+ };
609
+ }
610
+ renderGit(envInfo) {
611
+ if (!this.config.git?.enabled || !envInfo.gitBranch) {
612
+ return null;
613
+ }
614
+ const dirtyIndicator = envInfo.gitDirty ? " \u25CF" : "";
615
+ const icon = this.usePowerline ? this.symbols.branch : "";
616
+ const prefix = icon ? `${icon} ` : "";
617
+ return {
618
+ text: ` ${prefix}${envInfo.gitBranch}${dirtyIndicator} `,
619
+ colors: this.theme.git
620
+ };
621
+ }
622
+ renderModel(envInfo) {
623
+ if (!this.config.model?.enabled || !envInfo.model) {
624
+ return null;
625
+ }
626
+ const icon = this.usePowerline ? this.symbols.model : "";
627
+ const prefix = icon ? `${icon} ` : "";
628
+ return {
629
+ text: ` ${prefix}${envInfo.model} `,
630
+ colors: this.theme.model
631
+ };
540
632
  }
541
633
  renderBlock(blockInfo) {
542
634
  if (!blockInfo || !this.config.block?.enabled) {
543
- return "";
635
+ return null;
544
636
  }
637
+ const icon = this.usePowerline ? this.symbols.block : "BLK";
545
638
  if (blockInfo.percentUsed === null) {
546
- return `${this.symbols.block} --`;
639
+ return {
640
+ text: ` ${icon} -- `,
641
+ colors: this.theme.block
642
+ };
547
643
  }
548
644
  const percent = blockInfo.percentUsed;
549
- const displayStyle = this.config.block.displayStyle || "bar";
645
+ const colors = this.getColorsForPercent(percent, this.theme.block);
646
+ const displayStyle = this.config.block.displayStyle || "text";
550
647
  const barWidth = this.config.block.barWidth || 10;
551
648
  const showTime = this.config.block.showTimeRemaining ?? true;
552
- let display;
649
+ let text;
553
650
  if (displayStyle === "bar") {
554
- const bar = this.formatProgressBar(percent, barWidth, this.theme);
555
- display = `${bar} ${Math.round(percent)}%`;
651
+ const bar = this.formatProgressBar(percent, barWidth);
652
+ text = `${bar} ${Math.round(percent)}%`;
556
653
  } else {
557
- display = `${Math.round(percent)}%`;
654
+ text = `${Math.round(percent)}%`;
558
655
  }
559
656
  if (showTime && blockInfo.timeRemaining !== null) {
560
657
  const timeStr = this.formatTimeRemaining(blockInfo.timeRemaining);
561
- display += ` (${timeStr} left)`;
658
+ text += ` (${timeStr})`;
562
659
  }
563
- return `${this.symbols.block} ${display}`;
660
+ return {
661
+ text: ` ${icon} ${text} `,
662
+ colors
663
+ };
564
664
  }
565
665
  renderWeekly(weeklyInfo) {
566
666
  if (!weeklyInfo || !this.config.weekly?.enabled) {
567
- return "";
667
+ return null;
568
668
  }
669
+ const icon = this.usePowerline ? this.symbols.weekly : "WK";
569
670
  if (weeklyInfo.percentUsed === null) {
570
- return `${this.symbols.weekly} --`;
671
+ return {
672
+ text: ` ${icon} -- `,
673
+ colors: this.theme.weekly
674
+ };
571
675
  }
572
676
  const percent = weeklyInfo.percentUsed;
573
- const displayStyle = this.config.weekly.displayStyle || "bar";
677
+ const displayStyle = this.config.weekly.displayStyle || "text";
574
678
  const barWidth = this.config.weekly.barWidth || 10;
575
679
  const showWeekProgress = this.config.weekly.showWeekProgress ?? true;
576
- let display;
680
+ let text;
577
681
  if (displayStyle === "bar") {
578
- const bar = this.formatProgressBar(percent, barWidth, this.theme);
579
- display = `${bar} ${Math.round(percent)}%`;
682
+ const bar = this.formatProgressBar(percent, barWidth);
683
+ text = `${bar} ${Math.round(percent)}%`;
580
684
  } else {
581
- display = `${Math.round(percent)}%`;
685
+ text = `${Math.round(percent)}%`;
582
686
  }
583
687
  if (showWeekProgress) {
584
- display += ` (wk ${weeklyInfo.weekProgressPercent}%)`;
688
+ text += ` (wk ${weeklyInfo.weekProgressPercent}%)`;
585
689
  }
586
- return `${this.symbols.weekly} ${display}`;
690
+ return {
691
+ text: ` ${icon} ${text} `,
692
+ colors: this.theme.weekly
693
+ };
587
694
  }
588
- render(blockInfo, weeklyInfo) {
589
- const parts = [];
695
+ render(blockInfo, weeklyInfo, envInfo) {
696
+ const segments = [];
697
+ const dirSegment = this.renderDirectory(envInfo);
698
+ if (dirSegment) segments.push(dirSegment);
699
+ const gitSegment = this.renderGit(envInfo);
700
+ if (gitSegment) segments.push(gitSegment);
701
+ const modelSegment = this.renderModel(envInfo);
702
+ if (modelSegment) segments.push(modelSegment);
590
703
  const blockSegment = this.renderBlock(blockInfo);
591
- if (blockSegment) {
592
- parts.push(blockSegment);
593
- }
704
+ if (blockSegment) segments.push(blockSegment);
594
705
  const weeklySegment = this.renderWeekly(weeklyInfo);
595
- if (weeklySegment) {
596
- parts.push(weeklySegment);
597
- }
598
- if (parts.length === 0) {
706
+ if (weeklySegment) segments.push(weeklySegment);
707
+ if (segments.length === 0) {
599
708
  return "";
600
709
  }
601
- const separator = ` ${this.theme.separatorFg}${this.symbols.separator}${RESET_CODE} `;
602
- return parts.join(separator);
710
+ if (this.usePowerline) {
711
+ return this.renderPowerline(segments);
712
+ } else {
713
+ return this.renderFallback(segments);
714
+ }
603
715
  }
604
716
  };
605
717
 
718
+ // src/utils/environment.ts
719
+ import { execSync } from "child_process";
720
+ import { basename } from "path";
721
+
722
+ // src/utils/claude-hook.ts
723
+ async function readHookData() {
724
+ if (process.stdin.isTTY) {
725
+ debug("stdin is TTY, no hook data");
726
+ return null;
727
+ }
728
+ try {
729
+ const chunks = [];
730
+ const result = await Promise.race([
731
+ new Promise((resolve, reject) => {
732
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
733
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
734
+ process.stdin.on("error", reject);
735
+ }),
736
+ new Promise((resolve) => setTimeout(() => resolve(null), 100))
737
+ ]);
738
+ if (!result || result.trim() === "") {
739
+ debug("No stdin data received");
740
+ return null;
741
+ }
742
+ const hookData = JSON.parse(result);
743
+ debug("Hook data received:", JSON.stringify(hookData));
744
+ return hookData;
745
+ } catch (error) {
746
+ debug("Error reading hook data:", error);
747
+ return null;
748
+ }
749
+ }
750
+ function formatModelName(modelId, displayName) {
751
+ if (displayName && displayName.length <= 20) {
752
+ const clean = displayName.replace(/^Claude\s*/i, "").trim();
753
+ if (clean) return clean;
754
+ }
755
+ const mappings = {
756
+ "claude-opus-4-5-20251101": "Opus 4.5",
757
+ "claude-opus-4-20250514": "Opus 4",
758
+ "claude-sonnet-4-20250514": "Sonnet 4",
759
+ "claude-3-5-sonnet-20241022": "Sonnet 3.5",
760
+ "claude-3-5-sonnet-latest": "Sonnet 3.5",
761
+ "claude-3-5-sonnet": "Sonnet 3.5",
762
+ "claude-3-opus-20240229": "Opus 3",
763
+ "claude-3-opus": "Opus 3",
764
+ "claude-3-sonnet-20240229": "Sonnet 3",
765
+ "claude-3-haiku-20240307": "Haiku 3",
766
+ "claude-3-haiku": "Haiku 3"
767
+ };
768
+ if (mappings[modelId]) {
769
+ return mappings[modelId];
770
+ }
771
+ const lower = modelId.toLowerCase();
772
+ if (lower.includes("opus")) {
773
+ if (lower.includes("4-5") || lower.includes("4.5")) return "Opus 4.5";
774
+ if (lower.includes("4")) return "Opus 4";
775
+ if (lower.includes("3")) return "Opus 3";
776
+ return "Opus";
777
+ }
778
+ if (lower.includes("sonnet")) {
779
+ if (lower.includes("4")) return "Sonnet 4";
780
+ if (lower.includes("3-5") || lower.includes("3.5")) return "Sonnet 3.5";
781
+ if (lower.includes("3")) return "Sonnet 3";
782
+ return "Sonnet";
783
+ }
784
+ if (lower.includes("haiku")) {
785
+ if (lower.includes("3")) return "Haiku 3";
786
+ return "Haiku";
787
+ }
788
+ return modelId.length > 15 ? modelId.slice(0, 15) : modelId;
789
+ }
790
+
791
+ // src/utils/environment.ts
792
+ function getDirectoryName(hookData) {
793
+ try {
794
+ if (hookData?.workspace?.project_dir) {
795
+ return basename(hookData.workspace.project_dir);
796
+ }
797
+ if (hookData?.cwd) {
798
+ return basename(hookData.cwd);
799
+ }
800
+ return basename(process.cwd());
801
+ } catch (error) {
802
+ debug("Error getting directory name:", error);
803
+ return null;
804
+ }
805
+ }
806
+ function getGitBranch() {
807
+ try {
808
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
809
+ encoding: "utf-8",
810
+ stdio: ["pipe", "pipe", "pipe"]
811
+ }).trim();
812
+ return branch || null;
813
+ } catch (error) {
814
+ debug("Error getting git branch:", error);
815
+ return null;
816
+ }
817
+ }
818
+ function hasGitChanges() {
819
+ try {
820
+ const status = execSync("git status --porcelain", {
821
+ encoding: "utf-8",
822
+ stdio: ["pipe", "pipe", "pipe"]
823
+ }).trim();
824
+ return status.length > 0;
825
+ } catch (error) {
826
+ debug("Error checking git status:", error);
827
+ return false;
828
+ }
829
+ }
830
+ function getClaudeModel(hookData) {
831
+ if (hookData?.model?.id) {
832
+ return formatModelName(hookData.model.id, hookData.model.display_name);
833
+ }
834
+ const model = process.env.CLAUDE_MODEL || process.env.CLAUDE_CODE_MODEL || process.env.ANTHROPIC_MODEL;
835
+ if (model) {
836
+ return formatModelName(model);
837
+ }
838
+ return null;
839
+ }
840
+ function getEnvironmentInfo(hookData) {
841
+ return {
842
+ directory: getDirectoryName(hookData),
843
+ gitBranch: getGitBranch(),
844
+ gitDirty: hasGitChanges(),
845
+ model: getClaudeModel(hookData)
846
+ };
847
+ }
848
+
606
849
  // src/index.ts
607
850
  async function main() {
608
851
  try {
609
852
  const config = loadConfig();
610
853
  debug("Config loaded:", JSON.stringify(config));
854
+ const hookData = await readHookData();
855
+ debug("Hook data:", JSON.stringify(hookData));
856
+ const envInfo = getEnvironmentInfo(hookData);
857
+ debug("Environment info:", JSON.stringify(envInfo));
611
858
  const blockProvider = new BlockProvider();
612
859
  const weeklyProvider = new WeeklyProvider();
613
860
  const pollInterval = config.budget?.pollInterval ?? 15;
@@ -623,7 +870,7 @@ async function main() {
623
870
  debug("Block info:", JSON.stringify(blockInfo));
624
871
  debug("Weekly info:", JSON.stringify(weeklyInfo));
625
872
  const renderer = new Renderer(config);
626
- const output = renderer.render(blockInfo, weeklyInfo);
873
+ const output = renderer.render(blockInfo, weeklyInfo, envInfo);
627
874
  if (output) {
628
875
  process.stdout.write(output);
629
876
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-limitline",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "A statusline for Claude Code showing real-time usage limits and weekly tracking",
5
5
  "main": "dist/index.js",
6
6
  "bin": {