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.
Files changed (3) hide show
  1. package/README.md +50 -72
  2. package/dist/index.js +305 -60
  3. 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
- ```bash
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
- The easiest way to use claude-limitline is to add it directly to your Claude Code settings.
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
- ### Full Settings Example
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 `.claude-limitline.json` file in your home directory (`~/.claude-limitline.json`) or current working directory:
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
- ### Build
218
-
219
- ```bash
220
- npm run build
221
- ```
191
+ ## Testing
222
192
 
223
- ### Development Mode (watch)
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 run dev
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
- ### Run Locally
230
-
231
- ```bash
232
- node dist/index.js
233
-
234
- # With simulated hook data
235
- echo '{"model":{"id":"claude-opus-4-5-20251101"}}' | node dist/index.js
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
- block_cost: "\uF252",
427
- // Hourglass
428
- weekly_cost: "\uF073",
429
- // Calendar
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: "\uF0E7",
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(envInfo) {
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: ` ${envInfo.directory} `,
716
+ text: ` ${name} `,
607
717
  colors: this.theme.directory
608
718
  };
609
719
  }
610
- renderGit(envInfo) {
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}${envInfo.gitBranch}${dirtyIndicator} `,
732
+ text: ` ${prefix}${branch}${dirtyIndicator} `,
619
733
  colors: this.theme.git
620
734
  };
621
735
  }
622
- renderModel(envInfo) {
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(blockInfo) {
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
- renderWeekly(weeklyInfo) {
666
- if (!weeklyInfo || !this.config.weekly?.enabled) {
667
- return null;
668
- }
780
+ renderWeeklySimple(ctx) {
781
+ const info = ctx.weeklyInfo;
669
782
  const icon = this.usePowerline ? this.symbols.weekly : "WK";
670
- if (weeklyInfo.percentUsed === null) {
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 = weeklyInfo.percentUsed;
677
- const displayStyle = this.config.weekly.displayStyle || "text";
678
- const barWidth = this.config.weekly.barWidth || 10;
679
- const showWeekProgress = this.config.weekly.showWeekProgress ?? true;
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 ${weeklyInfo.weekProgressPercent}%)`;
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
- render(blockInfo, weeklyInfo, envInfo) {
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 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);
703
- const blockSegment = this.renderBlock(blockInfo);
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.1.0",
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": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
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"