ctxline-claude 0.0.5 → 0.0.7
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 +14 -13
- package/package.json +4 -4
- package/statusline.js +90 -61
package/README.md
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
<img src="https://img.shields.io/npm/dm/ctxline-claude" alt="npm downloads">
|
|
13
13
|
</a>
|
|
14
14
|
<a href="LICENSE">
|
|
15
|
-
<img src="https://img.shields.io/github/license/MithunWijayasiri/
|
|
15
|
+
<img src="https://img.shields.io/github/license/MithunWijayasiri/ctxline-claude" alt="license">
|
|
16
16
|
</a>
|
|
17
|
-
<a href="https://github.com/MithunWijayasiri/
|
|
18
|
-
<img src="https://img.shields.io/github/stars/MithunWijayasiri/
|
|
17
|
+
<a href="https://github.com/MithunWijayasiri/ctxline-claude/stargazers">
|
|
18
|
+
<img src="https://img.shields.io/github/stars/MithunWijayasiri/ctxline-claude" alt="stars">
|
|
19
19
|
</a>
|
|
20
20
|
</p>
|
|
21
21
|
|
|
@@ -43,8 +43,8 @@ Then restart Claude Code or start a new session. That's it.
|
|
|
43
43
|
**Clone & run the installer:**
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
git clone https://github.com/MithunWijayasiri/
|
|
47
|
-
cd
|
|
46
|
+
git clone https://github.com/MithunWijayasiri/ctxline-claude.git
|
|
47
|
+
cd ctxline-claude
|
|
48
48
|
./install.sh # macOS / Linux
|
|
49
49
|
./install.ps1 # Windows (PowerShell)
|
|
50
50
|
```
|
|
@@ -52,7 +52,7 @@ cd claudecode-statusline
|
|
|
52
52
|
**Manual:** download the script, then point `~/.claude/settings.json` at it.
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
curl -o ~/.claude/hooks/statusline.js https://raw.githubusercontent.com/MithunWijayasiri/
|
|
55
|
+
curl -o ~/.claude/hooks/statusline.js https://raw.githubusercontent.com/MithunWijayasiri/ctxline-claude/main/statusline.js
|
|
56
56
|
chmod +x ~/.claude/hooks/statusline.js
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -105,14 +105,15 @@ Remove-Item "$env:USERPROFILE\.claude\cache\usage-cache.json" -ErrorAction Silen
|
|
|
105
105
|
| **Weekly** | Weekly usage allowance + time until the weekly reset (subscription users) |
|
|
106
106
|
| **Task** | The in-progress todo, when there is one |
|
|
107
107
|
|
|
108
|
-
>
|
|
109
|
-
> limits.
|
|
108
|
+
> [!NOTE]
|
|
109
|
+
> Usage bars change color automatically as you approach your limits.
|
|
110
110
|
|
|
111
111
|
## How it works
|
|
112
112
|
|
|
113
|
-
- **Source** — context comes from Claude Code's session data
|
|
114
|
-
- **
|
|
115
|
-
- **
|
|
113
|
+
- **Source** — context comes from Claude Code's session data. Usage bars are read straight from the `rate_limits` field Claude Code pipes in (no network), falling back to `https://api.anthropic.com/api/oauth/usage` (the same `/usage` data — 5-hour and weekly limits) when that field isn't present yet. API-key users skip usage entirely.
|
|
114
|
+
- **No network on the fast path** — when `rate_limits` is in the session data, there's no API call at all. The fetch below only runs as a fallback (e.g. the first render of a session, before the field appears).
|
|
115
|
+
- **Adaptive timing** — for the fallback fetch: 1.5s timeout on the first prompt (cold start), 1.2s after (connection reused).
|
|
116
|
+
- **Caching** — the fallback fetch is cached at `~/.claude/cache/usage-cache.json`, shared across sessions. Within 30s the cache renders directly (the API call is skipped); if a live call fails, the last value (up to 10 min old) is shown so the bar never vanishes. The reset countdown recomputes every render.
|
|
116
117
|
- **Never breaks** — every failure path falls back silently; the statusline always prints.
|
|
117
118
|
|
|
118
119
|
## FAQ
|
|
@@ -120,7 +121,7 @@ Remove-Item "$env:USERPROFILE\.claude\cache\usage-cache.json" -ErrorAction Silen
|
|
|
120
121
|
<details>
|
|
121
122
|
<summary>Does this use the same data as /usage?</summary>
|
|
122
123
|
|
|
123
|
-
Yes.
|
|
124
|
+
Yes — the same 5-hour and weekly limits. It reads them from the session data Claude Code provides when available, and falls back to Anthropic's usage API (the endpoint `/usage` uses) otherwise.
|
|
124
125
|
|
|
125
126
|
</details>
|
|
126
127
|
|
|
@@ -141,7 +142,7 @@ No. All failures are handled silently and the statusline always renders.
|
|
|
141
142
|
<details>
|
|
142
143
|
<summary>Does it expose my API keys / auth tokens?</summary>
|
|
143
144
|
|
|
144
|
-
No. Your credentials never leave your machine.
|
|
145
|
+
No. Your credentials never leave your machine. On the fast path no token is read at all — usage comes straight from the session data. Only on the fallback fetch is the OAuth token read locally (from `~/.claude/.credentials.json` or the macOS keychain), used solely to authenticate the request to Anthropic's own usage API — the same endpoint `/usage` uses. Nothing is sent to any third party, logged, or cached; only the resulting usage percentages are stored locally.
|
|
145
146
|
|
|
146
147
|
</details>
|
|
147
148
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ctxline-claude",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "A customizable statusline for Claude Code that tracks context usage and session limits",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ctxline-claude": "bin/install.js"
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
],
|
|
29
29
|
"author": "Mithun Wijayasiri",
|
|
30
30
|
"license": "MIT",
|
|
31
|
-
"homepage": "https://github.com/MithunWijayasiri/
|
|
31
|
+
"homepage": "https://github.com/MithunWijayasiri/ctxline-claude#readme",
|
|
32
32
|
"bugs": {
|
|
33
|
-
"url": "https://github.com/MithunWijayasiri/
|
|
33
|
+
"url": "https://github.com/MithunWijayasiri/ctxline-claude/issues"
|
|
34
34
|
},
|
|
35
35
|
"repository": {
|
|
36
36
|
"type": "git",
|
|
37
|
-
"url": "https://github.com/MithunWijayasiri/
|
|
37
|
+
"url": "https://github.com/MithunWijayasiri/ctxline-claude.git"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/statusline.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Claude Code Enhanced Statusline
|
|
3
3
|
// Shows: directory | model | context usage | current (5-hour) + weekly usage | current task
|
|
4
4
|
// Auto-detects API key vs subscription usage
|
|
5
|
-
// https://github.com/MithunWijayasiri/
|
|
5
|
+
// https://github.com/MithunWijayasiri/ctxline-claude
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
@@ -109,23 +109,21 @@ function getContextBar(remaining) {
|
|
|
109
109
|
const filled = Math.round((used / 100) * BAR_WIDTH);
|
|
110
110
|
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(BAR_WIDTH - filled);
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
coloredBar = `${colors.orange}${bar} ${used}%${colors.reset}`;
|
|
119
|
-
} else {
|
|
120
|
-
coloredBar = `${colors.blink}${colors.red}${bar} ${used}%${colors.reset}`;
|
|
121
|
-
}
|
|
112
|
+
// Context color: green <50 / yellow <65 / orange <80 / blink-red >=80.
|
|
113
|
+
let color;
|
|
114
|
+
if (used < 50) color = colors.green;
|
|
115
|
+
else if (used < 65) color = colors.yellow;
|
|
116
|
+
else if (used < 80) color = colors.orange;
|
|
117
|
+
else color = colors.blink + colors.red;
|
|
122
118
|
|
|
123
|
-
|
|
119
|
+
// Compact label form: "C<used> <bar>" (e.g. "C45 ███░░░"), colored as a whole.
|
|
120
|
+
return `${color}C${used} ${bar}${colors.reset}`;
|
|
124
121
|
}
|
|
125
122
|
|
|
126
|
-
// Render
|
|
127
|
-
//
|
|
128
|
-
|
|
123
|
+
// Render a compact usage segment from raw data: "<label><pct> ↺ <countdown>"
|
|
124
|
+
// (e.g. "H81 ↺ 2h21m") — no bar. Called on every read (live or cached) so the reset
|
|
125
|
+
// countdown is always recomputed from resetsAt rather than frozen at fetch time.
|
|
126
|
+
function buildUsageBar(label, percentage, resetsAt) {
|
|
129
127
|
let timeStr = '';
|
|
130
128
|
if (resetsAt) {
|
|
131
129
|
const diffMins = Math.max(0, Math.floor((new Date(resetsAt) - new Date()) / 60000));
|
|
@@ -137,21 +135,18 @@ function buildUsageBar(percentage, resetsAt) {
|
|
|
137
135
|
else timeStr = `${mins}m`;
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
const filledWidth = Math.max(0, Math.min(BAR_WIDTH, Math.round((percentage / 100) * BAR_WIDTH)));
|
|
141
|
-
const filled = '█'.repeat(filledWidth);
|
|
142
|
-
const empty = '░'.repeat(BAR_WIDTH - filledWidth);
|
|
143
138
|
const color = getUsageColor(percentage);
|
|
144
|
-
const timePart = timeStr ? `${colors.dim}
|
|
139
|
+
const timePart = timeStr ? `${colors.dim} ↺ ${timeStr}${colors.reset}` : '';
|
|
145
140
|
|
|
146
|
-
return `${color}${
|
|
141
|
+
return `${color}${label}${percentage}${colors.reset}${timePart}`;
|
|
147
142
|
}
|
|
148
143
|
|
|
149
|
-
// Build both usage
|
|
150
|
-
// null/absent. Returns { current, weekly } where each is a rendered
|
|
144
|
+
// Build both usage segments from raw entries. Each entry is { percentage, resetsAt } or
|
|
145
|
+
// null/absent. Returns { current, weekly } where each is a rendered segment string or null.
|
|
151
146
|
function buildUsageBars(fiveHour, weekly) {
|
|
152
147
|
return {
|
|
153
|
-
current: fiveHour ? buildUsageBar(fiveHour.percentage, fiveHour.resetsAt) : null,
|
|
154
|
-
weekly: weekly ? buildUsageBar(weekly.percentage, weekly.resetsAt) : null
|
|
148
|
+
current: fiveHour ? buildUsageBar('H', fiveHour.percentage, fiveHour.resetsAt) : null,
|
|
149
|
+
weekly: weekly ? buildUsageBar('W', weekly.percentage, weekly.resetsAt) : null
|
|
155
150
|
};
|
|
156
151
|
}
|
|
157
152
|
|
|
@@ -163,6 +158,37 @@ function normalizePercentage(value) {
|
|
|
163
158
|
return Math.max(0, Math.min(100, Math.round(value)));
|
|
164
159
|
}
|
|
165
160
|
|
|
161
|
+
// Build usage bars from stdin `rate_limits` (Claude.ai Pro/Max, present only after the
|
|
162
|
+
// first API response of a session). Same data as the OAuth usage API, so reading it here
|
|
163
|
+
// skips the network/credentials/cache path entirely. `resets_at` is a Unix epoch in
|
|
164
|
+
// SECONDS (not ISO) — ×1000 before Date. Returns { current, weekly } bars, or null when
|
|
165
|
+
// rate_limits is absent or the required five_hour segment is unusable (caller falls back).
|
|
166
|
+
function buildUsageFromStdin(data) {
|
|
167
|
+
const rl = data?.rate_limits;
|
|
168
|
+
if (!rl) return null;
|
|
169
|
+
|
|
170
|
+
const toEntry = (seg) => {
|
|
171
|
+
if (!seg) return null;
|
|
172
|
+
const pct = normalizePercentage(seg.used_percentage);
|
|
173
|
+
if (pct == null) return null;
|
|
174
|
+
// resets_at is a Unix epoch in SECONDS. Coerce + validate defensively: a non-numeric
|
|
175
|
+
// or out-of-range value would make new Date(...).toISOString() throw, and this path
|
|
176
|
+
// runs outside outputStatus's try/catch. Fall back to resetsAt: null on anything bad.
|
|
177
|
+
let resetsAt = null;
|
|
178
|
+
const epoch = Number(seg.resets_at);
|
|
179
|
+
if (Number.isFinite(epoch) && epoch > 0) {
|
|
180
|
+
const d = new Date(epoch * 1000);
|
|
181
|
+
if (!Number.isNaN(d.getTime())) resetsAt = d.toISOString();
|
|
182
|
+
}
|
|
183
|
+
return { percentage: pct, resetsAt };
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const fiveHour = toEntry(rl.five_hour);
|
|
187
|
+
if (!fiveHour) return null; // five_hour is the required bar
|
|
188
|
+
const weekly = toEntry(rl.seven_day);
|
|
189
|
+
return buildUsageBars(fiveHour, weekly);
|
|
190
|
+
}
|
|
191
|
+
|
|
166
192
|
// Validate a single usage entry ({ percentage, resetsAt }). Returns true only for a
|
|
167
193
|
// finite 0-100 percentage and a parseable (or absent) resetsAt.
|
|
168
194
|
function isValidUsageEntry(entry) {
|
|
@@ -373,10 +399,10 @@ function outputStatus(data, usage) {
|
|
|
373
399
|
const parts = [];
|
|
374
400
|
parts.push(branch ? `${dirname} ${colors.dim}⎇ ${branch}${colors.reset}` : dirname);
|
|
375
401
|
parts.push(effort ? `${model}${getEffortColor(effort)} · ${effort}${colors.reset}` : model);
|
|
376
|
-
parts.push(
|
|
402
|
+
parts.push(contextBar);
|
|
377
403
|
|
|
378
|
-
if (usage?.current) parts.push(
|
|
379
|
-
if (usage?.weekly) parts.push(
|
|
404
|
+
if (usage?.current) parts.push(usage.current);
|
|
405
|
+
if (usage?.weekly) parts.push(usage.weekly);
|
|
380
406
|
|
|
381
407
|
if (task) parts.push(`${colors.dim}${task}${colors.reset}`);
|
|
382
408
|
process.stdout.write(parts.join(' \u2502 '));
|
|
@@ -387,27 +413,51 @@ function outputStatus(data, usage) {
|
|
|
387
413
|
|
|
388
414
|
function outputFallback(usage) {
|
|
389
415
|
const contextBar = getContextBar(undefined);
|
|
390
|
-
const parts = ['~', 'Claude',
|
|
391
|
-
if (usage?.current) parts.push(
|
|
392
|
-
if (usage?.weekly) parts.push(
|
|
416
|
+
const parts = ['~', 'Claude', contextBar];
|
|
417
|
+
if (usage?.current) parts.push(usage.current);
|
|
418
|
+
if (usage?.weekly) parts.push(usage.weekly);
|
|
393
419
|
process.stdout.write(parts.join(' \u2502 '));
|
|
394
420
|
}
|
|
395
421
|
|
|
396
|
-
//
|
|
397
|
-
|
|
422
|
+
// Resolve usage bars for a (possibly null) parsed stdin payload.
|
|
423
|
+
// Order: API-key users get none; otherwise prefer stdin `rate_limits` (no network),
|
|
424
|
+
// then fall back to the cache+API flow when stdin lacks it (cold start / non-Pro/Max).
|
|
425
|
+
function resolveUsage(data, callback) {
|
|
398
426
|
if (IS_API_KEY) {
|
|
399
|
-
callback(null);
|
|
400
|
-
} else {
|
|
401
|
-
getUsageWithCache(callback);
|
|
427
|
+
return callback(null);
|
|
402
428
|
}
|
|
429
|
+
const fromStdin = buildUsageFromStdin(data);
|
|
430
|
+
if (fromStdin) {
|
|
431
|
+
return callback(fromStdin);
|
|
432
|
+
}
|
|
433
|
+
getUsageWithCache(callback);
|
|
403
434
|
}
|
|
404
435
|
|
|
405
436
|
// Process with timeout
|
|
406
|
-
if
|
|
407
|
-
|
|
408
|
-
|
|
437
|
+
// Parse the accumulated stdin into a payload object, or null if empty/unparseable.
|
|
438
|
+
function parseInput(input) {
|
|
439
|
+
if (!input || input.length === 0) return null;
|
|
440
|
+
try {
|
|
441
|
+
return JSON.parse(input);
|
|
442
|
+
} catch (e) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Resolve usage for `data` (preferring stdin rate_limits), then render and exit.
|
|
448
|
+
function emit(data) {
|
|
449
|
+
resolveUsage(data, (usage) => {
|
|
450
|
+
if (data) {
|
|
451
|
+
outputStatus(data, usage);
|
|
452
|
+
} else {
|
|
453
|
+
outputFallback(usage);
|
|
454
|
+
}
|
|
409
455
|
process.exit(0);
|
|
410
456
|
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (process.stdin.isTTY) {
|
|
460
|
+
emit(null);
|
|
411
461
|
} else {
|
|
412
462
|
let input = '';
|
|
413
463
|
let timeoutReached = false;
|
|
@@ -416,19 +466,7 @@ if (process.stdin.isTTY) {
|
|
|
416
466
|
|
|
417
467
|
const timeout = setTimeout(() => {
|
|
418
468
|
timeoutReached = true;
|
|
419
|
-
|
|
420
|
-
if (input.length > 0) {
|
|
421
|
-
try {
|
|
422
|
-
const data = JSON.parse(input);
|
|
423
|
-
outputStatus(data, usage);
|
|
424
|
-
} catch (e) {
|
|
425
|
-
outputFallback(usage);
|
|
426
|
-
}
|
|
427
|
-
} else {
|
|
428
|
-
outputFallback(usage);
|
|
429
|
-
}
|
|
430
|
-
process.exit(0);
|
|
431
|
-
});
|
|
469
|
+
emit(parseInput(input));
|
|
432
470
|
}, overallTimeout);
|
|
433
471
|
|
|
434
472
|
process.stdin.setEncoding('utf8');
|
|
@@ -436,15 +474,6 @@ if (process.stdin.isTTY) {
|
|
|
436
474
|
process.stdin.on('end', () => {
|
|
437
475
|
if (timeoutReached) return;
|
|
438
476
|
clearTimeout(timeout);
|
|
439
|
-
|
|
440
|
-
getUsage((usage) => {
|
|
441
|
-
try {
|
|
442
|
-
const data = JSON.parse(input);
|
|
443
|
-
outputStatus(data, usage);
|
|
444
|
-
} catch (e) {
|
|
445
|
-
outputFallback(usage);
|
|
446
|
-
}
|
|
447
|
-
process.exit(0);
|
|
448
|
-
});
|
|
477
|
+
emit(parseInput(input));
|
|
449
478
|
});
|
|
450
479
|
}
|