claude-limitline 1.1.0 → 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.
Files changed (3) hide show
  1. package/README.md +11 -3
  2. package/dist/index.js +135 -52
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -111,13 +111,15 @@ echo '{"model":{"id":"claude-opus-4-5-20251101"}}' | npx claude-limitline
111
111
 
112
112
  ## Configuration
113
113
 
114
- Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitline.json`) or current working directory:
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:
115
115
 
116
116
  ```json
117
117
  {
118
118
  "display": {
119
119
  "style": "powerline",
120
- "useNerdFonts": true
120
+ "useNerdFonts": true,
121
+ "compactMode": "auto",
122
+ "compactWidth": 80
121
123
  },
122
124
  "directory": {
123
125
  "enabled": true
@@ -144,7 +146,9 @@ Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitl
144
146
  "pollInterval": 15,
145
147
  "warningThreshold": 80
146
148
  },
147
- "theme": "dark"
149
+ "theme": "dark",
150
+ "segmentOrder": ["directory", "git", "model", "block", "weekly"],
151
+ "showTrend": true
148
152
  }
149
153
  ```
150
154
 
@@ -153,6 +157,8 @@ Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitl
153
157
  | Option | Description | Default |
154
158
  |--------|-------------|---------|
155
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` |
156
162
  | `directory.enabled` | Show repository/directory name | `true` |
157
163
  | `git.enabled` | Show git branch with dirty indicator | `true` |
158
164
  | `model.enabled` | Show Claude model name | `true` |
@@ -167,6 +173,8 @@ Create a `.claude-limitline.json` file in your home directory (`~/.claude-limitl
167
173
  | `budget.pollInterval` | Minutes between API calls | `15` |
168
174
  | `budget.warningThreshold` | Percentage to trigger warning color | `80` |
169
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` |
170
178
 
171
179
  ### Available Themes
172
180
 
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
@@ -45,7 +47,9 @@ var DEFAULT_CONFIG = {
45
47
  pollInterval: 15,
46
48
  warningThreshold: 80
47
49
  },
48
- theme: "dark"
50
+ theme: "dark",
51
+ segmentOrder: ["directory", "git", "model", "block", "weekly"],
52
+ showTrend: true
49
53
  };
50
54
 
51
55
  // src/config/loader.ts
