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.
- package/README.md +73 -63
- package/dist/index.js +343 -96
- 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
|
|
3
|
+
A powerline-style statusline for Claude Code showing real-time usage limits, git info, and model details.
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|

|
|
7
7
|

|
|
8
8
|
|
|
9
|
+

|
|
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
|
-
- **
|
|
14
|
-
- **
|
|
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
|
-
|
|
17
|
-
|
|
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** (
|
|
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
|
-
###
|
|
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
|
-
|
|
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": "
|
|
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": "
|
|
133
|
+
"displayStyle": "text",
|
|
131
134
|
"barWidth": 10,
|
|
132
135
|
"showTimeRemaining": true
|
|
133
136
|
},
|
|
134
137
|
"weekly": {
|
|
135
138
|
"enabled": true,
|
|
136
|
-
"displayStyle": "
|
|
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
|
|
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"` | `"
|
|
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"` | `"
|
|
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
|
|
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
|
-
###
|
|
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
|
-
|
|
256
|
-
claude --login
|
|
261
|
+
echo '{"model":{"id":"claude-opus-4-5-20251101"}}' | claude-limitline
|
|
257
262
|
```
|
|
258
263
|
|
|
259
|
-
###
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
34
|
+
displayStyle: "text",
|
|
26
35
|
barWidth: 10,
|
|
27
36
|
showTimeRemaining: true
|
|
28
37
|
},
|
|
29
38
|
weekly: {
|
|
30
39
|
enabled: true,
|
|
31
|
-
displayStyle: "
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
559
|
+
formatProgressBar(percent, width) {
|
|
518
560
|
const filled = Math.round(percent / 100 * width);
|
|
519
561
|
const empty = width - filled;
|
|
520
|
-
|
|
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
|
|
568
|
+
return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
|
|
529
569
|
}
|
|
530
570
|
return `${minutes}m`;
|
|
531
571
|
}
|
|
532
|
-
|
|
572
|
+
getColorsForPercent(percent, baseColors) {
|
|
533
573
|
const threshold = this.config.budget?.warningThreshold ?? 80;
|
|
534
574
|
if (percent >= 100) {
|
|
535
|
-
return
|
|
575
|
+
return this.theme.critical;
|
|
536
576
|
} else if (percent >= threshold) {
|
|
537
|
-
return
|
|
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 {
|
|
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
|
|
639
|
+
return {
|
|
640
|
+
text: ` ${icon} -- `,
|
|
641
|
+
colors: this.theme.block
|
|
642
|
+
};
|
|
547
643
|
}
|
|
548
644
|
const percent = blockInfo.percentUsed;
|
|
549
|
-
const
|
|
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
|
|
649
|
+
let text;
|
|
553
650
|
if (displayStyle === "bar") {
|
|
554
|
-
const bar = this.formatProgressBar(percent, barWidth
|
|
555
|
-
|
|
651
|
+
const bar = this.formatProgressBar(percent, barWidth);
|
|
652
|
+
text = `${bar} ${Math.round(percent)}%`;
|
|
556
653
|
} else {
|
|
557
|
-
|
|
654
|
+
text = `${Math.round(percent)}%`;
|
|
558
655
|
}
|
|
559
656
|
if (showTime && blockInfo.timeRemaining !== null) {
|
|
560
657
|
const timeStr = this.formatTimeRemaining(blockInfo.timeRemaining);
|
|
561
|
-
|
|
658
|
+
text += ` (${timeStr})`;
|
|
562
659
|
}
|
|
563
|
-
return
|
|
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
|
|
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 || "
|
|
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
|
|
680
|
+
let text;
|
|
577
681
|
if (displayStyle === "bar") {
|
|
578
|
-
const bar = this.formatProgressBar(percent, barWidth
|
|
579
|
-
|
|
682
|
+
const bar = this.formatProgressBar(percent, barWidth);
|
|
683
|
+
text = `${bar} ${Math.round(percent)}%`;
|
|
580
684
|
} else {
|
|
581
|
-
|
|
685
|
+
text = `${Math.round(percent)}%`;
|
|
582
686
|
}
|
|
583
687
|
if (showWeekProgress) {
|
|
584
|
-
|
|
688
|
+
text += ` (wk ${weeklyInfo.weekProgressPercent}%)`;
|
|
585
689
|
}
|
|
586
|
-
return
|
|
690
|
+
return {
|
|
691
|
+
text: ` ${icon} ${text} `,
|
|
692
|
+
colors: this.theme.weekly
|
|
693
|
+
};
|
|
587
694
|
}
|
|
588
|
-
render(blockInfo, weeklyInfo) {
|
|
589
|
-
const
|
|
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
|
-
|
|
597
|
-
}
|
|
598
|
-
if (parts.length === 0) {
|
|
706
|
+
if (weeklySegment) segments.push(weeklySegment);
|
|
707
|
+
if (segments.length === 0) {
|
|
599
708
|
return "";
|
|
600
709
|
}
|
|
601
|
-
|
|
602
|
-
|
|
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
|
}
|