claude-limitline 1.1.0 → 1.3.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 +50 -72
- package/dist/index.js +305 -60
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -42,19 +42,11 @@ npm install -g claude-limitline
|
|
|
42
42
|
|
|
43
43
|
### From Source
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
git clone https://github.com/tylergraydev/claude-limitline.git
|
|
47
|
-
cd claude-limitline
|
|
48
|
-
npm install
|
|
49
|
-
npm run build
|
|
50
|
-
npm link
|
|
51
|
-
```
|
|
45
|
+
See [Development](#development) section, then run `npm link` to make it available globally.
|
|
52
46
|
|
|
53
47
|
## Quick Start
|
|
54
48
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
**Add to your Claude Code settings file** (`~/.claude/settings.json`):
|
|
49
|
+
Add to your Claude Code settings file (`~/.claude/settings.json`):
|
|
58
50
|
|
|
59
51
|
```json
|
|
60
52
|
{
|
|
@@ -67,57 +59,19 @@ The easiest way to use claude-limitline is to add it directly to your Claude Cod
|
|
|
67
59
|
|
|
68
60
|
That's it! The status line will now show your usage limits in Claude Code.
|
|
69
61
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Here's a complete example with other common settings:
|
|
73
|
-
|
|
74
|
-
```json
|
|
75
|
-
{
|
|
76
|
-
"permissions": {
|
|
77
|
-
"defaultMode": "default"
|
|
78
|
-
},
|
|
79
|
-
"statusLine": {
|
|
80
|
-
"type": "command",
|
|
81
|
-
"command": "npx claude-limitline"
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Global Install (faster startup)
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
npm install -g claude-limitline
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Then update your settings:
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"statusLine": {
|
|
97
|
-
"type": "command",
|
|
98
|
-
"command": "claude-limitline"
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### Test It
|
|
104
|
-
|
|
105
|
-
Run standalone to verify it's working:
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
# Simulate Claude Code hook data
|
|
109
|
-
echo '{"model":{"id":"claude-opus-4-5-20251101"}}' | npx claude-limitline
|
|
110
|
-
```
|
|
62
|
+
> **Tip:** For faster startup, use `"command": "claude-limitline"` after installing globally.
|
|
111
63
|
|
|
112
64
|
## Configuration
|
|
113
65
|
|
|
114
|
-
Create a
|
|
66
|
+
Create a `claude-limitline.json` file in your Claude config directory (`~/.claude/claude-limitline.json`) or `.claude-limitline.json` in your current working directory:
|
|
115
67
|
|
|
116
68
|
```json
|
|
117
69
|
{
|
|
118
70
|
"display": {
|
|
119
71
|
"style": "powerline",
|
|
120
|
-
"useNerdFonts": true
|
|
72
|
+
"useNerdFonts": true,
|
|
73
|
+
"compactMode": "auto",
|
|
74
|
+
"compactWidth": 80
|
|
121
75
|
},
|
|
122
76
|
"directory": {
|
|
123
77
|
"enabled": true
|
|
@@ -138,13 +92,16 @@ Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitl
|
|
|
138
92
|
"enabled": true,
|
|
139
93
|
"displayStyle": "text",
|
|
140
94
|
"barWidth": 10,
|
|
141
|
-
"showWeekProgress": true
|
|
95
|
+
"showWeekProgress": true,
|
|
96
|
+
"viewMode": "smart"
|
|
142
97
|
},
|
|
143
98
|
"budget": {
|
|
144
99
|
"pollInterval": 15,
|
|
145
100
|
"warningThreshold": 80
|
|
146
101
|
},
|
|
147
|
-
"theme": "dark"
|
|
102
|
+
"theme": "dark",
|
|
103
|
+
"segmentOrder": ["directory", "git", "model", "block", "weekly"],
|
|
104
|
+
"showTrend": true
|
|
148
105
|
}
|
|
149
106
|
```
|
|
150
107
|
|
|
@@ -153,6 +110,8 @@ Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitl
|
|
|
153
110
|
| Option | Description | Default |
|
|
154
111
|
|--------|-------------|---------|
|
|
155
112
|
| `display.useNerdFonts` | Use Nerd Font symbols for powerline | `true` |
|
|
113
|
+
| `display.compactMode` | `"auto"`, `"always"`, or `"never"` | `"auto"` |
|
|
114
|
+
| `display.compactWidth` | Terminal width threshold for compact mode | `80` |
|
|
156
115
|
| `directory.enabled` | Show repository/directory name | `true` |
|
|
157
116
|
| `git.enabled` | Show git branch with dirty indicator | `true` |
|
|
158
117
|
| `model.enabled` | Show Claude model name | `true` |
|
|
@@ -164,9 +123,24 @@ Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitl
|
|
|
164
123
|
| `weekly.displayStyle` | `"bar"` or `"text"` | `"text"` |
|
|
165
124
|
| `weekly.barWidth` | Width of progress bar in characters | `10` |
|
|
166
125
|
| `weekly.showWeekProgress` | Show week progress percentage | `true` |
|
|
126
|
+
| `weekly.viewMode` | `"simple"`, `"detailed"`, or `"smart"` | `"simple"` |
|
|
167
127
|
| `budget.pollInterval` | Minutes between API calls | `15` |
|
|
168
128
|
| `budget.warningThreshold` | Percentage to trigger warning color | `80` |
|
|
169
129
|
| `theme` | Color theme name | `"dark"` |
|
|
130
|
+
| `segmentOrder` | Array to customize segment order | `["directory", "git", "model", "block", "weekly"]` |
|
|
131
|
+
| `showTrend` | Show ↑↓ arrows for usage changes | `true` |
|
|
132
|
+
|
|
133
|
+
### Weekly View Modes
|
|
134
|
+
|
|
135
|
+
The weekly segment supports three view modes for displaying usage limits:
|
|
136
|
+
|
|
137
|
+
| Mode | Description | Example |
|
|
138
|
+
|------|-------------|---------|
|
|
139
|
+
| `simple` | Shows overall weekly usage only (default) | `○ 47% (wk 85%)` |
|
|
140
|
+
| `detailed` | Shows overall, Opus, and Sonnet usage side by side | `○47% ◈15% ◇7%` |
|
|
141
|
+
| `smart` | Shows the most restrictive (bottleneck) limit with indicator | `○47%▲ (wk 85%)` |
|
|
142
|
+
|
|
143
|
+
**Note:** Model-specific limits (Opus/Sonnet) are only available on certain subscription tiers. When a model-specific limit is not available, it will be hidden from the display.
|
|
170
144
|
|
|
171
145
|
### Available Themes
|
|
172
146
|
|
|
@@ -206,34 +180,38 @@ claude-limitline retrieves data from two sources:
|
|
|
206
180
|
|
|
207
181
|
## Development
|
|
208
182
|
|
|
209
|
-
### Setup
|
|
210
|
-
|
|
211
183
|
```bash
|
|
212
184
|
git clone https://github.com/tylergraydev/claude-limitline.git
|
|
213
185
|
cd claude-limitline
|
|
214
186
|
npm install
|
|
187
|
+
npm run build # Build once
|
|
188
|
+
npm run dev # Watch mode
|
|
215
189
|
```
|
|
216
190
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
```bash
|
|
220
|
-
npm run build
|
|
221
|
-
```
|
|
191
|
+
## Testing
|
|
222
192
|
|
|
223
|
-
|
|
193
|
+
The project uses [Vitest](https://vitest.dev/) for testing with 166 tests covering config loading, themes, segments, utilities, and rendering.
|
|
224
194
|
|
|
225
195
|
```bash
|
|
226
|
-
npm
|
|
196
|
+
npm test # Run tests once
|
|
197
|
+
npm run test:watch # Watch mode
|
|
198
|
+
npm run test:coverage # Coverage report
|
|
227
199
|
```
|
|
228
200
|
|
|
229
|
-
###
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
201
|
+
### Test Structure
|
|
202
|
+
|
|
203
|
+
| File | Tests | Coverage |
|
|
204
|
+
|------|-------|----------|
|
|
205
|
+
| `src/config/loader.test.ts` | 7 | Config loading, merging, fallbacks |
|
|
206
|
+
| `src/themes/index.test.ts` | 37 | Theme retrieval, color validation |
|
|
207
|
+
| `src/segments/block.test.ts` | 8 | Block segment, time calculations |
|
|
208
|
+
| `src/segments/weekly.test.ts` | 10 | Weekly segment, week progress |
|
|
209
|
+
| `src/utils/oauth.test.ts` | 10 | API responses, caching |
|
|
210
|
+
| `src/utils/claude-hook.test.ts` | 21 | Model name formatting |
|
|
211
|
+
| `src/utils/environment.test.ts` | 20 | Git branch, directory detection |
|
|
212
|
+
| `src/utils/terminal.test.ts` | 13 | Terminal width, ANSI handling |
|
|
213
|
+
| `src/utils/logger.test.ts` | 8 | Debug/error logging |
|
|
214
|
+
| `src/renderer.test.ts` | 21 | Segment rendering, ordering |
|
|
237
215
|
|
|
238
216
|
## Debug Mode
|
|
239
217
|
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,9 @@ function debug(...args) {
|
|
|
18
18
|
var DEFAULT_CONFIG = {
|
|
19
19
|
display: {
|
|
20
20
|
style: "powerline",
|
|
21
|
-
useNerdFonts: true
|
|
21
|
+
useNerdFonts: true,
|
|
22
|
+
compactMode: "auto",
|
|
23
|
+
compactWidth: 80
|
|
22
24
|
},
|
|
23
25
|
directory: {
|
|
24
26
|
enabled: true
|
|
@@ -39,13 +41,16 @@ var DEFAULT_CONFIG = {
|
|
|
39
41
|
enabled: true,
|
|
40
42
|
displayStyle: "text",
|
|
41
43
|
barWidth: 10,
|
|
42
|
-
showWeekProgress: true
|
|
44
|
+
showWeekProgress: true,
|
|
45
|
+
viewMode: "simple"
|
|
43
46
|
},
|
|
44
47
|
budget: {
|
|
45
48
|
pollInterval: 15,
|
|
46
49
|
warningThreshold: 80
|
|
47
50
|
},
|
|
48
|
-
theme: "dark"
|
|
51
|
+
theme: "dark",
|
|
52
|
+
segmentOrder: ["directory", "git", "model", "block", "weekly"],
|
|
53
|
+
showTrend: true
|
|
49
54
|
};
|
|
50
55
|
|
|
51
56
|
// src/config/loader.ts
|
|
@@ -65,8 +70,7 @@ function deepMerge(target, source) {
|
|
|
65
70
|
function loadConfig() {
|
|
66
71
|
const configPaths = [
|
|
67
72
|
path.join(process.cwd(), ".claude-limitline.json"),
|
|
68
|
-
path.join(os.homedir(), ".claude-limitline.json")
|
|
69
|
-
path.join(os.homedir(), ".config", "claude-limitline", "config.json")
|
|
73
|
+
path.join(os.homedir(), ".claude", "claude-limitline.json")
|
|
70
74
|
];
|
|
71
75
|
for (const configPath of configPaths) {
|
|
72
76
|
try {
|
|
@@ -256,6 +260,8 @@ async function fetchUsageFromAPI(token) {
|
|
|
256
260
|
return {
|
|
257
261
|
fiveHour: parseUsageBlock(data.five_hour),
|
|
258
262
|
sevenDay: parseUsageBlock(data.seven_day),
|
|
263
|
+
sevenDayOpus: parseUsageBlock(data.seven_day_opus ?? void 0),
|
|
264
|
+
sevenDaySonnet: parseUsageBlock(data.seven_day_sonnet ?? void 0),
|
|
259
265
|
raw: data
|
|
260
266
|
};
|
|
261
267
|
} catch (error) {
|
|
@@ -264,8 +270,32 @@ async function fetchUsageFromAPI(token) {
|
|
|
264
270
|
}
|
|
265
271
|
}
|
|
266
272
|
var cachedUsage = null;
|
|
273
|
+
var previousUsage = null;
|
|
267
274
|
var cacheTimestamp = 0;
|
|
268
275
|
var cachedToken = null;
|
|
276
|
+
function getUsageTrend() {
|
|
277
|
+
const result = {
|
|
278
|
+
fiveHourTrend: null,
|
|
279
|
+
sevenDayTrend: null,
|
|
280
|
+
sevenDayOpusTrend: null,
|
|
281
|
+
sevenDaySonnetTrend: null
|
|
282
|
+
};
|
|
283
|
+
if (!cachedUsage || !previousUsage) {
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
const compareTrend = (current, previous) => {
|
|
287
|
+
if (!current || !previous) return null;
|
|
288
|
+
const diff = current.percentUsed - previous.percentUsed;
|
|
289
|
+
if (diff > 0.5) return "up";
|
|
290
|
+
if (diff < -0.5) return "down";
|
|
291
|
+
return "same";
|
|
292
|
+
};
|
|
293
|
+
result.fiveHourTrend = compareTrend(cachedUsage.fiveHour, previousUsage.fiveHour);
|
|
294
|
+
result.sevenDayTrend = compareTrend(cachedUsage.sevenDay, previousUsage.sevenDay);
|
|
295
|
+
result.sevenDayOpusTrend = compareTrend(cachedUsage.sevenDayOpus, previousUsage.sevenDayOpus);
|
|
296
|
+
result.sevenDaySonnetTrend = compareTrend(cachedUsage.sevenDaySonnet, previousUsage.sevenDaySonnet);
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
269
299
|
async function getRealtimeUsage(pollIntervalMinutes = 15) {
|
|
270
300
|
const now = Date.now();
|
|
271
301
|
const cacheAgeMs = now - cacheTimestamp;
|
|
@@ -283,6 +313,7 @@ async function getRealtimeUsage(pollIntervalMinutes = 15) {
|
|
|
283
313
|
}
|
|
284
314
|
const usage = await fetchUsageFromAPI(cachedToken);
|
|
285
315
|
if (usage) {
|
|
316
|
+
previousUsage = cachedUsage;
|
|
286
317
|
cachedUsage = usage;
|
|
287
318
|
cacheTimestamp = now;
|
|
288
319
|
debug("Refreshed realtime usage cache");
|
|
@@ -389,7 +420,11 @@ var WeeklyProvider = class {
|
|
|
389
420
|
percentUsed: null,
|
|
390
421
|
resetAt: null,
|
|
391
422
|
isRealtime: false,
|
|
392
|
-
weekProgressPercent
|
|
423
|
+
weekProgressPercent,
|
|
424
|
+
opusPercentUsed: null,
|
|
425
|
+
sonnetPercentUsed: null,
|
|
426
|
+
opusResetAt: null,
|
|
427
|
+
sonnetResetAt: null
|
|
393
428
|
};
|
|
394
429
|
}
|
|
395
430
|
async getRealtimeWeeklyInfo(pollInterval) {
|
|
@@ -400,15 +435,27 @@ var WeeklyProvider = class {
|
|
|
400
435
|
return null;
|
|
401
436
|
}
|
|
402
437
|
const sevenDay = usage.sevenDay;
|
|
438
|
+
const sevenDayOpus = usage.sevenDayOpus;
|
|
439
|
+
const sevenDaySonnet = usage.sevenDaySonnet;
|
|
403
440
|
const weekProgressPercent = this.calculateWeekProgressFromResetTime(sevenDay.resetAt);
|
|
404
441
|
debug(
|
|
405
442
|
`Weekly segment (realtime): ${sevenDay.percentUsed}% used, resets at ${sevenDay.resetAt.toISOString()}`
|
|
406
443
|
);
|
|
444
|
+
if (sevenDayOpus) {
|
|
445
|
+
debug(`Weekly Opus: ${sevenDayOpus.percentUsed}% used`);
|
|
446
|
+
}
|
|
447
|
+
if (sevenDaySonnet) {
|
|
448
|
+
debug(`Weekly Sonnet: ${sevenDaySonnet.percentUsed}% used`);
|
|
449
|
+
}
|
|
407
450
|
return {
|
|
408
451
|
percentUsed: sevenDay.percentUsed,
|
|
409
452
|
resetAt: sevenDay.resetAt,
|
|
410
453
|
isRealtime: true,
|
|
411
|
-
weekProgressPercent
|
|
454
|
+
weekProgressPercent,
|
|
455
|
+
opusPercentUsed: sevenDayOpus?.percentUsed ?? null,
|
|
456
|
+
sonnetPercentUsed: sevenDaySonnet?.percentUsed ?? null,
|
|
457
|
+
opusResetAt: sevenDayOpus?.resetAt ?? null,
|
|
458
|
+
sonnetResetAt: sevenDaySonnet?.resetAt ?? null
|
|
412
459
|
};
|
|
413
460
|
} catch (error) {
|
|
414
461
|
debug("Error getting realtime weekly info:", error);
|
|
@@ -423,10 +470,18 @@ var SYMBOLS = {
|
|
|
423
470
|
left: "\uE0B2",
|
|
424
471
|
branch: "\uE0A0",
|
|
425
472
|
separator: "\uE0B1",
|
|
426
|
-
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
//
|
|
473
|
+
model: "\u2731",
|
|
474
|
+
// Heavy asterisk ✱
|
|
475
|
+
block_cost: "\u25EB",
|
|
476
|
+
// White square with vertical bisecting line ◫
|
|
477
|
+
weekly_cost: "\u25CB",
|
|
478
|
+
// Circle ○
|
|
479
|
+
opus_cost: "\u25C8",
|
|
480
|
+
// Diamond with dot ◈
|
|
481
|
+
sonnet_cost: "\u25C7",
|
|
482
|
+
// White diamond ◇
|
|
483
|
+
bottleneck: "\u25B2",
|
|
484
|
+
// Black up-pointing triangle ▲
|
|
430
485
|
progress_full: "\u2588",
|
|
431
486
|
// Full block
|
|
432
487
|
progress_empty: "\u2591"
|
|
@@ -437,8 +492,12 @@ var TEXT_SYMBOLS = {
|
|
|
437
492
|
left: "<",
|
|
438
493
|
branch: "",
|
|
439
494
|
separator: "|",
|
|
495
|
+
model: "*",
|
|
440
496
|
block_cost: "BLK",
|
|
441
497
|
weekly_cost: "WK",
|
|
498
|
+
opus_cost: "Op",
|
|
499
|
+
sonnet_cost: "So",
|
|
500
|
+
bottleneck: "*",
|
|
442
501
|
progress_full: "#",
|
|
443
502
|
progress_empty: "-"
|
|
444
503
|
};
|
|
@@ -472,6 +531,10 @@ var darkTheme = {
|
|
|
472
531
|
model: { bg: "#2d2d2d", fg: "#ffffff" },
|
|
473
532
|
block: { bg: "#2a2a2a", fg: "#87ceeb" },
|
|
474
533
|
weekly: { bg: "#1a1a1a", fg: "#98fb98" },
|
|
534
|
+
opus: { bg: "#1a1a1a", fg: "#c792ea" },
|
|
535
|
+
// Purple for Opus
|
|
536
|
+
sonnet: { bg: "#1a1a1a", fg: "#89ddff" },
|
|
537
|
+
// Light blue for Sonnet
|
|
475
538
|
warning: { bg: "#d75f00", fg: "#ffffff" },
|
|
476
539
|
critical: { bg: "#af0000", fg: "#ffffff" }
|
|
477
540
|
};
|
|
@@ -481,6 +544,10 @@ var lightTheme = {
|
|
|
481
544
|
model: { bg: "#87ceeb", fg: "#000000" },
|
|
482
545
|
block: { bg: "#6366f1", fg: "#ffffff" },
|
|
483
546
|
weekly: { bg: "#10b981", fg: "#ffffff" },
|
|
547
|
+
opus: { bg: "#8b5cf6", fg: "#ffffff" },
|
|
548
|
+
// Purple for Opus
|
|
549
|
+
sonnet: { bg: "#0ea5e9", fg: "#ffffff" },
|
|
550
|
+
// Sky blue for Sonnet
|
|
484
551
|
warning: { bg: "#f59e0b", fg: "#000000" },
|
|
485
552
|
critical: { bg: "#ef4444", fg: "#ffffff" }
|
|
486
553
|
};
|
|
@@ -490,6 +557,10 @@ var nordTheme = {
|
|
|
490
557
|
model: { bg: "#4c566a", fg: "#81a1c1" },
|
|
491
558
|
block: { bg: "#3b4252", fg: "#81a1c1" },
|
|
492
559
|
weekly: { bg: "#2e3440", fg: "#8fbcbb" },
|
|
560
|
+
opus: { bg: "#2e3440", fg: "#b48ead" },
|
|
561
|
+
// Nord purple for Opus
|
|
562
|
+
sonnet: { bg: "#2e3440", fg: "#88c0d0" },
|
|
563
|
+
// Nord frost for Sonnet
|
|
493
564
|
warning: { bg: "#d08770", fg: "#2e3440" },
|
|
494
565
|
critical: { bg: "#bf616a", fg: "#eceff4" }
|
|
495
566
|
};
|
|
@@ -499,6 +570,10 @@ var gruvboxTheme = {
|
|
|
499
570
|
model: { bg: "#665c54", fg: "#83a598" },
|
|
500
571
|
block: { bg: "#3c3836", fg: "#83a598" },
|
|
501
572
|
weekly: { bg: "#282828", fg: "#fabd2f" },
|
|
573
|
+
opus: { bg: "#282828", fg: "#d3869b" },
|
|
574
|
+
// Gruvbox purple for Opus
|
|
575
|
+
sonnet: { bg: "#282828", fg: "#8ec07c" },
|
|
576
|
+
// Gruvbox aqua for Sonnet
|
|
502
577
|
warning: { bg: "#d79921", fg: "#282828" },
|
|
503
578
|
critical: { bg: "#cc241d", fg: "#ebdbb2" }
|
|
504
579
|
};
|
|
@@ -508,6 +583,10 @@ var tokyoNightTheme = {
|
|
|
508
583
|
model: { bg: "#191b29", fg: "#fca7ea" },
|
|
509
584
|
block: { bg: "#2d3748", fg: "#7aa2f7" },
|
|
510
585
|
weekly: { bg: "#1a202c", fg: "#4fd6be" },
|
|
586
|
+
opus: { bg: "#1a202c", fg: "#bb9af7" },
|
|
587
|
+
// Tokyo purple for Opus
|
|
588
|
+
sonnet: { bg: "#1a202c", fg: "#7dcfff" },
|
|
589
|
+
// Tokyo cyan for Sonnet
|
|
511
590
|
warning: { bg: "#e0af68", fg: "#1a1b26" },
|
|
512
591
|
critical: { bg: "#f7768e", fg: "#1a1b26" }
|
|
513
592
|
};
|
|
@@ -517,6 +596,10 @@ var rosePineTheme = {
|
|
|
517
596
|
model: { bg: "#191724", fg: "#ebbcba" },
|
|
518
597
|
block: { bg: "#2a273f", fg: "#eb6f92" },
|
|
519
598
|
weekly: { bg: "#232136", fg: "#9ccfd8" },
|
|
599
|
+
opus: { bg: "#232136", fg: "#c4a7e7" },
|
|
600
|
+
// Rose Pine iris for Opus
|
|
601
|
+
sonnet: { bg: "#232136", fg: "#31748f" },
|
|
602
|
+
// Rose Pine pine for Sonnet
|
|
520
603
|
warning: { bg: "#f6c177", fg: "#191724" },
|
|
521
604
|
critical: { bg: "#eb6f92", fg: "#191724" }
|
|
522
605
|
};
|
|
@@ -532,6 +615,11 @@ function getTheme(name) {
|
|
|
532
615
|
return themes[name] || themes.dark;
|
|
533
616
|
}
|
|
534
617
|
|
|
618
|
+
// src/utils/terminal.ts
|
|
619
|
+
function getTerminalWidth() {
|
|
620
|
+
return process.stdout.columns || 80;
|
|
621
|
+
}
|
|
622
|
+
|
|
535
623
|
// src/renderer.ts
|
|
536
624
|
var Renderer = class {
|
|
537
625
|
config;
|
|
@@ -547,28 +635,49 @@ var Renderer = class {
|
|
|
547
635
|
this.symbols = {
|
|
548
636
|
block: symbolSet.block_cost,
|
|
549
637
|
weekly: symbolSet.weekly_cost,
|
|
638
|
+
opus: symbolSet.opus_cost,
|
|
639
|
+
sonnet: symbolSet.sonnet_cost,
|
|
640
|
+
bottleneck: symbolSet.bottleneck,
|
|
550
641
|
rightArrow: symbolSet.right,
|
|
551
642
|
separator: symbolSet.separator,
|
|
552
643
|
branch: symbolSet.branch,
|
|
553
|
-
model:
|
|
554
|
-
// Lightning bolt for model
|
|
644
|
+
model: symbolSet.model,
|
|
555
645
|
progressFull: symbolSet.progress_full,
|
|
556
|
-
progressEmpty: symbolSet.progress_empty
|
|
646
|
+
progressEmpty: symbolSet.progress_empty,
|
|
647
|
+
trendUp: "\u2191",
|
|
648
|
+
trendDown: "\u2193"
|
|
557
649
|
};
|
|
558
650
|
}
|
|
651
|
+
isCompactMode() {
|
|
652
|
+
const mode = this.config.display?.compactMode ?? "auto";
|
|
653
|
+
if (mode === "always") return true;
|
|
654
|
+
if (mode === "never") return false;
|
|
655
|
+
const threshold = this.config.display?.compactWidth ?? 80;
|
|
656
|
+
const termWidth = getTerminalWidth();
|
|
657
|
+
return termWidth < threshold;
|
|
658
|
+
}
|
|
559
659
|
formatProgressBar(percent, width) {
|
|
560
660
|
const filled = Math.round(percent / 100 * width);
|
|
561
661
|
const empty = width - filled;
|
|
562
662
|
return this.symbols.progressFull.repeat(filled) + this.symbols.progressEmpty.repeat(empty);
|
|
563
663
|
}
|
|
564
|
-
formatTimeRemaining(minutes) {
|
|
664
|
+
formatTimeRemaining(minutes, compact) {
|
|
565
665
|
if (minutes >= 60) {
|
|
566
666
|
const hours = Math.floor(minutes / 60);
|
|
567
667
|
const mins = minutes % 60;
|
|
668
|
+
if (compact) {
|
|
669
|
+
return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
|
|
670
|
+
}
|
|
568
671
|
return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
|
|
569
672
|
}
|
|
570
673
|
return `${minutes}m`;
|
|
571
674
|
}
|
|
675
|
+
getTrendSymbol(trend) {
|
|
676
|
+
if (!this.config.showTrend) return "";
|
|
677
|
+
if (trend === "up") return this.symbols.trendUp;
|
|
678
|
+
if (trend === "down") return this.symbols.trendDown;
|
|
679
|
+
return "";
|
|
680
|
+
}
|
|
572
681
|
getColorsForPercent(percent, baseColors) {
|
|
573
682
|
const threshold = this.config.budget?.warningThreshold ?? 80;
|
|
574
683
|
if (percent >= 100) {
|
|
@@ -598,63 +707,69 @@ var Renderer = class {
|
|
|
598
707
|
renderFallback(segments) {
|
|
599
708
|
return segments.map((seg) => ansi.bg(seg.colors.bg) + ansi.fg(seg.colors.fg) + seg.text + RESET_CODE).join(` ${this.symbols.separator} `);
|
|
600
709
|
}
|
|
601
|
-
renderDirectory(
|
|
602
|
-
if (!this.config.directory?.enabled || !envInfo.directory) {
|
|
710
|
+
renderDirectory(ctx) {
|
|
711
|
+
if (!this.config.directory?.enabled || !ctx.envInfo.directory) {
|
|
603
712
|
return null;
|
|
604
713
|
}
|
|
714
|
+
const name = ctx.compact && ctx.envInfo.directory.length > 12 ? ctx.envInfo.directory.slice(0, 10) + "\u2026" : ctx.envInfo.directory;
|
|
605
715
|
return {
|
|
606
|
-
text: ` ${
|
|
716
|
+
text: ` ${name} `,
|
|
607
717
|
colors: this.theme.directory
|
|
608
718
|
};
|
|
609
719
|
}
|
|
610
|
-
renderGit(
|
|
611
|
-
if (!this.config.git?.enabled || !envInfo.gitBranch) {
|
|
720
|
+
renderGit(ctx) {
|
|
721
|
+
if (!this.config.git?.enabled || !ctx.envInfo.gitBranch) {
|
|
612
722
|
return null;
|
|
613
723
|
}
|
|
614
|
-
const dirtyIndicator = envInfo.gitDirty ? " \u25CF" : "";
|
|
724
|
+
const dirtyIndicator = ctx.envInfo.gitDirty ? " \u25CF" : "";
|
|
615
725
|
const icon = this.usePowerline ? this.symbols.branch : "";
|
|
616
726
|
const prefix = icon ? `${icon} ` : "";
|
|
727
|
+
let branch = ctx.envInfo.gitBranch;
|
|
728
|
+
if (ctx.compact && branch.length > 10) {
|
|
729
|
+
branch = branch.slice(0, 8) + "\u2026";
|
|
730
|
+
}
|
|
617
731
|
return {
|
|
618
|
-
text: ` ${prefix}${
|
|
732
|
+
text: ` ${prefix}${branch}${dirtyIndicator} `,
|
|
619
733
|
colors: this.theme.git
|
|
620
734
|
};
|
|
621
735
|
}
|
|
622
|
-
renderModel(
|
|
623
|
-
if (!this.config.model?.enabled || !envInfo.model) {
|
|
736
|
+
renderModel(ctx) {
|
|
737
|
+
if (!this.config.model?.enabled || !ctx.envInfo.model) {
|
|
624
738
|
return null;
|
|
625
739
|
}
|
|
626
740
|
const icon = this.usePowerline ? this.symbols.model : "";
|
|
627
741
|
const prefix = icon ? `${icon} ` : "";
|
|
628
742
|
return {
|
|
629
|
-
text: ` ${prefix}${envInfo.model} `,
|
|
743
|
+
text: ` ${prefix}${ctx.envInfo.model} `,
|
|
630
744
|
colors: this.theme.model
|
|
631
745
|
};
|
|
632
746
|
}
|
|
633
|
-
renderBlock(
|
|
634
|
-
if (!blockInfo || !this.config.block?.enabled) {
|
|
747
|
+
renderBlock(ctx) {
|
|
748
|
+
if (!ctx.blockInfo || !this.config.block?.enabled) {
|
|
635
749
|
return null;
|
|
636
750
|
}
|
|
637
751
|
const icon = this.usePowerline ? this.symbols.block : "BLK";
|
|
638
|
-
if (blockInfo.percentUsed === null) {
|
|
752
|
+
if (ctx.blockInfo.percentUsed === null) {
|
|
639
753
|
return {
|
|
640
754
|
text: ` ${icon} -- `,
|
|
641
755
|
colors: this.theme.block
|
|
642
756
|
};
|
|
643
757
|
}
|
|
644
|
-
const percent = blockInfo.percentUsed;
|
|
758
|
+
const percent = ctx.blockInfo.percentUsed;
|
|
645
759
|
const colors = this.getColorsForPercent(percent, this.theme.block);
|
|
646
760
|
const displayStyle = this.config.block.displayStyle || "text";
|
|
647
761
|
const barWidth = this.config.block.barWidth || 10;
|
|
648
762
|
const showTime = this.config.block.showTimeRemaining ?? true;
|
|
763
|
+
const trend = this.getTrendSymbol(ctx.trendInfo?.fiveHourTrend ?? null);
|
|
649
764
|
let text;
|
|
650
|
-
if (displayStyle === "bar") {
|
|
765
|
+
if (displayStyle === "bar" && !ctx.compact) {
|
|
651
766
|
const bar = this.formatProgressBar(percent, barWidth);
|
|
652
|
-
text = `${bar} ${Math.round(percent)}
|
|
767
|
+
text = `${bar} ${Math.round(percent)}%${trend}`;
|
|
653
768
|
} else {
|
|
654
|
-
text = `${Math.round(percent)}
|
|
769
|
+
text = `${Math.round(percent)}%${trend}`;
|
|
655
770
|
}
|
|
656
|
-
if (showTime && blockInfo.timeRemaining !== null) {
|
|
657
|
-
const timeStr = this.formatTimeRemaining(blockInfo.timeRemaining);
|
|
771
|
+
if (showTime && ctx.blockInfo.timeRemaining !== null && !ctx.compact) {
|
|
772
|
+
const timeStr = this.formatTimeRemaining(ctx.blockInfo.timeRemaining, ctx.compact);
|
|
658
773
|
text += ` (${timeStr})`;
|
|
659
774
|
}
|
|
660
775
|
return {
|
|
@@ -662,48 +777,176 @@ var Renderer = class {
|
|
|
662
777
|
colors
|
|
663
778
|
};
|
|
664
779
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
return null;
|
|
668
|
-
}
|
|
780
|
+
renderWeeklySimple(ctx) {
|
|
781
|
+
const info = ctx.weeklyInfo;
|
|
669
782
|
const icon = this.usePowerline ? this.symbols.weekly : "WK";
|
|
670
|
-
if (
|
|
783
|
+
if (info.percentUsed === null) {
|
|
671
784
|
return {
|
|
672
785
|
text: ` ${icon} -- `,
|
|
673
786
|
colors: this.theme.weekly
|
|
674
787
|
};
|
|
675
788
|
}
|
|
676
|
-
const percent =
|
|
677
|
-
const displayStyle = this.config.weekly
|
|
678
|
-
const barWidth = this.config.weekly
|
|
679
|
-
const showWeekProgress = this.config.weekly
|
|
789
|
+
const percent = info.percentUsed;
|
|
790
|
+
const displayStyle = this.config.weekly?.displayStyle || "text";
|
|
791
|
+
const barWidth = this.config.weekly?.barWidth || 10;
|
|
792
|
+
const showWeekProgress = this.config.weekly?.showWeekProgress ?? true;
|
|
793
|
+
const trend = this.getTrendSymbol(ctx.trendInfo?.sevenDayTrend ?? null);
|
|
680
794
|
let text;
|
|
681
|
-
if (displayStyle === "bar") {
|
|
795
|
+
if (displayStyle === "bar" && !ctx.compact) {
|
|
682
796
|
const bar = this.formatProgressBar(percent, barWidth);
|
|
683
|
-
text = `${bar} ${Math.round(percent)}
|
|
797
|
+
text = `${bar} ${Math.round(percent)}%${trend}`;
|
|
684
798
|
} else {
|
|
685
|
-
text = `${Math.round(percent)}
|
|
799
|
+
text = `${Math.round(percent)}%${trend}`;
|
|
686
800
|
}
|
|
687
|
-
if (showWeekProgress) {
|
|
688
|
-
text += ` (wk ${
|
|
801
|
+
if (showWeekProgress && !ctx.compact) {
|
|
802
|
+
text += ` (wk ${info.weekProgressPercent}%)`;
|
|
689
803
|
}
|
|
690
804
|
return {
|
|
691
805
|
text: ` ${icon} ${text} `,
|
|
692
806
|
colors: this.theme.weekly
|
|
693
807
|
};
|
|
694
808
|
}
|
|
695
|
-
|
|
809
|
+
renderWeeklyDetailed(ctx) {
|
|
810
|
+
const info = ctx.weeklyInfo;
|
|
811
|
+
const overallIcon = this.usePowerline ? this.symbols.weekly : "All";
|
|
812
|
+
const opusIcon = this.usePowerline ? this.symbols.opus : "Op";
|
|
813
|
+
const sonnetIcon = this.usePowerline ? this.symbols.sonnet : "So";
|
|
814
|
+
const parts = [];
|
|
815
|
+
if (info.percentUsed !== null) {
|
|
816
|
+
const trend = this.getTrendSymbol(ctx.trendInfo?.sevenDayTrend ?? null);
|
|
817
|
+
parts.push(`${overallIcon}${Math.round(info.percentUsed)}%${trend}`);
|
|
818
|
+
}
|
|
819
|
+
if (info.opusPercentUsed !== null) {
|
|
820
|
+
const trend = this.getTrendSymbol(ctx.trendInfo?.sevenDayOpusTrend ?? null);
|
|
821
|
+
parts.push(`${opusIcon}${Math.round(info.opusPercentUsed)}%${trend}`);
|
|
822
|
+
}
|
|
823
|
+
if (info.sonnetPercentUsed !== null) {
|
|
824
|
+
const trend = this.getTrendSymbol(ctx.trendInfo?.sevenDaySonnetTrend ?? null);
|
|
825
|
+
parts.push(`${sonnetIcon}${Math.round(info.sonnetPercentUsed)}%${trend}`);
|
|
826
|
+
}
|
|
827
|
+
if (parts.length === 0) {
|
|
828
|
+
return {
|
|
829
|
+
text: ` ${overallIcon} -- `,
|
|
830
|
+
colors: this.theme.weekly
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
const separator = ctx.compact ? " " : " ";
|
|
834
|
+
const text = parts.join(separator);
|
|
835
|
+
const maxPercent = Math.max(
|
|
836
|
+
info.percentUsed ?? 0,
|
|
837
|
+
info.opusPercentUsed ?? 0,
|
|
838
|
+
info.sonnetPercentUsed ?? 0
|
|
839
|
+
);
|
|
840
|
+
const colors = this.getColorsForPercent(maxPercent, this.theme.weekly);
|
|
841
|
+
return {
|
|
842
|
+
text: ` ${text} `,
|
|
843
|
+
colors
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
renderWeeklySmart(ctx) {
|
|
847
|
+
const info = ctx.weeklyInfo;
|
|
848
|
+
const overallIcon = this.usePowerline ? this.symbols.weekly : "All";
|
|
849
|
+
const opusIcon = this.usePowerline ? this.symbols.opus : "Op";
|
|
850
|
+
const sonnetIcon = this.usePowerline ? this.symbols.sonnet : "So";
|
|
851
|
+
const limits = [];
|
|
852
|
+
if (info.percentUsed !== null) {
|
|
853
|
+
limits.push({
|
|
854
|
+
name: "all",
|
|
855
|
+
icon: overallIcon,
|
|
856
|
+
percent: info.percentUsed,
|
|
857
|
+
trend: ctx.trendInfo?.sevenDayTrend ?? null,
|
|
858
|
+
colors: this.theme.weekly
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
if (info.opusPercentUsed !== null) {
|
|
862
|
+
limits.push({
|
|
863
|
+
name: "opus",
|
|
864
|
+
icon: opusIcon,
|
|
865
|
+
percent: info.opusPercentUsed,
|
|
866
|
+
trend: ctx.trendInfo?.sevenDayOpusTrend ?? null,
|
|
867
|
+
colors: this.theme.opus
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
if (info.sonnetPercentUsed !== null) {
|
|
871
|
+
limits.push({
|
|
872
|
+
name: "sonnet",
|
|
873
|
+
icon: sonnetIcon,
|
|
874
|
+
percent: info.sonnetPercentUsed,
|
|
875
|
+
trend: ctx.trendInfo?.sevenDaySonnetTrend ?? null,
|
|
876
|
+
colors: this.theme.sonnet
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
if (limits.length === 0) {
|
|
880
|
+
return {
|
|
881
|
+
text: ` ${overallIcon} -- `,
|
|
882
|
+
colors: this.theme.weekly
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
const bottleneck = limits.reduce((a, b) => a.percent >= b.percent ? a : b);
|
|
886
|
+
const trend = this.getTrendSymbol(bottleneck.trend);
|
|
887
|
+
const bottleneckIndicator = limits.length > 1 ? this.symbols.bottleneck : "";
|
|
888
|
+
let text = `${bottleneck.icon}${Math.round(bottleneck.percent)}%${trend}`;
|
|
889
|
+
if (bottleneckIndicator && !ctx.compact) {
|
|
890
|
+
text += bottleneckIndicator;
|
|
891
|
+
}
|
|
892
|
+
const showWeekProgress = this.config.weekly?.showWeekProgress ?? true;
|
|
893
|
+
if (showWeekProgress && !ctx.compact) {
|
|
894
|
+
text += ` (wk ${info.weekProgressPercent}%)`;
|
|
895
|
+
}
|
|
896
|
+
const colors = this.getColorsForPercent(bottleneck.percent, bottleneck.colors);
|
|
897
|
+
return {
|
|
898
|
+
text: ` ${text} `,
|
|
899
|
+
colors
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
renderWeekly(ctx) {
|
|
903
|
+
if (!ctx.weeklyInfo || !this.config.weekly?.enabled) {
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
const viewMode = this.config.weekly?.viewMode ?? "simple";
|
|
907
|
+
switch (viewMode) {
|
|
908
|
+
case "detailed":
|
|
909
|
+
return this.renderWeeklyDetailed(ctx);
|
|
910
|
+
case "smart":
|
|
911
|
+
return this.renderWeeklySmart(ctx);
|
|
912
|
+
case "simple":
|
|
913
|
+
default:
|
|
914
|
+
return this.renderWeeklySimple(ctx);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
getSegment(name, ctx) {
|
|
918
|
+
switch (name) {
|
|
919
|
+
case "directory":
|
|
920
|
+
return this.renderDirectory(ctx);
|
|
921
|
+
case "git":
|
|
922
|
+
return this.renderGit(ctx);
|
|
923
|
+
case "model":
|
|
924
|
+
return this.renderModel(ctx);
|
|
925
|
+
case "block":
|
|
926
|
+
return this.renderBlock(ctx);
|
|
927
|
+
case "weekly":
|
|
928
|
+
return this.renderWeekly(ctx);
|
|
929
|
+
default:
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
render(blockInfo, weeklyInfo, envInfo, trendInfo = null) {
|
|
934
|
+
const compact = this.isCompactMode();
|
|
935
|
+
const ctx = {
|
|
936
|
+
blockInfo,
|
|
937
|
+
weeklyInfo,
|
|
938
|
+
envInfo,
|
|
939
|
+
trendInfo,
|
|
940
|
+
compact
|
|
941
|
+
};
|
|
696
942
|
const segments = [];
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
if (blockSegment) segments.push(blockSegment);
|
|
705
|
-
const weeklySegment = this.renderWeekly(weeklyInfo);
|
|
706
|
-
if (weeklySegment) segments.push(weeklySegment);
|
|
943
|
+
const order = this.config.segmentOrder ?? ["directory", "git", "model", "block", "weekly"];
|
|
944
|
+
for (const name of order) {
|
|
945
|
+
const segment = this.getSegment(name, ctx);
|
|
946
|
+
if (segment) {
|
|
947
|
+
segments.push(segment);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
707
950
|
if (segments.length === 0) {
|
|
708
951
|
return "";
|
|
709
952
|
}
|
|
@@ -869,8 +1112,10 @@ async function main() {
|
|
|
869
1112
|
]);
|
|
870
1113
|
debug("Block info:", JSON.stringify(blockInfo));
|
|
871
1114
|
debug("Weekly info:", JSON.stringify(weeklyInfo));
|
|
1115
|
+
const trendInfo = config.showTrend ? getUsageTrend() : null;
|
|
1116
|
+
debug("Trend info:", JSON.stringify(trendInfo));
|
|
872
1117
|
const renderer = new Renderer(config);
|
|
873
|
-
const output = renderer.render(blockInfo, weeklyInfo, envInfo);
|
|
1118
|
+
const output = renderer.render(blockInfo, weeklyInfo, envInfo, trendInfo);
|
|
874
1119
|
if (output) {
|
|
875
1120
|
process.stdout.write(output);
|
|
876
1121
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-limitline",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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": {
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"start": "node dist/index.js",
|
|
14
14
|
"typecheck": "tsc --noEmit",
|
|
15
15
|
"lint": "eslint src/",
|
|
16
|
-
"test": "
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"test:coverage": "vitest run --coverage"
|
|
17
19
|
},
|
|
18
20
|
"repository": {
|
|
19
21
|
"type": "git",
|
|
@@ -37,8 +39,10 @@
|
|
|
37
39
|
"homepage": "https://github.com/tylergraydev/claude-limitline#readme",
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@types/node": "^20.10.0",
|
|
42
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
40
43
|
"tsup": "^8.0.0",
|
|
41
|
-
"typescript": "^5.3.0"
|
|
44
|
+
"typescript": "^5.3.0",
|
|
45
|
+
"vitest": "^4.0.16"
|
|
42
46
|
},
|
|
43
47
|
"engines": {
|
|
44
48
|
"node": ">=18.0.0"
|