claude-limitline 1.0.1 → 1.2.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 +84 -66
- package/dist/index.js +449 -119
- 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,33 +105,40 @@ 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
|
|
119
113
|
|
|
120
|
-
Create a
|
|
114
|
+
Create a `claude-limitline.json` file in your Claude config directory (`~/.claude/claude-limitline.json`) or `.claude-limitline.json` in your current working directory:
|
|
121
115
|
|
|
122
116
|
```json
|
|
123
117
|
{
|
|
124
118
|
"display": {
|
|
125
|
-
"style": "
|
|
126
|
-
"useNerdFonts": true
|
|
119
|
+
"style": "powerline",
|
|
120
|
+
"useNerdFonts": true,
|
|
121
|
+
"compactMode": "auto",
|
|
122
|
+
"compactWidth": 80
|
|
123
|
+
},
|
|
124
|
+
"directory": {
|
|
125
|
+
"enabled": true
|
|
126
|
+
},
|
|
127
|
+
"git": {
|
|
128
|
+
"enabled": true
|
|
129
|
+
},
|
|
130
|
+
"model": {
|
|
131
|
+
"enabled": true
|
|
127
132
|
},
|
|
128
133
|
"block": {
|
|
129
134
|
"enabled": true,
|
|
130
|
-
"displayStyle": "
|
|
135
|
+
"displayStyle": "text",
|
|
131
136
|
"barWidth": 10,
|
|
132
137
|
"showTimeRemaining": true
|
|
133
138
|
},
|
|
134
139
|
"weekly": {
|
|
135
140
|
"enabled": true,
|
|
136
|
-
"displayStyle": "
|
|
141
|
+
"displayStyle": "text",
|
|
137
142
|
"barWidth": 10,
|
|
138
143
|
"showWeekProgress": true
|
|
139
144
|
},
|
|
@@ -141,7 +146,9 @@ Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitl
|
|
|
141
146
|
"pollInterval": 15,
|
|
142
147
|
"warningThreshold": 80
|
|
143
148
|
},
|
|
144
|
-
"theme": "dark"
|
|
149
|
+
"theme": "dark",
|
|
150
|
+
"segmentOrder": ["directory", "git", "model", "block", "weekly"],
|
|
151
|
+
"showTrend": true
|
|
145
152
|
}
|
|
146
153
|
```
|
|
147
154
|
|
|
@@ -149,29 +156,55 @@ Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitl
|
|
|
149
156
|
|
|
150
157
|
| Option | Description | Default |
|
|
151
158
|
|--------|-------------|---------|
|
|
152
|
-
| `display.useNerdFonts` | Use Nerd Font symbols
|
|
159
|
+
| `display.useNerdFonts` | Use Nerd Font symbols for powerline | `true` |
|
|
160
|
+
| `display.compactMode` | `"auto"`, `"always"`, or `"never"` | `"auto"` |
|
|
161
|
+
| `display.compactWidth` | Terminal width threshold for compact mode | `80` |
|
|
162
|
+
| `directory.enabled` | Show repository/directory name | `true` |
|
|
163
|
+
| `git.enabled` | Show git branch with dirty indicator | `true` |
|
|
164
|
+
| `model.enabled` | Show Claude model name | `true` |
|
|
153
165
|
| `block.enabled` | Show 5-hour block usage | `true` |
|
|
154
|
-
| `block.displayStyle` | `"bar"` or `"text"` | `"
|
|
166
|
+
| `block.displayStyle` | `"bar"` or `"text"` | `"text"` |
|
|
155
167
|
| `block.barWidth` | Width of progress bar in characters | `10` |
|
|
156
168
|
| `block.showTimeRemaining` | Show time until block resets | `true` |
|
|
157
169
|
| `weekly.enabled` | Show 7-day rolling usage | `true` |
|
|
158
|
-
| `weekly.displayStyle` | `"bar"` or `"text"` | `"
|
|
170
|
+
| `weekly.displayStyle` | `"bar"` or `"text"` | `"text"` |
|
|
159
171
|
| `weekly.barWidth` | Width of progress bar in characters | `10` |
|
|
160
172
|
| `weekly.showWeekProgress` | Show week progress percentage | `true` |
|
|
161
173
|
| `budget.pollInterval` | Minutes between API calls | `15` |
|
|
162
174
|
| `budget.warningThreshold` | Percentage to trigger warning color | `80` |
|
|
163
175
|
| `theme` | Color theme name | `"dark"` |
|
|
176
|
+
| `segmentOrder` | Array to customize segment order | `["directory", "git", "model", "block", "weekly"]` |
|
|
177
|
+
| `showTrend` | Show ↑↓ arrows for usage changes | `true` |
|
|
164
178
|
|
|
165
179
|
### Available Themes
|
|
166
180
|
|
|
167
|
-
- `dark` - Default dark theme
|
|
168
|
-
- `light` - Light background theme
|
|
181
|
+
- `dark` - Default dark theme with warm browns and cool cyans
|
|
182
|
+
- `light` - Light background theme with vibrant colors
|
|
169
183
|
- `nord` - Nord color palette
|
|
170
184
|
- `gruvbox` - Gruvbox color palette
|
|
185
|
+
- `tokyo-night` - Tokyo Night color palette
|
|
186
|
+
- `rose-pine` - Rosé Pine color palette
|
|
187
|
+
|
|
188
|
+
## Segments
|
|
189
|
+
|
|
190
|
+
The statusline displays the following segments (all configurable):
|
|
191
|
+
|
|
192
|
+
| Segment | Description | Color (dark theme) |
|
|
193
|
+
|---------|-------------|-------------------|
|
|
194
|
+
| **Directory** | Current repo/project name | Brown/Orange |
|
|
195
|
+
| **Git** | Branch name + dirty indicator (●) | Dark Gray |
|
|
196
|
+
| **Model** | Claude model (Opus 4.5, Sonnet 4, etc.) | Dark Gray |
|
|
197
|
+
| **Block** | 5-hour usage % + time remaining | Cyan (warning: Orange, critical: Red) |
|
|
198
|
+
| **Weekly** | 7-day usage % + week progress | Green |
|
|
171
199
|
|
|
172
200
|
## How It Works
|
|
173
201
|
|
|
174
|
-
claude-limitline retrieves
|
|
202
|
+
claude-limitline retrieves data from two sources:
|
|
203
|
+
|
|
204
|
+
1. **Hook Data (stdin)** - Claude Code passes JSON with model info, workspace, and session data
|
|
205
|
+
2. **Usage API** - Fetches usage limits from Anthropic's OAuth usage endpoint
|
|
206
|
+
|
|
207
|
+
### OAuth Token Location
|
|
175
208
|
|
|
176
209
|
| Platform | Location |
|
|
177
210
|
|----------|----------|
|
|
@@ -179,15 +212,6 @@ claude-limitline retrieves your Claude usage data from Anthropic's OAuth usage A
|
|
|
179
212
|
| **macOS** | Keychain or `~/.claude/.credentials.json` |
|
|
180
213
|
| **Linux** | secret-tool (GNOME Keyring) or `~/.claude/.credentials.json` |
|
|
181
214
|
|
|
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
215
|
## Development
|
|
192
216
|
|
|
193
217
|
### Setup
|
|
@@ -210,16 +234,13 @@ npm run build
|
|
|
210
234
|
npm run dev
|
|
211
235
|
```
|
|
212
236
|
|
|
213
|
-
### Type Checking
|
|
214
|
-
|
|
215
|
-
```bash
|
|
216
|
-
npm run typecheck
|
|
217
|
-
```
|
|
218
|
-
|
|
219
237
|
### Run Locally
|
|
220
238
|
|
|
221
239
|
```bash
|
|
222
240
|
node dist/index.js
|
|
241
|
+
|
|
242
|
+
# With simulated hook data
|
|
243
|
+
echo '{"model":{"id":"claude-opus-4-5-20251101"}}' | node dist/index.js
|
|
223
244
|
```
|
|
224
245
|
|
|
225
246
|
## Debug Mode
|
|
@@ -241,25 +262,22 @@ Debug output is written to stderr so it won't interfere with the status line out
|
|
|
241
262
|
|
|
242
263
|
## Troubleshooting
|
|
243
264
|
|
|
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:
|
|
265
|
+
### Model not showing
|
|
253
266
|
|
|
267
|
+
The model is passed via stdin from Claude Code. If running standalone, pipe in hook data:
|
|
254
268
|
```bash
|
|
255
|
-
|
|
256
|
-
claude --login
|
|
269
|
+
echo '{"model":{"id":"claude-opus-4-5-20251101"}}' | claude-limitline
|
|
257
270
|
```
|
|
258
271
|
|
|
259
|
-
###
|
|
272
|
+
### "No data" or empty output
|
|
273
|
+
|
|
274
|
+
1. **Check OAuth token**: Make sure you're logged into Claude Code (`claude --login`)
|
|
275
|
+
2. **Check credentials file**: Verify `~/.claude/.credentials.json` exists
|
|
276
|
+
3. **Enable debug mode**: Run with `CLAUDE_LIMITLINE_DEBUG=true`
|
|
277
|
+
|
|
278
|
+
### Git branch not showing
|
|
260
279
|
|
|
261
|
-
|
|
262
|
-
- Check if you've exceeded API rate limits (try increasing `pollInterval`)
|
|
280
|
+
Make sure you're in a git repository. The git segment only appears when a `.git` directory is found.
|
|
263
281
|
|
|
264
282
|
## Contributing
|
|
265
283
|
|
package/dist/index.js
CHANGED
|
@@ -17,18 +17,29 @@ function debug(...args) {
|
|
|
17
17
|
// src/config/types.ts
|
|
18
18
|
var DEFAULT_CONFIG = {
|
|
19
19
|
display: {
|
|
20
|
-
style: "
|
|
21
|
-
useNerdFonts: true
|
|
20
|
+
style: "powerline",
|
|
21
|
+
useNerdFonts: true,
|
|
22
|
+
compactMode: "auto",
|
|
23
|
+
compactWidth: 80
|
|
24
|
+
},
|
|
25
|
+
directory: {
|
|
26
|
+
enabled: true
|
|
27
|
+
},
|
|
28
|
+
git: {
|
|
29
|
+
enabled: true
|
|
30
|
+
},
|
|
31
|
+
model: {
|
|
32
|
+
enabled: true
|
|
22
33
|
},
|
|
23
34
|
block: {
|
|
24
35
|
enabled: true,
|
|
25
|
-
displayStyle: "
|
|
36
|
+
displayStyle: "text",
|
|
26
37
|
barWidth: 10,
|
|
27
38
|
showTimeRemaining: true
|
|
28
39
|
},
|
|
29
40
|
weekly: {
|
|
30
41
|
enabled: true,
|
|
31
|
-
displayStyle: "
|
|
42
|
+
displayStyle: "text",
|
|
32
43
|
barWidth: 10,
|
|
33
44
|
showWeekProgress: true
|
|
34
45
|
},
|
|
@@ -36,7 +47,9 @@ var DEFAULT_CONFIG = {
|
|
|
36
47
|
pollInterval: 15,
|
|
37
48
|
warningThreshold: 80
|
|
38
49
|
},
|
|
39
|
-
theme: "dark"
|
|
50
|
+
theme: "dark",
|
|
51
|
+
segmentOrder: ["directory", "git", "model", "block", "weekly"],
|
|
52
|
+
showTrend: true
|
|
40
53
|
};
|
|
41
54
|
|
|
42
55
|
// src/config/loader.ts
|
|
@@ -56,8 +69,7 @@ function deepMerge(target, source) {
|
|
|
56
69
|
function loadConfig() {
|
|
57
70
|
const configPaths = [
|
|
58
71
|
path.join(process.cwd(), ".claude-limitline.json"),
|
|
59
|
-
path.join(os.homedir(), ".claude-limitline.json")
|
|
60
|
-
path.join(os.homedir(), ".config", "claude-limitline", "config.json")
|
|
72
|
+
path.join(os.homedir(), ".claude", "claude-limitline.json")
|
|
61
73
|
];
|
|
62
74
|
for (const configPath of configPaths) {
|
|
63
75
|
try {
|
|
@@ -255,8 +267,31 @@ async function fetchUsageFromAPI(token) {
|
|
|
255
267
|
}
|
|
256
268
|
}
|
|
257
269
|
var cachedUsage = null;
|
|
270
|
+
var previousUsage = null;
|
|
258
271
|
var cacheTimestamp = 0;
|
|
259
272
|
var cachedToken = null;
|
|
273
|
+
function getUsageTrend() {
|
|
274
|
+
const result = {
|
|
275
|
+
fiveHourTrend: null,
|
|
276
|
+
sevenDayTrend: null
|
|
277
|
+
};
|
|
278
|
+
if (!cachedUsage || !previousUsage) {
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
if (cachedUsage.fiveHour && previousUsage.fiveHour) {
|
|
282
|
+
const diff = cachedUsage.fiveHour.percentUsed - previousUsage.fiveHour.percentUsed;
|
|
283
|
+
if (diff > 0.5) result.fiveHourTrend = "up";
|
|
284
|
+
else if (diff < -0.5) result.fiveHourTrend = "down";
|
|
285
|
+
else result.fiveHourTrend = "same";
|
|
286
|
+
}
|
|
287
|
+
if (cachedUsage.sevenDay && previousUsage.sevenDay) {
|
|
288
|
+
const diff = cachedUsage.sevenDay.percentUsed - previousUsage.sevenDay.percentUsed;
|
|
289
|
+
if (diff > 0.5) result.sevenDayTrend = "up";
|
|
290
|
+
else if (diff < -0.5) result.sevenDayTrend = "down";
|
|
291
|
+
else result.sevenDayTrend = "same";
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
260
295
|
async function getRealtimeUsage(pollIntervalMinutes = 15) {
|
|
261
296
|
const now = Date.now();
|
|
262
297
|
const cacheAgeMs = now - cacheTimestamp;
|
|
@@ -274,6 +309,7 @@ async function getRealtimeUsage(pollIntervalMinutes = 15) {
|
|
|
274
309
|
}
|
|
275
310
|
const usage = await fetchUsageFromAPI(cachedToken);
|
|
276
311
|
if (usage) {
|
|
312
|
+
previousUsage = cachedUsage;
|
|
277
313
|
cachedUsage = usage;
|
|
278
314
|
cacheTimestamp = now;
|
|
279
315
|
debug("Refreshed realtime usage cache");
|
|
@@ -414,10 +450,12 @@ var SYMBOLS = {
|
|
|
414
450
|
left: "\uE0B2",
|
|
415
451
|
branch: "\uE0A0",
|
|
416
452
|
separator: "\uE0B1",
|
|
417
|
-
|
|
418
|
-
//
|
|
419
|
-
|
|
420
|
-
//
|
|
453
|
+
model: "\u2731",
|
|
454
|
+
// Heavy asterisk ✱
|
|
455
|
+
block_cost: "\u25EB",
|
|
456
|
+
// White square with vertical bisecting line ◫
|
|
457
|
+
weekly_cost: "\u25CB",
|
|
458
|
+
// Circle ○
|
|
421
459
|
progress_full: "\u2588",
|
|
422
460
|
// Full block
|
|
423
461
|
progress_empty: "\u2591"
|
|
@@ -428,6 +466,7 @@ var TEXT_SYMBOLS = {
|
|
|
428
466
|
left: "<",
|
|
429
467
|
branch: "",
|
|
430
468
|
separator: "|",
|
|
469
|
+
model: "*",
|
|
431
470
|
block_cost: "BLK",
|
|
432
471
|
weekly_cost: "WK",
|
|
433
472
|
progress_full: "#",
|
|
@@ -436,178 +475,467 @@ var TEXT_SYMBOLS = {
|
|
|
436
475
|
var RESET_CODE = "\x1B[0m";
|
|
437
476
|
|
|
438
477
|
// 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)
|
|
478
|
+
function hexToAnsi256(hex) {
|
|
479
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
480
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
481
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
482
|
+
if (r === g && g === b) {
|
|
483
|
+
if (r < 8) return 16;
|
|
484
|
+
if (r > 248) return 231;
|
|
485
|
+
return Math.round((r - 8) / 247 * 24) + 232;
|
|
493
486
|
}
|
|
487
|
+
const ri = Math.round(r / 255 * 5);
|
|
488
|
+
const gi = Math.round(g / 255 * 5);
|
|
489
|
+
const bi = Math.round(b / 255 * 5);
|
|
490
|
+
return 16 + 36 * ri + 6 * gi + bi;
|
|
491
|
+
}
|
|
492
|
+
var ansi = {
|
|
493
|
+
fg: (hex) => `\x1B[38;5;${hexToAnsi256(hex)}m`,
|
|
494
|
+
bg: (hex) => `\x1B[48;5;${hexToAnsi256(hex)}m`,
|
|
495
|
+
fgRaw: (n) => `\x1B[38;5;${n}m`,
|
|
496
|
+
bgRaw: (n) => `\x1B[48;5;${n}m`,
|
|
497
|
+
reset: "\x1B[0m"
|
|
498
|
+
};
|
|
499
|
+
var darkTheme = {
|
|
500
|
+
directory: { bg: "#8b4513", fg: "#ffffff" },
|
|
501
|
+
git: { bg: "#404040", fg: "#ffffff" },
|
|
502
|
+
model: { bg: "#2d2d2d", fg: "#ffffff" },
|
|
503
|
+
block: { bg: "#2a2a2a", fg: "#87ceeb" },
|
|
504
|
+
weekly: { bg: "#1a1a1a", fg: "#98fb98" },
|
|
505
|
+
warning: { bg: "#d75f00", fg: "#ffffff" },
|
|
506
|
+
critical: { bg: "#af0000", fg: "#ffffff" }
|
|
507
|
+
};
|
|
508
|
+
var lightTheme = {
|
|
509
|
+
directory: { bg: "#ff6b47", fg: "#ffffff" },
|
|
510
|
+
git: { bg: "#4fb3d9", fg: "#ffffff" },
|
|
511
|
+
model: { bg: "#87ceeb", fg: "#000000" },
|
|
512
|
+
block: { bg: "#6366f1", fg: "#ffffff" },
|
|
513
|
+
weekly: { bg: "#10b981", fg: "#ffffff" },
|
|
514
|
+
warning: { bg: "#f59e0b", fg: "#000000" },
|
|
515
|
+
critical: { bg: "#ef4444", fg: "#ffffff" }
|
|
516
|
+
};
|
|
517
|
+
var nordTheme = {
|
|
518
|
+
directory: { bg: "#434c5e", fg: "#d8dee9" },
|
|
519
|
+
git: { bg: "#3b4252", fg: "#a3be8c" },
|
|
520
|
+
model: { bg: "#4c566a", fg: "#81a1c1" },
|
|
521
|
+
block: { bg: "#3b4252", fg: "#81a1c1" },
|
|
522
|
+
weekly: { bg: "#2e3440", fg: "#8fbcbb" },
|
|
523
|
+
warning: { bg: "#d08770", fg: "#2e3440" },
|
|
524
|
+
critical: { bg: "#bf616a", fg: "#eceff4" }
|
|
525
|
+
};
|
|
526
|
+
var gruvboxTheme = {
|
|
527
|
+
directory: { bg: "#504945", fg: "#ebdbb2" },
|
|
528
|
+
git: { bg: "#3c3836", fg: "#b8bb26" },
|
|
529
|
+
model: { bg: "#665c54", fg: "#83a598" },
|
|
530
|
+
block: { bg: "#3c3836", fg: "#83a598" },
|
|
531
|
+
weekly: { bg: "#282828", fg: "#fabd2f" },
|
|
532
|
+
warning: { bg: "#d79921", fg: "#282828" },
|
|
533
|
+
critical: { bg: "#cc241d", fg: "#ebdbb2" }
|
|
534
|
+
};
|
|
535
|
+
var tokyoNightTheme = {
|
|
536
|
+
directory: { bg: "#2f334d", fg: "#82aaff" },
|
|
537
|
+
git: { bg: "#1e2030", fg: "#c3e88d" },
|
|
538
|
+
model: { bg: "#191b29", fg: "#fca7ea" },
|
|
539
|
+
block: { bg: "#2d3748", fg: "#7aa2f7" },
|
|
540
|
+
weekly: { bg: "#1a202c", fg: "#4fd6be" },
|
|
541
|
+
warning: { bg: "#e0af68", fg: "#1a1b26" },
|
|
542
|
+
critical: { bg: "#f7768e", fg: "#1a1b26" }
|
|
543
|
+
};
|
|
544
|
+
var rosePineTheme = {
|
|
545
|
+
directory: { bg: "#26233a", fg: "#c4a7e7" },
|
|
546
|
+
git: { bg: "#1f1d2e", fg: "#9ccfd8" },
|
|
547
|
+
model: { bg: "#191724", fg: "#ebbcba" },
|
|
548
|
+
block: { bg: "#2a273f", fg: "#eb6f92" },
|
|
549
|
+
weekly: { bg: "#232136", fg: "#9ccfd8" },
|
|
550
|
+
warning: { bg: "#f6c177", fg: "#191724" },
|
|
551
|
+
critical: { bg: "#eb6f92", fg: "#191724" }
|
|
552
|
+
};
|
|
553
|
+
var themes = {
|
|
554
|
+
dark: darkTheme,
|
|
555
|
+
light: lightTheme,
|
|
556
|
+
nord: nordTheme,
|
|
557
|
+
gruvbox: gruvboxTheme,
|
|
558
|
+
"tokyo-night": tokyoNightTheme,
|
|
559
|
+
"rose-pine": rosePineTheme
|
|
494
560
|
};
|
|
495
561
|
function getTheme(name) {
|
|
496
562
|
return themes[name] || themes.dark;
|
|
497
563
|
}
|
|
498
564
|
|
|
565
|
+
// src/utils/terminal.ts
|
|
566
|
+
function getTerminalWidth() {
|
|
567
|
+
return process.stdout.columns || 80;
|
|
568
|
+
}
|
|
569
|
+
|
|
499
570
|
// src/renderer.ts
|
|
500
571
|
var Renderer = class {
|
|
501
572
|
config;
|
|
502
573
|
theme;
|
|
503
574
|
symbols;
|
|
575
|
+
usePowerline;
|
|
504
576
|
constructor(config) {
|
|
505
577
|
this.config = config;
|
|
506
578
|
this.theme = getTheme(config.theme || "dark");
|
|
507
579
|
const useNerd = config.display?.useNerdFonts ?? true;
|
|
508
580
|
const symbolSet = useNerd ? SYMBOLS : TEXT_SYMBOLS;
|
|
581
|
+
this.usePowerline = useNerd;
|
|
509
582
|
this.symbols = {
|
|
510
583
|
block: symbolSet.block_cost,
|
|
511
584
|
weekly: symbolSet.weekly_cost,
|
|
585
|
+
rightArrow: symbolSet.right,
|
|
512
586
|
separator: symbolSet.separator,
|
|
587
|
+
branch: symbolSet.branch,
|
|
588
|
+
model: symbolSet.model,
|
|
513
589
|
progressFull: symbolSet.progress_full,
|
|
514
|
-
progressEmpty: symbolSet.progress_empty
|
|
590
|
+
progressEmpty: symbolSet.progress_empty,
|
|
591
|
+
trendUp: "\u2191",
|
|
592
|
+
trendDown: "\u2193"
|
|
515
593
|
};
|
|
516
594
|
}
|
|
517
|
-
|
|
595
|
+
isCompactMode() {
|
|
596
|
+
const mode = this.config.display?.compactMode ?? "auto";
|
|
597
|
+
if (mode === "always") return true;
|
|
598
|
+
if (mode === "never") return false;
|
|
599
|
+
const threshold = this.config.display?.compactWidth ?? 80;
|
|
600
|
+
const termWidth = getTerminalWidth();
|
|
601
|
+
return termWidth < threshold;
|
|
602
|
+
}
|
|
603
|
+
formatProgressBar(percent, width) {
|
|
518
604
|
const filled = Math.round(percent / 100 * width);
|
|
519
605
|
const empty = width - filled;
|
|
520
|
-
|
|
521
|
-
const emptyBar = colors.progressEmpty + this.symbols.progressEmpty.repeat(empty);
|
|
522
|
-
return filledBar + emptyBar + RESET_CODE;
|
|
606
|
+
return this.symbols.progressFull.repeat(filled) + this.symbols.progressEmpty.repeat(empty);
|
|
523
607
|
}
|
|
524
|
-
formatTimeRemaining(minutes) {
|
|
608
|
+
formatTimeRemaining(minutes, compact) {
|
|
525
609
|
if (minutes >= 60) {
|
|
526
610
|
const hours = Math.floor(minutes / 60);
|
|
527
611
|
const mins = minutes % 60;
|
|
528
|
-
|
|
612
|
+
if (compact) {
|
|
613
|
+
return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
|
|
614
|
+
}
|
|
615
|
+
return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
|
|
529
616
|
}
|
|
530
617
|
return `${minutes}m`;
|
|
531
618
|
}
|
|
532
|
-
|
|
619
|
+
getTrendSymbol(trend) {
|
|
620
|
+
if (!this.config.showTrend) return "";
|
|
621
|
+
if (trend === "up") return this.symbols.trendUp;
|
|
622
|
+
if (trend === "down") return this.symbols.trendDown;
|
|
623
|
+
return "";
|
|
624
|
+
}
|
|
625
|
+
getColorsForPercent(percent, baseColors) {
|
|
533
626
|
const threshold = this.config.budget?.warningThreshold ?? 80;
|
|
534
627
|
if (percent >= 100) {
|
|
535
|
-
return
|
|
628
|
+
return this.theme.critical;
|
|
536
629
|
} else if (percent >= threshold) {
|
|
537
|
-
return
|
|
630
|
+
return this.theme.warning;
|
|
538
631
|
}
|
|
539
|
-
return
|
|
632
|
+
return baseColors;
|
|
540
633
|
}
|
|
541
|
-
|
|
542
|
-
if (
|
|
543
|
-
|
|
634
|
+
renderPowerline(segments) {
|
|
635
|
+
if (segments.length === 0) return "";
|
|
636
|
+
let output = "";
|
|
637
|
+
for (let i = 0; i < segments.length; i++) {
|
|
638
|
+
const seg = segments[i];
|
|
639
|
+
const nextColors = i < segments.length - 1 ? segments[i + 1].colors : null;
|
|
640
|
+
output += ansi.bg(seg.colors.bg) + ansi.fg(seg.colors.fg) + seg.text;
|
|
641
|
+
output += RESET_CODE;
|
|
642
|
+
if (nextColors) {
|
|
643
|
+
output += ansi.fg(seg.colors.bg) + ansi.bg(nextColors.bg) + this.symbols.rightArrow;
|
|
644
|
+
} else {
|
|
645
|
+
output += ansi.fg(seg.colors.bg) + this.symbols.rightArrow;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
output += RESET_CODE;
|
|
649
|
+
return output;
|
|
650
|
+
}
|
|
651
|
+
renderFallback(segments) {
|
|
652
|
+
return segments.map((seg) => ansi.bg(seg.colors.bg) + ansi.fg(seg.colors.fg) + seg.text + RESET_CODE).join(` ${this.symbols.separator} `);
|
|
653
|
+
}
|
|
654
|
+
renderDirectory(ctx) {
|
|
655
|
+
if (!this.config.directory?.enabled || !ctx.envInfo.directory) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
const name = ctx.compact && ctx.envInfo.directory.length > 12 ? ctx.envInfo.directory.slice(0, 10) + "\u2026" : ctx.envInfo.directory;
|
|
659
|
+
return {
|
|
660
|
+
text: ` ${name} `,
|
|
661
|
+
colors: this.theme.directory
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
renderGit(ctx) {
|
|
665
|
+
if (!this.config.git?.enabled || !ctx.envInfo.gitBranch) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
const dirtyIndicator = ctx.envInfo.gitDirty ? " \u25CF" : "";
|
|
669
|
+
const icon = this.usePowerline ? this.symbols.branch : "";
|
|
670
|
+
const prefix = icon ? `${icon} ` : "";
|
|
671
|
+
let branch = ctx.envInfo.gitBranch;
|
|
672
|
+
if (ctx.compact && branch.length > 10) {
|
|
673
|
+
branch = branch.slice(0, 8) + "\u2026";
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
text: ` ${prefix}${branch}${dirtyIndicator} `,
|
|
677
|
+
colors: this.theme.git
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
renderModel(ctx) {
|
|
681
|
+
if (!this.config.model?.enabled || !ctx.envInfo.model) {
|
|
682
|
+
return null;
|
|
544
683
|
}
|
|
545
|
-
|
|
546
|
-
|
|
684
|
+
const icon = this.usePowerline ? this.symbols.model : "";
|
|
685
|
+
const prefix = icon ? `${icon} ` : "";
|
|
686
|
+
return {
|
|
687
|
+
text: ` ${prefix}${ctx.envInfo.model} `,
|
|
688
|
+
colors: this.theme.model
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
renderBlock(ctx) {
|
|
692
|
+
if (!ctx.blockInfo || !this.config.block?.enabled) {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
const icon = this.usePowerline ? this.symbols.block : "BLK";
|
|
696
|
+
if (ctx.blockInfo.percentUsed === null) {
|
|
697
|
+
return {
|
|
698
|
+
text: ` ${icon} -- `,
|
|
699
|
+
colors: this.theme.block
|
|
700
|
+
};
|
|
547
701
|
}
|
|
548
|
-
const percent = blockInfo.percentUsed;
|
|
549
|
-
const
|
|
702
|
+
const percent = ctx.blockInfo.percentUsed;
|
|
703
|
+
const colors = this.getColorsForPercent(percent, this.theme.block);
|
|
704
|
+
const displayStyle = this.config.block.displayStyle || "text";
|
|
550
705
|
const barWidth = this.config.block.barWidth || 10;
|
|
551
706
|
const showTime = this.config.block.showTimeRemaining ?? true;
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
707
|
+
const trend = this.getTrendSymbol(ctx.trendInfo?.fiveHourTrend ?? null);
|
|
708
|
+
let text;
|
|
709
|
+
if (displayStyle === "bar" && !ctx.compact) {
|
|
710
|
+
const bar = this.formatProgressBar(percent, barWidth);
|
|
711
|
+
text = `${bar} ${Math.round(percent)}%${trend}`;
|
|
556
712
|
} else {
|
|
557
|
-
|
|
713
|
+
text = `${Math.round(percent)}%${trend}`;
|
|
558
714
|
}
|
|
559
|
-
if (showTime && blockInfo.timeRemaining !== null) {
|
|
560
|
-
const timeStr = this.formatTimeRemaining(blockInfo.timeRemaining);
|
|
561
|
-
|
|
715
|
+
if (showTime && ctx.blockInfo.timeRemaining !== null && !ctx.compact) {
|
|
716
|
+
const timeStr = this.formatTimeRemaining(ctx.blockInfo.timeRemaining, ctx.compact);
|
|
717
|
+
text += ` (${timeStr})`;
|
|
562
718
|
}
|
|
563
|
-
return
|
|
719
|
+
return {
|
|
720
|
+
text: ` ${icon} ${text} `,
|
|
721
|
+
colors
|
|
722
|
+
};
|
|
564
723
|
}
|
|
565
|
-
renderWeekly(
|
|
566
|
-
if (!weeklyInfo || !this.config.weekly?.enabled) {
|
|
567
|
-
return
|
|
724
|
+
renderWeekly(ctx) {
|
|
725
|
+
if (!ctx.weeklyInfo || !this.config.weekly?.enabled) {
|
|
726
|
+
return null;
|
|
568
727
|
}
|
|
569
|
-
|
|
570
|
-
|
|
728
|
+
const icon = this.usePowerline ? this.symbols.weekly : "WK";
|
|
729
|
+
if (ctx.weeklyInfo.percentUsed === null) {
|
|
730
|
+
return {
|
|
731
|
+
text: ` ${icon} -- `,
|
|
732
|
+
colors: this.theme.weekly
|
|
733
|
+
};
|
|
571
734
|
}
|
|
572
|
-
const percent = weeklyInfo.percentUsed;
|
|
573
|
-
const displayStyle = this.config.weekly.displayStyle || "
|
|
735
|
+
const percent = ctx.weeklyInfo.percentUsed;
|
|
736
|
+
const displayStyle = this.config.weekly.displayStyle || "text";
|
|
574
737
|
const barWidth = this.config.weekly.barWidth || 10;
|
|
575
738
|
const showWeekProgress = this.config.weekly.showWeekProgress ?? true;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
739
|
+
const trend = this.getTrendSymbol(ctx.trendInfo?.sevenDayTrend ?? null);
|
|
740
|
+
let text;
|
|
741
|
+
if (displayStyle === "bar" && !ctx.compact) {
|
|
742
|
+
const bar = this.formatProgressBar(percent, barWidth);
|
|
743
|
+
text = `${bar} ${Math.round(percent)}%${trend}`;
|
|
580
744
|
} else {
|
|
581
|
-
|
|
745
|
+
text = `${Math.round(percent)}%${trend}`;
|
|
582
746
|
}
|
|
583
|
-
if (showWeekProgress) {
|
|
584
|
-
|
|
747
|
+
if (showWeekProgress && !ctx.compact) {
|
|
748
|
+
text += ` (wk ${ctx.weeklyInfo.weekProgressPercent}%)`;
|
|
585
749
|
}
|
|
586
|
-
return
|
|
750
|
+
return {
|
|
751
|
+
text: ` ${icon} ${text} `,
|
|
752
|
+
colors: this.theme.weekly
|
|
753
|
+
};
|
|
587
754
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
755
|
+
getSegment(name, ctx) {
|
|
756
|
+
switch (name) {
|
|
757
|
+
case "directory":
|
|
758
|
+
return this.renderDirectory(ctx);
|
|
759
|
+
case "git":
|
|
760
|
+
return this.renderGit(ctx);
|
|
761
|
+
case "model":
|
|
762
|
+
return this.renderModel(ctx);
|
|
763
|
+
case "block":
|
|
764
|
+
return this.renderBlock(ctx);
|
|
765
|
+
case "weekly":
|
|
766
|
+
return this.renderWeekly(ctx);
|
|
767
|
+
default:
|
|
768
|
+
return null;
|
|
593
769
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
770
|
+
}
|
|
771
|
+
render(blockInfo, weeklyInfo, envInfo, trendInfo = null) {
|
|
772
|
+
const compact = this.isCompactMode();
|
|
773
|
+
const ctx = {
|
|
774
|
+
blockInfo,
|
|
775
|
+
weeklyInfo,
|
|
776
|
+
envInfo,
|
|
777
|
+
trendInfo,
|
|
778
|
+
compact
|
|
779
|
+
};
|
|
780
|
+
const segments = [];
|
|
781
|
+
const order = this.config.segmentOrder ?? ["directory", "git", "model", "block", "weekly"];
|
|
782
|
+
for (const name of order) {
|
|
783
|
+
const segment = this.getSegment(name, ctx);
|
|
784
|
+
if (segment) {
|
|
785
|
+
segments.push(segment);
|
|
786
|
+
}
|
|
597
787
|
}
|
|
598
|
-
if (
|
|
788
|
+
if (segments.length === 0) {
|
|
599
789
|
return "";
|
|
600
790
|
}
|
|
601
|
-
|
|
602
|
-
|
|
791
|
+
if (this.usePowerline) {
|
|
792
|
+
return this.renderPowerline(segments);
|
|
793
|
+
} else {
|
|
794
|
+
return this.renderFallback(segments);
|
|
795
|
+
}
|
|
603
796
|
}
|
|
604
797
|
};
|
|
605
798
|
|
|
799
|
+
// src/utils/environment.ts
|
|
800
|
+
import { execSync } from "child_process";
|
|
801
|
+
import { basename } from "path";
|
|
802
|
+
|
|
803
|
+
// src/utils/claude-hook.ts
|
|
804
|
+
async function readHookData() {
|
|
805
|
+
if (process.stdin.isTTY) {
|
|
806
|
+
debug("stdin is TTY, no hook data");
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
const chunks = [];
|
|
811
|
+
const result = await Promise.race([
|
|
812
|
+
new Promise((resolve, reject) => {
|
|
813
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
814
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
815
|
+
process.stdin.on("error", reject);
|
|
816
|
+
}),
|
|
817
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 100))
|
|
818
|
+
]);
|
|
819
|
+
if (!result || result.trim() === "") {
|
|
820
|
+
debug("No stdin data received");
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
const hookData = JSON.parse(result);
|
|
824
|
+
debug("Hook data received:", JSON.stringify(hookData));
|
|
825
|
+
return hookData;
|
|
826
|
+
} catch (error) {
|
|
827
|
+
debug("Error reading hook data:", error);
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function formatModelName(modelId, displayName) {
|
|
832
|
+
if (displayName && displayName.length <= 20) {
|
|
833
|
+
const clean = displayName.replace(/^Claude\s*/i, "").trim();
|
|
834
|
+
if (clean) return clean;
|
|
835
|
+
}
|
|
836
|
+
const mappings = {
|
|
837
|
+
"claude-opus-4-5-20251101": "Opus 4.5",
|
|
838
|
+
"claude-opus-4-20250514": "Opus 4",
|
|
839
|
+
"claude-sonnet-4-20250514": "Sonnet 4",
|
|
840
|
+
"claude-3-5-sonnet-20241022": "Sonnet 3.5",
|
|
841
|
+
"claude-3-5-sonnet-latest": "Sonnet 3.5",
|
|
842
|
+
"claude-3-5-sonnet": "Sonnet 3.5",
|
|
843
|
+
"claude-3-opus-20240229": "Opus 3",
|
|
844
|
+
"claude-3-opus": "Opus 3",
|
|
845
|
+
"claude-3-sonnet-20240229": "Sonnet 3",
|
|
846
|
+
"claude-3-haiku-20240307": "Haiku 3",
|
|
847
|
+
"claude-3-haiku": "Haiku 3"
|
|
848
|
+
};
|
|
849
|
+
if (mappings[modelId]) {
|
|
850
|
+
return mappings[modelId];
|
|
851
|
+
}
|
|
852
|
+
const lower = modelId.toLowerCase();
|
|
853
|
+
if (lower.includes("opus")) {
|
|
854
|
+
if (lower.includes("4-5") || lower.includes("4.5")) return "Opus 4.5";
|
|
855
|
+
if (lower.includes("4")) return "Opus 4";
|
|
856
|
+
if (lower.includes("3")) return "Opus 3";
|
|
857
|
+
return "Opus";
|
|
858
|
+
}
|
|
859
|
+
if (lower.includes("sonnet")) {
|
|
860
|
+
if (lower.includes("4")) return "Sonnet 4";
|
|
861
|
+
if (lower.includes("3-5") || lower.includes("3.5")) return "Sonnet 3.5";
|
|
862
|
+
if (lower.includes("3")) return "Sonnet 3";
|
|
863
|
+
return "Sonnet";
|
|
864
|
+
}
|
|
865
|
+
if (lower.includes("haiku")) {
|
|
866
|
+
if (lower.includes("3")) return "Haiku 3";
|
|
867
|
+
return "Haiku";
|
|
868
|
+
}
|
|
869
|
+
return modelId.length > 15 ? modelId.slice(0, 15) : modelId;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/utils/environment.ts
|
|
873
|
+
function getDirectoryName(hookData) {
|
|
874
|
+
try {
|
|
875
|
+
if (hookData?.workspace?.project_dir) {
|
|
876
|
+
return basename(hookData.workspace.project_dir);
|
|
877
|
+
}
|
|
878
|
+
if (hookData?.cwd) {
|
|
879
|
+
return basename(hookData.cwd);
|
|
880
|
+
}
|
|
881
|
+
return basename(process.cwd());
|
|
882
|
+
} catch (error) {
|
|
883
|
+
debug("Error getting directory name:", error);
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
function getGitBranch() {
|
|
888
|
+
try {
|
|
889
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
890
|
+
encoding: "utf-8",
|
|
891
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
892
|
+
}).trim();
|
|
893
|
+
return branch || null;
|
|
894
|
+
} catch (error) {
|
|
895
|
+
debug("Error getting git branch:", error);
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function hasGitChanges() {
|
|
900
|
+
try {
|
|
901
|
+
const status = execSync("git status --porcelain", {
|
|
902
|
+
encoding: "utf-8",
|
|
903
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
904
|
+
}).trim();
|
|
905
|
+
return status.length > 0;
|
|
906
|
+
} catch (error) {
|
|
907
|
+
debug("Error checking git status:", error);
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
function getClaudeModel(hookData) {
|
|
912
|
+
if (hookData?.model?.id) {
|
|
913
|
+
return formatModelName(hookData.model.id, hookData.model.display_name);
|
|
914
|
+
}
|
|
915
|
+
const model = process.env.CLAUDE_MODEL || process.env.CLAUDE_CODE_MODEL || process.env.ANTHROPIC_MODEL;
|
|
916
|
+
if (model) {
|
|
917
|
+
return formatModelName(model);
|
|
918
|
+
}
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
function getEnvironmentInfo(hookData) {
|
|
922
|
+
return {
|
|
923
|
+
directory: getDirectoryName(hookData),
|
|
924
|
+
gitBranch: getGitBranch(),
|
|
925
|
+
gitDirty: hasGitChanges(),
|
|
926
|
+
model: getClaudeModel(hookData)
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
606
930
|
// src/index.ts
|
|
607
931
|
async function main() {
|
|
608
932
|
try {
|
|
609
933
|
const config = loadConfig();
|
|
610
934
|
debug("Config loaded:", JSON.stringify(config));
|
|
935
|
+
const hookData = await readHookData();
|
|
936
|
+
debug("Hook data:", JSON.stringify(hookData));
|
|
937
|
+
const envInfo = getEnvironmentInfo(hookData);
|
|
938
|
+
debug("Environment info:", JSON.stringify(envInfo));
|
|
611
939
|
const blockProvider = new BlockProvider();
|
|
612
940
|
const weeklyProvider = new WeeklyProvider();
|
|
613
941
|
const pollInterval = config.budget?.pollInterval ?? 15;
|
|
@@ -622,8 +950,10 @@ async function main() {
|
|
|
622
950
|
]);
|
|
623
951
|
debug("Block info:", JSON.stringify(blockInfo));
|
|
624
952
|
debug("Weekly info:", JSON.stringify(weeklyInfo));
|
|
953
|
+
const trendInfo = config.showTrend ? getUsageTrend() : null;
|
|
954
|
+
debug("Trend info:", JSON.stringify(trendInfo));
|
|
625
955
|
const renderer = new Renderer(config);
|
|
626
|
-
const output = renderer.render(blockInfo, weeklyInfo);
|
|
956
|
+
const output = renderer.render(blockInfo, weeklyInfo, envInfo, trendInfo);
|
|
627
957
|
if (output) {
|
|
628
958
|
process.stdout.write(output);
|
|
629
959
|
}
|