claude-limit-statusline 0.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/LICENSE +21 -0
- package/README.md +157 -0
- package/bin/cli.js +364 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ann0nip
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# claude-limit-statusline
|
|
2
|
+
|
|
3
|
+
A [Claude Code](https://code.claude.com/docs) status line that shows your **real
|
|
4
|
+
subscription limits** — the 5‑hour session window and the 7‑day weekly window —
|
|
5
|
+
with a **live reset countdown**.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
🤖 Opus 4.8 (1M context) | 🧠 42k (4%) | ⏳ Session 17% · resets in 0h47m (23:12) | 📅 Week 10% · resets in 2d 21h (Jun 03 19:54)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Unlike tools that estimate the 5‑hour block from local logs, this reads the
|
|
12
|
+
**official `rate_limits` payload** that Claude Code provides on stdin — the same
|
|
13
|
+
numbers you see when you run `/usage`. No guessing, no rounding to the hour.
|
|
14
|
+
|
|
15
|
+
## Who is this for?
|
|
16
|
+
|
|
17
|
+
This shows the **subscription rate limits** that Anthropic exposes only to
|
|
18
|
+
**Claude.ai Pro/Max** users. If you use the **pay‑as‑you‑go API**, the
|
|
19
|
+
`rate_limits` field is not present — you probably want a cost tracker like
|
|
20
|
+
[`ccusage`](https://github.com/ryoppippi/ccusage) instead.
|
|
21
|
+
|
|
22
|
+
| | Pro/Max subscription | Pay‑as‑you‑go API |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `rate_limits` in status line | ✅ yes | ❌ no |
|
|
25
|
+
| What this tool shows | 5h + 7d limit % and reset | — |
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g claude-limit-statusline
|
|
31
|
+
cc-limits --install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That's it. `--install` writes the `statusLine` entry into `~/.claude/settings.json`
|
|
35
|
+
for you (merging, never clobbering your other settings). Then open a **new**
|
|
36
|
+
Claude Code session and send one message — `rate_limits` populates after the
|
|
37
|
+
first API response.
|
|
38
|
+
|
|
39
|
+
Want only the two limits? Pass your display options straight through:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cc-limits --install --segments=session,week
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
To remove it again:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cc-limits --uninstall
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> **Why `--install` instead of editing by hand?** It records an **absolute**
|
|
52
|
+
> `node` + script path, so it works even under nvm/Volta where a globally
|
|
53
|
+
> installed command isn't on the `PATH` of the non‑login shell Claude Code uses
|
|
54
|
+
> for the status line.
|
|
55
|
+
|
|
56
|
+
<details>
|
|
57
|
+
<summary>Manual setup (if you prefer)</summary>
|
|
58
|
+
|
|
59
|
+
Add this to `~/.claude/settings.json`:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"statusLine": {
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "cc-limits"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If the bar stays blank (nvm/Volta `PATH` issue), use absolute paths instead —
|
|
71
|
+
`"command": "/path/to/node /path/to/cli.js"` (find them with `which node` and
|
|
72
|
+
`npm root -g`), which is exactly what `cc-limits --install` does automatically.
|
|
73
|
+
|
|
74
|
+
</details>
|
|
75
|
+
|
|
76
|
+
## Output
|
|
77
|
+
|
|
78
|
+
| Segment | Meaning | Source |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| `🤖 model` | Active model | local |
|
|
81
|
+
| `🧠 42k (4%)` | Tokens in the current context window | local |
|
|
82
|
+
| `⏳ Session 17% · resets in 0h47m (23:12)` | **Real** 5‑hour limit used + reset | server |
|
|
83
|
+
| `📅 Week 10% · resets in 2d 21h (Jun 03 19:54)` | **Real** 7‑day limit used + reset | server |
|
|
84
|
+
|
|
85
|
+
The percentage **is** your "how close am I to the limit" gauge. Subscription
|
|
86
|
+
limits are dynamic, so Anthropic does not expose a fixed token cap — only a
|
|
87
|
+
percentage, which is exactly what this surfaces.
|
|
88
|
+
|
|
89
|
+
Before the first API response (and right after `/compact`) the session segment
|
|
90
|
+
shows `⏳ Session --` until fresh data arrives.
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
Pick **which segments** to show (and their order). The four segments are
|
|
95
|
+
`model`, `context`, `session`, `week`.
|
|
96
|
+
|
|
97
|
+
```jsonc
|
|
98
|
+
// Only the two limits, nothing else:
|
|
99
|
+
"command": "cc-limits --segments=session,week"
|
|
100
|
+
|
|
101
|
+
// Everything except the context tokens:
|
|
102
|
+
"command": "cc-limits --no-context"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Pick **which reset countdowns** to show:
|
|
106
|
+
|
|
107
|
+
```jsonc
|
|
108
|
+
"command": "cc-limits --reset=session" // session reset only
|
|
109
|
+
"command": "cc-limits --no-reset" // just percentages, no countdowns
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Flags
|
|
113
|
+
|
|
114
|
+
| Flag | Description |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `--segments=a,b,c` | Allowlist + order. Subset of `model,context,session,week` |
|
|
117
|
+
| `--no-<segment>` | Hide one segment (e.g. `--no-context`). Repeatable |
|
|
118
|
+
| `--reset=both\|session\|week\|none` | Which reset countdowns to show (default `both`) |
|
|
119
|
+
| `--no-reset` | Shorthand for `--reset=none` |
|
|
120
|
+
| `--no-color` | Disable ANSI colors |
|
|
121
|
+
| `--demo` | Print a sample line (no stdin needed) |
|
|
122
|
+
| `-h`, `--help` | Show help |
|
|
123
|
+
|
|
124
|
+
### Environment variables
|
|
125
|
+
|
|
126
|
+
Equivalent to the flags, handy if you don't want to edit the command string:
|
|
127
|
+
|
|
128
|
+
| Env var | Default | Description |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| `CC_LIMITS_SEGMENTS` | `model,context,session,week` | Segments + order |
|
|
131
|
+
| `CC_LIMITS_RESET` | `both` | `both` / `session` / `week` / `none` |
|
|
132
|
+
| `CC_LIMITS_WARN` | `70` | % at/above which a limit turns yellow |
|
|
133
|
+
| `CC_LIMITS_CRIT` | `90` | % at/above which a limit turns red |
|
|
134
|
+
| `CC_LIMITS_SEP` | `" \| "` | Separator between segments |
|
|
135
|
+
| `NO_COLOR` | — | Set to disable ANSI colors |
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
cc-limits --demo
|
|
139
|
+
cc-limits --segments=session,week --reset=session --demo
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## How it works
|
|
143
|
+
|
|
144
|
+
Claude Code runs your status-line command on every update and pipes a JSON
|
|
145
|
+
[status-line payload](https://code.claude.com/docs/en/statusline) to stdin. This
|
|
146
|
+
program parses it and reads:
|
|
147
|
+
|
|
148
|
+
- `rate_limits.five_hour.used_percentage` / `.resets_at`
|
|
149
|
+
- `rate_limits.seven_day.used_percentage` / `.resets_at`
|
|
150
|
+
- `context_window.*` for the token/context segment
|
|
151
|
+
|
|
152
|
+
`resets_at` is Unix epoch seconds; the countdown is computed against the current
|
|
153
|
+
time. Everything runs with **zero dependencies** for fast startup.
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* claude-limit-statusline
|
|
6
|
+
*
|
|
7
|
+
* A Claude Code status line that shows your REAL subscription rate limits
|
|
8
|
+
* (5-hour session + 7-day weekly) with a live reset countdown.
|
|
9
|
+
*
|
|
10
|
+
* Claude Code pipes a JSON payload on stdin. For Claude.ai Pro/Max
|
|
11
|
+
* subscribers it contains a `rate_limits` object sourced from Anthropic's
|
|
12
|
+
* servers — the same numbers you see in `/usage`. This reads those fields
|
|
13
|
+
* and prints a single status line. It does NOT estimate locally.
|
|
14
|
+
*
|
|
15
|
+
* Docs: https://code.claude.com/docs/en/statusline
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const os = require("os");
|
|
21
|
+
|
|
22
|
+
const argv = process.argv.slice(2);
|
|
23
|
+
|
|
24
|
+
// ---------- arg / env helpers ----------
|
|
25
|
+
function getFlagValue(key) {
|
|
26
|
+
for (let i = 0; i < argv.length; i++) {
|
|
27
|
+
const a = argv[i];
|
|
28
|
+
if (a === key) {
|
|
29
|
+
const next = argv[i + 1];
|
|
30
|
+
return next && !next.startsWith("--") ? next : "";
|
|
31
|
+
}
|
|
32
|
+
if (a.startsWith(key + "=")) return a.slice(key.length + 1);
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
function parseList(val) {
|
|
37
|
+
return String(val)
|
|
38
|
+
.split(",")
|
|
39
|
+
.map((s) => s.trim().toLowerCase())
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
function numEnv(name, def) {
|
|
43
|
+
const v = Number(process.env[name]);
|
|
44
|
+
return Number.isFinite(v) ? v : def;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------- config ----------
|
|
48
|
+
const ALL_SEGMENTS = ["model", "context", "session", "week"];
|
|
49
|
+
|
|
50
|
+
// Which segments to show. --segments / CC_LIMITS_SEGMENTS = allowlist (and order).
|
|
51
|
+
// Otherwise the full set minus any --no-<segment> flags.
|
|
52
|
+
let SEGMENTS;
|
|
53
|
+
const segSel = getFlagValue("--segments") || process.env.CC_LIMITS_SEGMENTS;
|
|
54
|
+
if (segSel != null && segSel !== "") {
|
|
55
|
+
SEGMENTS = parseList(segSel).filter((s) => ALL_SEGMENTS.includes(s));
|
|
56
|
+
} else {
|
|
57
|
+
SEGMENTS = ALL_SEGMENTS.filter((s) => !argv.includes(`--no-${s}`));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Which reset countdowns to show: both | session | week | none.
|
|
61
|
+
let RESET_MODE = (
|
|
62
|
+
getFlagValue("--reset") ||
|
|
63
|
+
process.env.CC_LIMITS_RESET ||
|
|
64
|
+
"both"
|
|
65
|
+
).toLowerCase();
|
|
66
|
+
if (argv.includes("--no-reset")) RESET_MODE = "none";
|
|
67
|
+
function showReset(which) {
|
|
68
|
+
return RESET_MODE === "both" || RESET_MODE === which;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const WARN_PCT = numEnv("CC_LIMITS_WARN", 70);
|
|
72
|
+
const CRIT_PCT = numEnv("CC_LIMITS_CRIT", 90);
|
|
73
|
+
const SEP = process.env.CC_LIMITS_SEP || " | ";
|
|
74
|
+
const NO_COLOR =
|
|
75
|
+
argv.includes("--no-color") ||
|
|
76
|
+
(process.env.NO_COLOR != null && process.env.NO_COLOR !== "");
|
|
77
|
+
|
|
78
|
+
// ---------- colors ----------
|
|
79
|
+
const C = {
|
|
80
|
+
reset: "\x1b[0m",
|
|
81
|
+
dim: "\x1b[2m",
|
|
82
|
+
red: "\x1b[31m",
|
|
83
|
+
green: "\x1b[32m",
|
|
84
|
+
yellow: "\x1b[33m",
|
|
85
|
+
cyan: "\x1b[36m",
|
|
86
|
+
gray: "\x1b[90m",
|
|
87
|
+
};
|
|
88
|
+
function paint(s, color) {
|
|
89
|
+
if (NO_COLOR || !color) return s;
|
|
90
|
+
return color + s + C.reset;
|
|
91
|
+
}
|
|
92
|
+
function pctColor(p) {
|
|
93
|
+
if (p >= CRIT_PCT) return C.red;
|
|
94
|
+
if (p >= WARN_PCT) return C.yellow;
|
|
95
|
+
return C.green;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------- formatters ----------
|
|
99
|
+
const MON = [
|
|
100
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
101
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
102
|
+
];
|
|
103
|
+
function pad(n) {
|
|
104
|
+
return String(n).padStart(2, "0");
|
|
105
|
+
}
|
|
106
|
+
// Strip control chars / ANSI escapes from untrusted string fields before
|
|
107
|
+
// printing them to the terminal.
|
|
108
|
+
function clean(s) {
|
|
109
|
+
return String(s).replace(/[\x00-\x1f\x7f]/g, "");
|
|
110
|
+
}
|
|
111
|
+
function round(n) {
|
|
112
|
+
return Math.round(Number(n));
|
|
113
|
+
}
|
|
114
|
+
function humanTokens(n) {
|
|
115
|
+
n = Number(n) || 0;
|
|
116
|
+
if (n >= 1000) return Math.round(n / 1000) + "k";
|
|
117
|
+
return String(n);
|
|
118
|
+
}
|
|
119
|
+
function fmtCountdown(epochSec) {
|
|
120
|
+
let diff = Math.floor(epochSec - Date.now() / 1000);
|
|
121
|
+
if (diff < 0) diff = 0;
|
|
122
|
+
const d = Math.floor(diff / 86400);
|
|
123
|
+
const h = Math.floor((diff % 86400) / 3600);
|
|
124
|
+
const m = Math.floor((diff % 3600) / 60);
|
|
125
|
+
if (d > 0) return `${d}d ${h}h`;
|
|
126
|
+
return `${h}h${pad(m)}m`;
|
|
127
|
+
}
|
|
128
|
+
function fmtClock(epochSec, withDate) {
|
|
129
|
+
const d = new Date(epochSec * 1000);
|
|
130
|
+
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
131
|
+
if (withDate) return `${MON[d.getMonth()]} ${pad(d.getDate())} ${time}`;
|
|
132
|
+
return time;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------- segment renderers ----------
|
|
136
|
+
function renderModel(data) {
|
|
137
|
+
const model = clean(data?.model?.display_name || "Claude");
|
|
138
|
+
return paint("🤖 " + model, C.cyan);
|
|
139
|
+
}
|
|
140
|
+
function renderContext(data) {
|
|
141
|
+
const cw = data?.context_window || {};
|
|
142
|
+
const tokens =
|
|
143
|
+
(Number(cw.total_input_tokens) || 0) +
|
|
144
|
+
(Number(cw.total_output_tokens) || 0);
|
|
145
|
+
let s = "🧠 " + humanTokens(tokens);
|
|
146
|
+
if (cw.used_percentage != null) s += ` (${round(cw.used_percentage)}%)`;
|
|
147
|
+
return paint(s, C.gray);
|
|
148
|
+
}
|
|
149
|
+
function renderLimit(limit, { icon, label, which, withDate }) {
|
|
150
|
+
if (!limit || limit.used_percentage == null) {
|
|
151
|
+
return paint(`${icon} ${label} --`, C.dim);
|
|
152
|
+
}
|
|
153
|
+
const p = round(limit.used_percentage);
|
|
154
|
+
let s = `${icon} ${label} ${paint(p + "%", pctColor(p))}`;
|
|
155
|
+
if (limit.resets_at > 0 && showReset(which)) {
|
|
156
|
+
s += paint(
|
|
157
|
+
` · resets in ${fmtCountdown(limit.resets_at)} (${fmtClock(
|
|
158
|
+
limit.resets_at,
|
|
159
|
+
withDate
|
|
160
|
+
)})`,
|
|
161
|
+
C.dim
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return s;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function render(data) {
|
|
168
|
+
const rl = data?.rate_limits || {};
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const seg of SEGMENTS) {
|
|
171
|
+
if (seg === "model") out.push(renderModel(data));
|
|
172
|
+
else if (seg === "context") out.push(renderContext(data));
|
|
173
|
+
else if (seg === "session")
|
|
174
|
+
out.push(
|
|
175
|
+
renderLimit(rl.five_hour, {
|
|
176
|
+
icon: "⏳",
|
|
177
|
+
label: "Session",
|
|
178
|
+
which: "session",
|
|
179
|
+
withDate: false,
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
else if (seg === "week")
|
|
183
|
+
out.push(
|
|
184
|
+
renderLimit(rl.seven_day, {
|
|
185
|
+
icon: "📅",
|
|
186
|
+
label: "Week",
|
|
187
|
+
which: "week",
|
|
188
|
+
withDate: true,
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return out.join(SEP);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------- demo payload ----------
|
|
196
|
+
function demoPayload() {
|
|
197
|
+
const now = Math.floor(Date.now() / 1000);
|
|
198
|
+
return {
|
|
199
|
+
model: { display_name: "Opus 4.8 (1M context)" },
|
|
200
|
+
context_window: {
|
|
201
|
+
used_percentage: 4,
|
|
202
|
+
total_input_tokens: 40000,
|
|
203
|
+
total_output_tokens: 2328,
|
|
204
|
+
},
|
|
205
|
+
rate_limits: {
|
|
206
|
+
five_hour: { used_percentage: 17, resets_at: now + 2880 },
|
|
207
|
+
seven_day: { used_percentage: 10, resets_at: now + 250000 },
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------- main ----------
|
|
213
|
+
function out(line) {
|
|
214
|
+
process.stdout.write(line + "\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
218
|
+
out(
|
|
219
|
+
[
|
|
220
|
+
"claude-limit-statusline (cc-limits)",
|
|
221
|
+
"",
|
|
222
|
+
"Reads Claude Code's JSON status-line payload on stdin and prints your",
|
|
223
|
+
"real Pro/Max rate limits (5h session + 7d week) with reset countdowns.",
|
|
224
|
+
"",
|
|
225
|
+
"Setup (writes ~/.claude/settings.json for you):",
|
|
226
|
+
" cc-limits --install configure the status line",
|
|
227
|
+
" cc-limits --install --segments=session,week ...with display options",
|
|
228
|
+
" cc-limits --uninstall remove it again",
|
|
229
|
+
"",
|
|
230
|
+
"Or set it manually in ~/.claude/settings.json:",
|
|
231
|
+
' "statusLine": { "type": "command", "command": "cc-limits" }',
|
|
232
|
+
"",
|
|
233
|
+
"Segments (default: all, in this order): model, context, session, week",
|
|
234
|
+
" --segments=session,week Show only these, in this order",
|
|
235
|
+
" --no-context Hide a single segment (repeatable)",
|
|
236
|
+
" --no-model --no-week ...",
|
|
237
|
+
"",
|
|
238
|
+
"Reset countdowns:",
|
|
239
|
+
" --reset=both|session|week|none Which resets to show (default both)",
|
|
240
|
+
" --no-reset Same as --reset=none",
|
|
241
|
+
"",
|
|
242
|
+
"Other flags: --demo, --no-color, -h/--help",
|
|
243
|
+
"",
|
|
244
|
+
"Env vars:",
|
|
245
|
+
" CC_LIMITS_SEGMENTS=model,context,session,week",
|
|
246
|
+
" CC_LIMITS_RESET=both|session|week|none",
|
|
247
|
+
" CC_LIMITS_WARN=70 yellow threshold (% of a limit)",
|
|
248
|
+
" CC_LIMITS_CRIT=90 red threshold",
|
|
249
|
+
" CC_LIMITS_SEP=' | ' segment separator",
|
|
250
|
+
" NO_COLOR disable colors",
|
|
251
|
+
].join("\n")
|
|
252
|
+
);
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------- install / uninstall into ~/.claude/settings.json ----------
|
|
257
|
+
function settingsPath() {
|
|
258
|
+
return path.join(os.homedir(), ".claude", "settings.json");
|
|
259
|
+
}
|
|
260
|
+
function quoteArg(s) {
|
|
261
|
+
return /[\s"\\]/.test(s) ? '"' + s.replace(/(["\\])/g, "\\$1") + '"' : s;
|
|
262
|
+
}
|
|
263
|
+
function buildCommand(passthrough) {
|
|
264
|
+
// Absolute node + script path => immune to the non-login-shell PATH issue
|
|
265
|
+
// (e.g. nvm) that can leave a globally-installed `cc-limits` off the PATH.
|
|
266
|
+
return [process.execPath, __filename, ...passthrough].map(quoteArg).join(" ");
|
|
267
|
+
}
|
|
268
|
+
function readSettings(p) {
|
|
269
|
+
let raw;
|
|
270
|
+
try {
|
|
271
|
+
raw = fs.readFileSync(p, "utf8");
|
|
272
|
+
} catch (e) {
|
|
273
|
+
if (e.code === "ENOENT") return { settings: {}, existed: false };
|
|
274
|
+
console.error("cc-limits: cannot read " + p + ": " + e.message);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
return { settings: raw.trim() ? JSON.parse(raw) : {}, existed: true };
|
|
279
|
+
} catch {
|
|
280
|
+
console.error(
|
|
281
|
+
"cc-limits: " + p + " is not valid JSON — aborting so it isn't clobbered.\n" +
|
|
282
|
+
"Fix or remove it, then re-run, or configure the status line manually."
|
|
283
|
+
);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function doInstall(passthrough) {
|
|
288
|
+
const p = settingsPath();
|
|
289
|
+
const { settings, existed } = readSettings(p);
|
|
290
|
+
settings.statusLine = { type: "command", command: buildCommand(passthrough) };
|
|
291
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
292
|
+
fs.writeFileSync(p, JSON.stringify(settings, null, 2) + "\n");
|
|
293
|
+
console.log("cc-limits: status line " + (existed ? "updated in " : "written to ") + p);
|
|
294
|
+
console.log("→ " + settings.statusLine.command);
|
|
295
|
+
console.log("Open a NEW Claude Code session and send one message to see it.");
|
|
296
|
+
}
|
|
297
|
+
function doUninstall() {
|
|
298
|
+
const p = settingsPath();
|
|
299
|
+
const { settings, existed } = readSettings(p);
|
|
300
|
+
if (!existed || !settings.statusLine) {
|
|
301
|
+
console.log("cc-limits: no status line configured in " + p + " — nothing to do.");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
delete settings.statusLine;
|
|
305
|
+
fs.writeFileSync(p, JSON.stringify(settings, null, 2) + "\n");
|
|
306
|
+
console.log("cc-limits: removed status line from " + p);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (argv.includes("--install")) {
|
|
310
|
+
const passthrough = argv.filter((a) => a !== "--install" && a !== "--uninstall");
|
|
311
|
+
doInstall(passthrough);
|
|
312
|
+
process.exit(0);
|
|
313
|
+
}
|
|
314
|
+
if (argv.includes("--uninstall")) {
|
|
315
|
+
doUninstall();
|
|
316
|
+
process.exit(0);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (argv.includes("--demo")) {
|
|
320
|
+
out(render(demoPayload()));
|
|
321
|
+
process.exit(0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (process.stdin.isTTY) {
|
|
325
|
+
out(
|
|
326
|
+
render(demoPayload()) +
|
|
327
|
+
" " +
|
|
328
|
+
paint("(demo — pipe Claude Code JSON in)", C.dim)
|
|
329
|
+
);
|
|
330
|
+
process.exit(0);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const MAX_INPUT = 1 << 20; // 1 MB safety cap on stdin
|
|
334
|
+
let input = "";
|
|
335
|
+
let done = false;
|
|
336
|
+
|
|
337
|
+
function finish(raw) {
|
|
338
|
+
if (done) return;
|
|
339
|
+
done = true;
|
|
340
|
+
let data = {};
|
|
341
|
+
try {
|
|
342
|
+
data = raw ? JSON.parse(raw) : {};
|
|
343
|
+
} catch {
|
|
344
|
+
data = {};
|
|
345
|
+
}
|
|
346
|
+
out(render(data));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Never hang: if no EOF arrives, render with whatever we have after 2s.
|
|
350
|
+
const timer = setTimeout(() => finish(input), 2000);
|
|
351
|
+
if (timer.unref) timer.unref();
|
|
352
|
+
|
|
353
|
+
process.stdin.setEncoding("utf8");
|
|
354
|
+
process.stdin.on("data", (chunk) => {
|
|
355
|
+
if (input.length < MAX_INPUT) input += chunk;
|
|
356
|
+
});
|
|
357
|
+
process.stdin.on("error", () => {
|
|
358
|
+
clearTimeout(timer);
|
|
359
|
+
finish("");
|
|
360
|
+
});
|
|
361
|
+
process.stdin.on("end", () => {
|
|
362
|
+
clearTimeout(timer);
|
|
363
|
+
finish(input);
|
|
364
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-limit-statusline",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Claude Code status line that shows your REAL Pro/Max subscription limits — 5-hour session and 7-day weekly usage with a live reset countdown. Uses the official rate_limits payload, not a local estimate.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc-limits": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"demo": "node bin/cli.js --demo"
|
|
16
|
+
},
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"claude",
|
|
22
|
+
"claude-code",
|
|
23
|
+
"statusline",
|
|
24
|
+
"status-line",
|
|
25
|
+
"rate-limits",
|
|
26
|
+
"usage",
|
|
27
|
+
"pro",
|
|
28
|
+
"max",
|
|
29
|
+
"subscription",
|
|
30
|
+
"anthropic"
|
|
31
|
+
],
|
|
32
|
+
"author": "Ann0nip (https://github.com/ann0nip)",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/ann0nip/claude-limit-statusline.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/ann0nip/claude-limit-statusline#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/ann0nip/claude-limit-statusline/issues"
|
|
41
|
+
}
|
|
42
|
+
}
|