@@ -65,8 +69,7 @@ function deepMerge(target, source) {
65
69
  function loadConfig() {
66
70
  const configPaths = [
67
71
  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")
72
+ path.join(os.homedir(), ".claude", "claude-limitline.json")
70
73
  ];
71
74
  for (const configPath of configPaths) {
72
75
  try {
@@ -264,8 +267,31 @@ async function fetchUsageFromAPI(token) {
264
267
  }
265
268
  }
266
269
  var cachedUsage = null;
270
+ var previousUsage = null;
267
271
  var cacheTimestamp = 0;
268
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
+ }
269
295
  async function getRealtimeUsage(pollIntervalMinutes = 15) {
270
296
  const now = Date.now();
271
297
  const cacheAgeMs = now - cacheTimestamp;
@@ -283,6 +309,7 @@ async function getRealtimeUsage(pollIntervalMinutes = 15) {
283
309
  }
284
310
  const usage = await fetchUsageFromAPI(cachedToken);
285
311
  if (usage) {
312
+ previousUsage = cachedUsage;
286
313
  cachedUsage = usage;
287
314
  cacheTimestamp = now;
288
315
  debug("Refreshed realtime usage cache");
@@ -423,10 +450,12 @@ var SYMBOLS = {
423
450
  left: "\uE0B2",
424
451
  branch: "\uE0A0",
425
452
  separator: "\uE0B1",
426
- block_cost: "\uF252",
427
- // Hourglass
428
- weekly_cost: "\uF073",
429
- // Calendar
453
+ model: "\u2731",
454
+ // Heavy asterisk ✱
455
+ block_cost: "\u25EB",
456
+ // White square with vertical bisecting line ◫
457
+ weekly_cost: "\u25CB",
458
+ // Circle ○
430
459
  progress_full: "\u2588",
431
460
  // Full block
432
461
  progress_empty: "\u2591"
@@ -437,6 +466,7 @@ var TEXT_SYMBOLS = {
437
466
  left: "<",
438
467
  branch: "",
439
468
  separator: "|",
469
+ model: "*",
440
470
  block_cost: "BLK",
441
471
  weekly_cost: "WK",
442
472
  progress_full: "#",
@@ -532,6 +562,11 @@ function getTheme(name) {
532
562
  return themes[name] || themes.dark;
533
563
  }
534
564
 
565
+ // src/utils/terminal.ts
566
+ function getTerminalWidth() {
567
+ return process.stdout.columns || 80;
568
+ }
569
+
535
570
  // src/renderer.ts
536
571
  var Renderer = class {
537
572
  config;
@@ -550,25 +585,43 @@ var Renderer = class {
550
585
  rightArrow: symbolSet.right,
551
586
  separator: symbolSet.separator,
552
587
  branch: symbolSet.branch,
553
- model: "\uF0E7",
554
- // Lightning bolt for model
588
+ model: symbolSet.model,
555
589
  progressFull: symbolSet.progress_full,
556
- progressEmpty: symbolSet.progress_empty
590
+ progressEmpty: symbolSet.progress_empty,
591
+ trendUp: "\u2191",
592
+ trendDown: "\u2193"
557
593
  };
558
594
  }
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
+ }
559
603
  formatProgressBar(percent, width) {
560
604
  const filled = Math.round(percent / 100 * width);
561
605
  const empty = width - filled;
562
606
  return this.symbols.progressFull.repeat(filled) + this.symbols.progressEmpty.repeat(empty);
563
607
  }
564
- formatTimeRemaining(minutes) {
608
+ formatTimeRemaining(minutes, compact) {
565
609
  if (minutes >= 60) {
566
610
  const hours = Math.floor(minutes / 60);
567
611
  const mins = minutes % 60;
612
+ if (compact) {
613
+ return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
614
+ }
568
615
  return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
569
616
  }
570
617
  return `${minutes}m`;
571
618
  }
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
+ }
572
625
  getColorsForPercent(percent, baseColors) {
573
626
  const threshold = this.config.budget?.warningThreshold ?? 80;
574
627
  if (percent >= 100) {
@@ -598,63 +651,69 @@ var Renderer = class {
598
651
  renderFallback(segments) {
599
652
  return segments.map((seg) => ansi.bg(seg.colors.bg) + ansi.fg(seg.colors.fg) + seg.text + RESET_CODE).join(` ${this.symbols.separator} `);
600
653
  }
601
- renderDirectory(envInfo) {
602
- if (!this.config.directory?.enabled || !envInfo.directory) {
654
+ renderDirectory(ctx) {
655
+ if (!this.config.directory?.enabled || !ctx.envInfo.directory) {
603
656
  return null;
604
657
  }
658
+ const name = ctx.compact && ctx.envInfo.directory.length > 12 ? ctx.envInfo.directory.slice(0, 10) + "\u2026" : ctx.envInfo.directory;
605
659
  return {
606
- text: ` ${envInfo.directory} `,
660
+ text: ` ${name} `,
607
661
  colors: this.theme.directory
608
662
  };
609
663
  }
610
- renderGit(envInfo) {
611
- if (!this.config.git?.enabled || !envInfo.gitBranch) {
664
+ renderGit(ctx) {
665
+ if (!this.config.git?.enabled || !ctx.envInfo.gitBranch) {
612
666
  return null;
613
667
  }
614
- const dirtyIndicator = envInfo.gitDirty ? " \u25CF" : "";
668
+ const dirtyIndicator = ctx.envInfo.gitDirty ? " \u25CF" : "";
615
669
  const icon = this.usePowerline ? this.symbols.branch : "";
616
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
+ }
617
675
  return {
618
- text: ` ${prefix}${envInfo.gitBranch}${dirtyIndicator} `,
676
+ text: ` ${prefix}${branch}${dirtyIndicator} `,
619
677
  colors: this.theme.git
620
678
  };
621
679
  }
622
- renderModel(envInfo) {
623
- if (!this.config.model?.enabled || !envInfo.model) {
680
+ renderModel(ctx) {
681
+ if (!this.config.model?.enabled || !ctx.envInfo.model) {
624
682
  return null;
625
683
  }
626
684
  const icon = this.usePowerline ? this.symbols.model : "";
627
685
  const prefix = icon ? `${icon} ` : "";
628
686
  return {
629
- text: ` ${prefix}${envInfo.model} `,
687
+ text: ` ${prefix}${ctx.envInfo.model} `,
630
688
  colors: this.theme.model
631
689
  };
632
690
  }
633
- renderBlock(blockInfo) {
634
- if (!blockInfo || !this.config.block?.enabled) {
691
+ renderBlock(ctx) {
692
+ if (!ctx.blockInfo || !this.config.block?.enabled) {
635
693
  return null;
636
694
  }
637
695
  const icon = this.usePowerline ? this.symbols.block : "BLK";
638
- if (blockInfo.percentUsed === null) {
696
+ if (ctx.blockInfo.percentUsed === null) {
639
697
  return {
640
698
  text: ` ${icon} -- `,
641
699
  colors: this.theme.block
642
700
  };
643
701
  }
644
- const percent = blockInfo.percentUsed;
702
+ const percent = ctx.blockInfo.percentUsed;
645
703
  const colors = this.getColorsForPercent(percent, this.theme.block);
646
704
  const displayStyle = this.config.block.displayStyle || "text";
647
705
  const barWidth = this.config.block.barWidth || 10;
648
706
  const showTime = this.config.block.showTimeRemaining ?? true;
707
+ const trend = this.getTrendSymbol(ctx.trendInfo?.fiveHourTrend ?? null);
649
708
  let text;
650
- if (displayStyle === "bar") {
709
+ if (displayStyle === "bar" && !ctx.compact) {
651
710
  const bar = this.formatProgressBar(percent, barWidth);
652
- text = `${bar} ${Math.round(percent)}%`;
711
+ text = `${bar} ${Math.round(percent)}%${trend}`;
653
712
  } else {
654
- text = `${Math.round(percent)}%`;
713
+ text = `${Math.round(percent)}%${trend}`;
655
714
  }
656
- if (showTime && blockInfo.timeRemaining !== null) {
657
- const timeStr = this.formatTimeRemaining(blockInfo.timeRemaining);
715
+ if (showTime && ctx.blockInfo.timeRemaining !== null && !ctx.compact) {
716
+ const timeStr = this.formatTimeRemaining(ctx.blockInfo.timeRemaining, ctx.compact);
658
717
  text += ` (${timeStr})`;
659
718
  }
660
719
  return {
@@ -662,48 +721,70 @@ var Renderer = class {
662
721
  colors
663
722
  };
664
723
  }
665
- renderWeekly(weeklyInfo) {
666
- if (!weeklyInfo || !this.config.weekly?.enabled) {
724
+ renderWeekly(ctx) {
725
+ if (!ctx.weeklyInfo || !this.config.weekly?.enabled) {
667
726
  return null;
668
727
  }
669
728
  const icon = this.usePowerline ? this.symbols.weekly : "WK";
670
- if (weeklyInfo.percentUsed === null) {
729
+ if (ctx.weeklyInfo.percentUsed === null) {
671
730
  return {
672
731
  text: ` ${icon} -- `,
673
732
  colors: this.theme.weekly
674
733
  };
675
734
  }
676
- const percent = weeklyInfo.percentUsed;
735
+ const percent = ctx.weeklyInfo.percentUsed;
677
736
  const displayStyle = this.config.weekly.displayStyle || "text";
678
737
  const barWidth = this.config.weekly.barWidth || 10;
679
738
  const showWeekProgress = this.config.weekly.showWeekProgress ?? true;
739
+ const trend = this.getTrendSymbol(ctx.trendInfo?.sevenDayTrend ?? null);
680
740
  let text;
681
- if (displayStyle === "bar") {
741
+ if (displayStyle === "bar" && !ctx.compact) {
682
742
  const bar = this.formatProgressBar(percent, barWidth);
683
- text = `${bar} ${Math.round(percent)}%`;
743
+ text = `${bar} ${Math.round(percent)}%${trend}`;
684
744
  } else {
685
- text = `${Math.round(percent)}%`;
745
+ text = `${Math.round(percent)}%${trend}`;
686
746
  }
687
- if (showWeekProgress) {
688
- text += ` (wk ${weeklyInfo.weekProgressPercent}%)`;
747
+ if (showWeekProgress && !ctx.compact) {
748
+ text += ` (wk ${ctx.weeklyInfo.weekProgressPercent}%)`;
689
749
  }
690
750
  return {
691
751
  text: ` ${icon} ${text} `,
692
752
  colors: this.theme.weekly
693
753
  };
694
754
  }
695
- render(blockInfo, weeklyInfo, envInfo) {
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;
769
+ }
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
+ };
696
780
  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);
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
+ }
787
+ }
707
788
  if (segments.length === 0) {
708
789
  return "";
709
790
  }
@@ -869,8 +950,10 @@ async function main() {
869
950
  ]);
870
951
  debug("Block info:", JSON.stringify(blockInfo));
871
952
  debug("Weekly info:", JSON.stringify(weeklyInfo));
953
+ const trendInfo = config.showTrend ? getUsageTrend() : null;
954
+ debug("Trend info:", JSON.stringify(trendInfo));
872
955
  const renderer = new Renderer(config);
873
- const output = renderer.render(blockInfo, weeklyInfo, envInfo);
956
+ const output = renderer.render(blockInfo, weeklyInfo, envInfo, trendInfo);
874
957
  if (output) {
875
958
  process.stdout.write(output);
876
959
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-limitline",
3
- "version": "1.1.0",
3
+ "version": "1.2.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": {