@alyibrahim/claude-statusline 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 +64 -26
  2. package/package.json +6 -6
  3. package/statusline.js +99 -7
package/README.md CHANGED
@@ -2,12 +2,15 @@
2
2
 
3
3
  [![CI](https://github.com/AlyIbrahim1/claude-statusline/actions/workflows/ci.yml/badge.svg)](https://github.com/AlyIbrahim1/claude-statusline/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/@alyibrahim/claude-statusline)](https://www.npmjs.com/package/@alyibrahim/claude-statusline)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
6
 
6
- A fast statusline for [Claude Code](https://claude.ai/code). Shows model, git branch, context usage, subscription rate limits, and session cost updating after every response.
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.
7
8
 
8
- Runs as a compiled Rust binary on Linux x64/arm64, macOS x64/arm64, and Windows x64 — no Node.js startup overhead on every prompt. Falls back to Node.js automatically on unsupported platforms.
9
+ Runs as a **compiled Rust binary** (~5ms startup vs ~100ms for Node.js). Zero shell dependencies. One install command.
9
10
 
10
- ![statusline screenshot](.github/image.png)
11
+ ![statusline screenshot](https://raw.githubusercontent.com/AlyIbrahim1/claude-statusline/main/.github/image.png)
12
+
13
+ ---
11
14
 
12
15
  ## Install
13
16
 
@@ -15,36 +18,69 @@ Runs as a compiled Rust binary on Linux x64/arm64, macOS x64/arm64, and Windows
15
18
  npm install -g @alyibrahim/claude-statusline
16
19
  ```
17
20
 
18
- That's it. The statusline is configured automatically. Restart Claude Code to see it.
21
+ Done. The statusline configures itself automatically. Restart Claude Code to see it.
19
22
 
20
- **Manual setup** (if auto-setup failed):
21
- ```bash
22
- claude-statusline setup
23
- ```
23
+ > If auto-setup didn't run: `claude-statusline setup`
24
24
 
25
- ## Requirements
25
+ ---
26
+
27
+ ## What you get
28
+
29
+ **Line 1** — Model name · Effort level · Active subagents · Current task · Directory `(branch +commits)` · Context bar
30
+
31
+ **Line 2** — Weekly token usage · 5h usage · Reset countdown *(Pro/Max)* — or — Session cost *(API key)* · Session token count
32
+
33
+ | Feature | Details |
34
+ |---|---|
35
+ | Context bar | Normalized to usable % — accounts for the auto-compact buffer |
36
+ | Rate limits | Shows 5h and weekly usage with color-coded thresholds |
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
+ | Active agents | Counts running subagents from your `~/.claude/todos/` directory |
40
+ | Effort level | Reads `CLAUDE_CODE_EFFORT_LEVEL` env var or `settings.json` |
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 |
26
44
 
27
- - **Node.js >=16** — needed for install/uninstall lifecycle scripts
28
- - **git** — optional, used for branch display; gracefully absent if not installed
45
+ ---
29
46
 
30
- No `jq`, `bc`, `ccusage`, or other external tools needed.
47
+ ## Platform support
31
48
 
32
- ## What it shows
49
+ The Rust binary is pre-built and installed automatically for your platform via npm `optionalDependencies`:
33
50
 
34
- **Line 1:** Model · Effort level · Active agents · Current task · Directory `[git branch]` · Context bar
51
+ | Platform | Package |
52
+ |---|---|
53
+ | Linux x64 | [`@alyibrahim/claude-statusline-linux-x64`](https://www.npmjs.com/package/@alyibrahim/claude-statusline-linux-x64) |
54
+ | Linux arm64 | [`@alyibrahim/claude-statusline-linux-arm64`](https://www.npmjs.com/package/@alyibrahim/claude-statusline-linux-arm64) |
55
+ | macOS x64 (Intel) | [`@alyibrahim/claude-statusline-darwin-x64`](https://www.npmjs.com/package/@alyibrahim/claude-statusline-darwin-x64) |
56
+ | macOS arm64 (Apple Silicon) | [`@alyibrahim/claude-statusline-darwin-arm64`](https://www.npmjs.com/package/@alyibrahim/claude-statusline-darwin-arm64) |
57
+ | Windows x64 | [`@alyibrahim/claude-statusline-win32-x64`](https://www.npmjs.com/package/@alyibrahim/claude-statusline-win32-x64) |
35
58
 
36
- **Line 2:** Weekly usage · 5h usage · Reset countdown *(subscription)* or Session cost *(API key)*
59
+ npm picks the right one automatically. If your platform isn't listed, the JS fallback is used instead — no action needed.
60
+
61
+ ---
37
62
 
38
63
  ## Why this one
39
64
 
40
- | | This package | Others |
65
+ | | claude-statusline | Others |
41
66
  |---|---|---|
42
- | Fast startup | compiled Rust binary | Node.js cold-start every prompt |
43
- | No dependencies | no `jq`, `bc`, etc. | Require external tools |
44
- | No API calls | reads stdin directly | Poll OAuth endpoint, hit rate limits |
45
- | Subscription vs API aware | | Show cost for everyone |
46
- | Context bar normalized | usable % | Raw remaining % |
47
- | Active agent counter | | — |
67
+ | Startup time | ~5ms (Rust binary) | ~100ms (Node.js cold-start every prompt) |
68
+ | Shell dependencies | None | Require `jq`, `bc`, or `ccusage` |
69
+ | API calls | None reads Claude's stdin directly | Poll OAuth endpoint, risk rate limits |
70
+ | Subscription-aware | Shows usage/resets for Pro/Max, cost for API | Treat everyone as API user |
71
+ | Context bar | Usable % after auto-compact buffer | Raw remaining % |
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 | — |
75
+
76
+ ---
77
+
78
+ ## Requirements
79
+
80
+ - **Node.js ≥16** — for install/uninstall scripts only (not needed at runtime on supported platforms)
81
+ - **git** — optional, enables branch display
82
+
83
+ ---
48
84
 
49
85
  ## Uninstall
50
86
 
@@ -53,13 +89,15 @@ claude-statusline uninstall
53
89
  npm uninstall -g @alyibrahim/claude-statusline
54
90
  ```
55
91
 
56
- > Run `claude-statusline uninstall` first regardless of package manager this removes the statusline from `~/.claude/settings.json` before the package files are deleted.
92
+ > Always run `claude-statusline uninstall` first — it removes the `statusLine` entry from `~/.claude/settings.json` before the files are deleted.
93
+
94
+ ---
57
95
 
58
96
  ## Notes
59
97
 
60
- - **Switched Node versions?** Re-run `claude-statusline setup` — only needed if the Rust binary wasn't installed (unsupported platform fallback).
61
- - Writes only the `statusLine` key in `~/.claude/settings.json` — all other settings are preserved.
62
- - Respects `$CLAUDE_CONFIG_DIR` if set.
98
+ - Settings are written only to the `statusLine` key all other `~/.claude/settings.json` keys are untouched
99
+ - Respects `$CLAUDE_CONFIG_DIR` if set
100
+ - Switched Node versions on an unsupported platform? Re-run `claude-statusline setup`
63
101
 
64
102
  ## License
65
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alyibrahim/claude-statusline",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A zero-dependency Claude Code statusline — reads rate limits from stdin, no API calls",
5
5
  "engines": {
6
6
  "node": ">=16"
@@ -21,11 +21,11 @@
21
21
  ],
22
22
  "license": "MIT",
23
23
  "optionalDependencies": {
24
- "@alyibrahim/claude-statusline-linux-x64": "1.1.0",
25
- "@alyibrahim/claude-statusline-linux-arm64": "1.1.0",
26
- "@alyibrahim/claude-statusline-darwin-x64": "1.1.0",
27
- "@alyibrahim/claude-statusline-darwin-arm64": "1.1.0",
28
- "@alyibrahim/claude-statusline-win32-x64": "1.1.0"
24
+ "@alyibrahim/claude-statusline-linux-x64": "1.2.0",
25
+ "@alyibrahim/claude-statusline-linux-arm64": "1.2.0",
26
+ "@alyibrahim/claude-statusline-darwin-x64": "1.2.0",
27
+ "@alyibrahim/claude-statusline-darwin-arm64": "1.2.0",
28
+ "@alyibrahim/claude-statusline-win32-x64": "1.2.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "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 (command is a fixed string; dir is passed as cwd, not interpolated — no injection risk)
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
- // dirname is bright; branch stays cyan; surrounding prefix/suffix stay dim
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[${safeBranch}]\x1b[0m`
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).