@alyibrahim/claude-statusline 1.1.1 → 1.2.1
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 +8 -4
- package/package.json +33 -7
- package/statusline.js +99 -7
package/README.md
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/AlyIbrahim1/claude-statusline/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/@alyibrahim/claude-statusline)
|
|
5
|
-
[](https://www.npmjs.com/package/@alyibrahim/claude-statusline)
|
|
6
5
|
[](LICENSE)
|
|
7
6
|
|
|
8
|
-
**A rich, fast statusline for [Claude Code](https://claude.ai/code)** — shows model, git branch, context usage, rate limits,
|
|
7
|
+
**A rich, fast statusline for [Claude Code](https://claude.ai/code)** — shows model, git branch, context usage, rate limits, session cost, and real-time token consumption after every response.
|
|
9
8
|
|
|
10
9
|
Runs as a **compiled Rust binary** (~5ms startup vs ~100ms for Node.js). Zero shell dependencies. One install command.
|
|
11
10
|
|
|
@@ -27,18 +26,21 @@ Done. The statusline configures itself automatically. Restart Claude Code to see
|
|
|
27
26
|
|
|
28
27
|
## What you get
|
|
29
28
|
|
|
30
|
-
**Line 1** — Model name · Effort level · Active subagents · Current task · Directory `
|
|
29
|
+
**Line 1** — Model name · Effort level · Active subagents · Current task · Directory `(branch +commits)` · Context bar
|
|
31
30
|
|
|
32
|
-
**Line 2** — Weekly token usage · 5h usage · Reset countdown *(Pro/Max)* — or — Session cost *(API key)*
|
|
31
|
+
**Line 2** — Weekly token usage · 5h usage · Reset countdown *(Pro/Max)* — or — Session cost *(API key)* · Session token count
|
|
33
32
|
|
|
34
33
|
| Feature | Details |
|
|
35
34
|
|---|---|
|
|
36
35
|
| Context bar | Normalized to usable % — accounts for the auto-compact buffer |
|
|
37
36
|
| Rate limits | Shows 5h and weekly usage with color-coded thresholds |
|
|
38
37
|
| Session cost | Displayed only for API key users, hidden for subscribers |
|
|
38
|
+
| Session tokens | Real-time token consumption via JSONL offset caching — updates between turns, not just at turn start |
|
|
39
39
|
| Active agents | Counts running subagents from your `~/.claude/todos/` directory |
|
|
40
40
|
| Effort level | Reads `CLAUDE_CODE_EFFORT_LEVEL` env var or `settings.json` |
|
|
41
41
|
| Git branch | Detected automatically, silently absent if not a git repo |
|
|
42
|
+
| Session commits | Shows `+N` next to the branch for commits made during the current session |
|
|
43
|
+
| Directory label | Displays as `~/parent/dir` so you always know which project you're in |
|
|
42
44
|
|
|
43
45
|
---
|
|
44
46
|
|
|
@@ -68,6 +70,8 @@ npm picks the right one automatically. If your platform isn't listed, the JS fal
|
|
|
68
70
|
| Subscription-aware | Shows usage/resets for Pro/Max, cost for API | Treat everyone as API user |
|
|
69
71
|
| Context bar | Usable % after auto-compact buffer | Raw remaining % |
|
|
70
72
|
| Subagent counter | Counts active agents from todos dir | — |
|
|
73
|
+
| Session tokens | Real-time via JSONL offset cache | Stale stdin snapshot or none |
|
|
74
|
+
| Session commits | Tracks git commits made this session | — |
|
|
71
75
|
|
|
72
76
|
---
|
|
73
77
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alyibrahim/claude-statusline",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "Rich statusline for Claude Code — model, context bar, real-time token tracking, git branch, rate limits, and session stats. Rust binary, ~5ms startup.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"anthropic",
|
|
9
|
+
"statusline",
|
|
10
|
+
"status-bar",
|
|
11
|
+
"terminal",
|
|
12
|
+
"cli",
|
|
13
|
+
"ai",
|
|
14
|
+
"llm",
|
|
15
|
+
"token-tracking",
|
|
16
|
+
"context-window",
|
|
17
|
+
"rate-limit",
|
|
18
|
+
"git",
|
|
19
|
+
"developer-tools",
|
|
20
|
+
"productivity",
|
|
21
|
+
"rust"
|
|
22
|
+
],
|
|
23
|
+
"homepage": "https://github.com/AlyIbrahim1/claude-statusline#readme",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/AlyIbrahim1/claude-statusline.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/AlyIbrahim1/claude-statusline/issues"
|
|
30
|
+
},
|
|
5
31
|
"engines": {
|
|
6
32
|
"node": ">=16"
|
|
7
33
|
},
|
|
@@ -21,11 +47,11 @@
|
|
|
21
47
|
],
|
|
22
48
|
"license": "MIT",
|
|
23
49
|
"optionalDependencies": {
|
|
24
|
-
"@alyibrahim/claude-statusline-linux-x64": "1.
|
|
25
|
-
"@alyibrahim/claude-statusline-linux-arm64": "1.
|
|
26
|
-
"@alyibrahim/claude-statusline-darwin-x64": "1.
|
|
27
|
-
"@alyibrahim/claude-statusline-darwin-arm64": "1.
|
|
28
|
-
"@alyibrahim/claude-statusline-win32-x64": "1.
|
|
50
|
+
"@alyibrahim/claude-statusline-linux-x64": "1.2.0",
|
|
51
|
+
"@alyibrahim/claude-statusline-linux-arm64": "1.2.0",
|
|
52
|
+
"@alyibrahim/claude-statusline-darwin-x64": "1.2.0",
|
|
53
|
+
"@alyibrahim/claude-statusline-darwin-arm64": "1.2.0",
|
|
54
|
+
"@alyibrahim/claude-statusline-win32-x64": "1.2.0"
|
|
29
55
|
},
|
|
30
56
|
"devDependencies": {
|
|
31
57
|
"jest": "^29.0.0"
|
package/statusline.js
CHANGED
|
@@ -5,7 +5,58 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
|
-
const { execSync } = require('child_process');
|
|
8
|
+
const { execSync, execFileSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
// Reads cumulative token totals from the session JSONL file, using a byte-offset
|
|
11
|
+
// cache so only new bytes are parsed on each invocation (O(new bytes) not O(file)).
|
|
12
|
+
// Returns { totalIn, totalOut } or null on any error.
|
|
13
|
+
function readSessionTokens(claudeDir, session, absDir) {
|
|
14
|
+
if (!session) return null;
|
|
15
|
+
const slug = absDir.replace(/\//g, '-');
|
|
16
|
+
const jsonlPath = path.join(claudeDir, 'projects', slug, `${session}.jsonl`);
|
|
17
|
+
const cachePath = path.join(claudeDir, `statusline-tokcache-${session}.json`);
|
|
18
|
+
try {
|
|
19
|
+
const fileSize = fs.statSync(jsonlPath).size;
|
|
20
|
+
let totalIn = 0, totalOut = 0, cachedOffset = 0;
|
|
21
|
+
try {
|
|
22
|
+
const cached = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
23
|
+
totalIn = cached.totalIn || 0;
|
|
24
|
+
totalOut = cached.totalOut || 0;
|
|
25
|
+
cachedOffset = Math.min(cached.offset || 0, fileSize);
|
|
26
|
+
} catch (e) {}
|
|
27
|
+
if (fileSize > cachedOffset) {
|
|
28
|
+
const fd = fs.openSync(jsonlPath, 'r');
|
|
29
|
+
const buf = Buffer.alloc(fileSize - cachedOffset);
|
|
30
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, cachedOffset);
|
|
31
|
+
fs.closeSync(fd);
|
|
32
|
+
const content = buf.subarray(0, bytesRead).toString('utf8');
|
|
33
|
+
// Exclude the last element: it's either an empty string (content ends with \n)
|
|
34
|
+
// or a potentially incomplete line (file was mid-write).
|
|
35
|
+
const safeLines = content.split('\n').slice(0, -1);
|
|
36
|
+
for (const line of safeLines) {
|
|
37
|
+
if (!line.trim()) continue;
|
|
38
|
+
try {
|
|
39
|
+
const entry = JSON.parse(line);
|
|
40
|
+
if (entry.type === 'assistant' && entry.message?.usage) {
|
|
41
|
+
const u = entry.message.usage;
|
|
42
|
+
totalIn += (u.input_tokens || 0) + (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0);
|
|
43
|
+
totalOut += (u.output_tokens || 0);
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {}
|
|
46
|
+
}
|
|
47
|
+
// Advance offset by the bytes of all complete lines (each terminated by \n)
|
|
48
|
+
const processed = safeLines.join('\n') + '\n';
|
|
49
|
+
try {
|
|
50
|
+
fs.writeFileSync(cachePath, JSON.stringify({
|
|
51
|
+
totalIn, totalOut, offset: cachedOffset + Buffer.from(processed, 'utf8').length,
|
|
52
|
+
}));
|
|
53
|
+
} catch (e) {}
|
|
54
|
+
}
|
|
55
|
+
return { totalIn, totalOut };
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
9
60
|
|
|
10
61
|
// Read JSON from stdin
|
|
11
62
|
let input = '';
|
|
@@ -104,26 +155,67 @@ process.stdin.on('end', () => {
|
|
|
104
155
|
}
|
|
105
156
|
}
|
|
106
157
|
|
|
107
|
-
// Git branch
|
|
158
|
+
// Git branch + session commit counter
|
|
159
|
+
const absDir = path.resolve(dir);
|
|
108
160
|
let branch = '';
|
|
161
|
+
let commitCount = 0;
|
|
109
162
|
try {
|
|
110
163
|
branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
164
|
+
const headSha = execSync('git rev-parse HEAD', { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
165
|
+
if (session) {
|
|
166
|
+
const sessionFile = path.join(claudeDir, `statusline-session-${session}.json`);
|
|
167
|
+
let sessionData = {};
|
|
168
|
+
try { sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8')); } catch (e) {}
|
|
169
|
+
if (!sessionData[absDir]) {
|
|
170
|
+
sessionData[absDir] = headSha;
|
|
171
|
+
try { fs.writeFileSync(sessionFile, JSON.stringify(sessionData)); } catch (e) {}
|
|
172
|
+
}
|
|
173
|
+
const baseline = sessionData[absDir];
|
|
174
|
+
if (baseline !== headSha) {
|
|
175
|
+
const countStr = execFileSync('git', ['rev-list', '--count', `${baseline}..HEAD`], { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
176
|
+
commitCount = parseInt(countStr, 10) || 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
111
179
|
} catch (e) {}
|
|
112
180
|
|
|
113
181
|
// Output
|
|
114
|
-
const dirname = sanitize(path.basename(dir));
|
|
115
182
|
const safeBranch = sanitize(branch);
|
|
116
|
-
|
|
183
|
+
let dirLabel;
|
|
184
|
+
if (absDir === homeDir) {
|
|
185
|
+
dirLabel = '~';
|
|
186
|
+
} else if (path.dirname(absDir) === homeDir) {
|
|
187
|
+
dirLabel = `~/${path.basename(absDir)}`;
|
|
188
|
+
} else {
|
|
189
|
+
dirLabel = `~/${path.basename(path.dirname(absDir))}/${path.basename(absDir)}`;
|
|
190
|
+
}
|
|
191
|
+
const dirname = sanitize(dirLabel);
|
|
192
|
+
// dirname is bright; branch stays cyan; commit count dim after branch
|
|
193
|
+
const commitSuffix = commitCount > 0 ? ` \x1b[32m+${commitCount}` : '';
|
|
194
|
+
const branchStr = `(${safeBranch})${commitSuffix}\x1b[0m \x1b[2m│\x1b[0m`;
|
|
117
195
|
const dirDisplay = safeBranch
|
|
118
|
-
? `\x1b[1m\x1b[97m${dirname}\x1b[0m\x1b[2m \x1b[36m
|
|
196
|
+
? `\x1b[1m\x1b[97m${dirname}\x1b[0m\x1b[2m \x1b[36m${branchStr}\x1b[0m`
|
|
119
197
|
: `\x1b[1m\x1b[97m${dirname}\x1b[0m`;
|
|
120
198
|
const u5h = usageLine('Current', pct5h, resetSuffix), u7d = usageLine('Weekly', pctWeek);
|
|
121
199
|
const costDisplay = sessionCost !== null
|
|
122
200
|
? ` \x1b[33m$${sessionCost < 0.01 ? sessionCost.toFixed(4) : sessionCost.toFixed(2)}\x1b[0m`
|
|
123
201
|
: '';
|
|
202
|
+
// Session token consumption — prefer JSONL-sourced totals (accurate through
|
|
203
|
+
// the last completed tool use) over the stdin snapshot (only updated at turn start).
|
|
204
|
+
const stdinIn = data.context_window?.total_input_tokens ?? null;
|
|
205
|
+
const stdinOut = data.context_window?.total_output_tokens ?? null;
|
|
206
|
+
const jsonlTok = readSessionTokens(claudeDir, session, absDir);
|
|
207
|
+
const totalIn = jsonlTok && jsonlTok.totalIn > (stdinIn ?? 0) ? jsonlTok.totalIn : stdinIn;
|
|
208
|
+
const totalOut = jsonlTok && jsonlTok.totalOut > (stdinOut ?? 0) ? jsonlTok.totalOut : stdinOut;
|
|
209
|
+
let tokenDisplay = '';
|
|
210
|
+
if (totalIn != null || totalOut != null || jsonlTok) {
|
|
211
|
+
const total = (totalIn ?? 0) + (totalOut ?? 0);
|
|
212
|
+
const fmt = n => n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
213
|
+
tokenDisplay = `\x1b[2m│\x1b[0m \x1b[97m${fmt(total)} tok\x1b[0m`;
|
|
214
|
+
}
|
|
215
|
+
|
|
124
216
|
const usageContent = [u7d, u5h].filter(Boolean).join(' ');
|
|
125
|
-
const line2 = (usageContent || costDisplay)
|
|
126
|
-
? `\x1b[0m\x1b[32mUsage\x1b[0m \x1b[2m│\x1b[0m ${[usageContent, costDisplay].filter(Boolean).join(' ')}`
|
|
217
|
+
const line2 = (usageContent || costDisplay || tokenDisplay)
|
|
218
|
+
? `\x1b[0m\x1b[32mUsage\x1b[0m \x1b[2m│\x1b[0m ${[usageContent, costDisplay, tokenDisplay].filter(Boolean).join(' ')}`
|
|
127
219
|
: '';
|
|
128
220
|
// Effort level: read from env var first, then settings.json, then fall back to
|
|
129
221
|
// model-based default (sonnet-4/opus-4 default to "medium" in Claude Code).
|