claude-simple-status 1.0.2 → 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.
- package/README.md +25 -3
- package/package.json +2 -2
- package/statusline.mjs +48 -33
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://nodejs.org/)
|
|
10
10
|
[]()
|
|
11
11
|
|
|
12
|
-
A simple, no-frills statusline for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) that shows what matters: **git branch, model, context usage, and
|
|
12
|
+
A simple, no-frills statusline for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) that shows what matters: **project name, git branch, model, context usage, quota, and API costs**.
|
|
13
13
|
|
|
14
14
|

|
|
15
15
|
|
|
@@ -19,7 +19,10 @@ A simple, no-frills statusline for [Claude Code](https://docs.anthropic.com/en/d
|
|
|
19
19
|
- **Cross-platform** — works on macOS, Linux, and Windows
|
|
20
20
|
- **Non-blocking** — returns cached data instantly, refreshes quota in the background
|
|
21
21
|
- **Color-coded** — green/orange/red percentages at a glance
|
|
22
|
-
- **
|
|
22
|
+
- **Project name** — bold uppercase project directory name so you never mix up sessions
|
|
23
|
+
- **Git-aware** — shows the current branch name in repos (cached 30s to reduce overhead)
|
|
24
|
+
- **API cost tracking** — pay-as-you-go API users see cumulative session cost instead of quota
|
|
25
|
+
- **Stale-aware** — shows `--` for quota values when cache is outdated, real values appear after first refresh
|
|
23
26
|
- **Timezone-smart** — quota reset time converted to your local timezone
|
|
24
27
|
|
|
25
28
|
If the quota API is unreachable, a red `ERR` indicator appears at the end and clears automatically once the connection recovers.
|
|
@@ -92,16 +95,23 @@ To uninstall, remove `~/.claude/statusline/` and the `"statusLine"` block from s
|
|
|
92
95
|
|
|
93
96
|
## How it works
|
|
94
97
|
|
|
95
|
-
1. Receives model/context info from Claude Code via stdin (JSON)
|
|
98
|
+
1. Receives model/context/cost info from Claude Code via stdin (JSON)
|
|
96
99
|
2. Reads cached quota data and returns immediately (never blocks the UI)
|
|
97
100
|
3. If the cache is stale (>2 minutes), refreshes from Anthropic's OAuth API in the background
|
|
98
101
|
4. Converts UTC reset time to your local timezone
|
|
99
102
|
5. Outputs a formatted statusline with ANSI colors
|
|
100
103
|
|
|
104
|
+
**Subscription users** see quota percentages and reset times. **API (pay-as-you-go) users** see cumulative session cost (e.g. `$4.72`) — calculated by Claude Code from actual token usage, no external pricing lookups needed.
|
|
105
|
+
|
|
101
106
|
Quota data is cached to the system temp directory and refreshed every 2 minutes. Since Claude Code calls the statusline on every message update, this avoids excessive API calls while keeping the data fresh.
|
|
102
107
|
|
|
103
108
|
## Troubleshooting
|
|
104
109
|
|
|
110
|
+
**Indicators:**
|
|
111
|
+
- `--` for quota values means the cache is stale (>5 minutes old) — values appear after the first background refresh
|
|
112
|
+
- `?` means quota data has never been fetched yet
|
|
113
|
+
- `ERR` (red) means the last quota fetch failed — clears automatically on recovery
|
|
114
|
+
|
|
105
115
|
If the statusline shows `ERR`, check the error log:
|
|
106
116
|
|
|
107
117
|
```bash
|
|
@@ -122,6 +132,18 @@ rm /tmp/claude-statusline-quota.json
|
|
|
122
132
|
Remove-Item $env:TEMP\claude-statusline-quota.json
|
|
123
133
|
```
|
|
124
134
|
|
|
135
|
+
## Related projects
|
|
136
|
+
|
|
137
|
+
### [claude-rig](https://github.com/edimuj/claude-rig)
|
|
138
|
+
|
|
139
|
+
Run multiple isolated Claude Code configurations simultaneously — each with its own plugins, skills, MCP servers, and settings. When a session is launched through claude-rig, the active profile name appears in the statusline in bold magenta:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
MY-PROJECT [main] | minimal | Opus 4.6 | 12% | 14:30 | 5h:34% | 7d:12%
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
No configuration needed — claude-simple-status detects claude-rig automatically. Users not using claude-rig are unaffected.
|
|
146
|
+
|
|
125
147
|
## Contributing
|
|
126
148
|
|
|
127
149
|
Contributions are welcome! This project follows a few principles:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-simple-status",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "A simple statusline for Claude Code — git branch, model, context usage, and
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "A simple statusline for Claude Code — project name, git branch, model, context usage, quota, and API costs at a glance",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-simple-status": "statusline.mjs"
|
package/statusline.mjs
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
// Claude Code Statusline - Shows Branch | Model | Context % | Next Reset | 5h Quota % | 7d Quota %
|
|
3
3
|
// Cross-platform Node.js version (no dependencies)
|
|
4
4
|
|
|
5
|
-
import { readFileSync, writeFileSync, mkdirSync, rmdirSync, statSync, existsSync
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, rmdirSync, statSync, existsSync } from 'fs';
|
|
6
6
|
import { homedir, tmpdir } from 'os';
|
|
7
|
-
import { join } from 'path';
|
|
7
|
+
import { join, basename } from 'path';
|
|
8
8
|
import { spawn, execSync } from 'child_process';
|
|
9
|
-
import { request } from 'https';
|
|
10
9
|
|
|
11
10
|
// Handle --uninstall flag (workaround: npm doesn't run preuninstall for global packages)
|
|
12
11
|
if (process.argv.includes('--uninstall')) {
|
|
@@ -31,6 +30,8 @@ const GREEN = '\x1b[0;32m';
|
|
|
31
30
|
const ORANGE = '\x1b[0;33m';
|
|
32
31
|
const RED = '\x1b[0;31m';
|
|
33
32
|
const CYAN = '\x1b[0;36m';
|
|
33
|
+
const WHITE_BOLD = '\x1b[1;37m';
|
|
34
|
+
const MAGENTA_BOLD = '\x1b[1;35m';
|
|
34
35
|
const YELLOW_BOLD = '\x1b[1;33m';
|
|
35
36
|
const RESET = '\x1b[0m';
|
|
36
37
|
|
|
@@ -40,7 +41,10 @@ const CACHE_FILE = join(tmpdir(), 'claude-statusline-quota.json');
|
|
|
40
41
|
const LOCK_DIR = join(tmpdir(), 'claude-statusline-quota.lock');
|
|
41
42
|
const ERROR_FILE = join(tmpdir(), 'claude-statusline-error');
|
|
42
43
|
const LOG_FILE = join(tmpdir(), 'claude-statusline.log');
|
|
43
|
-
const CACHE_MAX_AGE = 120; // seconds
|
|
44
|
+
const CACHE_MAX_AGE = 120; // seconds - when to fetch
|
|
45
|
+
const CACHE_STALE_AGE = 300; // seconds - when to show "--" instead of old values
|
|
46
|
+
const GIT_BRANCH_CACHE = join(tmpdir(), 'claude-statusline-branches.json');
|
|
47
|
+
const GIT_BRANCH_MAX_AGE = 30; // seconds
|
|
44
48
|
|
|
45
49
|
// Color a percentage value based on thresholds
|
|
46
50
|
function colorPct(val) {
|
|
@@ -89,30 +93,6 @@ function acquireLock() {
|
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
// Release lock
|
|
93
|
-
function releaseLock() {
|
|
94
|
-
try { rmdirSync(LOCK_DIR); } catch {}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Log error with timestamp
|
|
98
|
-
function logError(msg) {
|
|
99
|
-
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
100
|
-
try {
|
|
101
|
-
appendFileSync(LOG_FILE, `[${ts}] ${msg}\n`);
|
|
102
|
-
writeFileSync(ERROR_FILE, msg);
|
|
103
|
-
// Trim log to last 50 lines
|
|
104
|
-
const lines = readFileSync(LOG_FILE, 'utf8').split('\n').filter(Boolean);
|
|
105
|
-
if (lines.length > 50) {
|
|
106
|
-
writeFileSync(LOG_FILE, lines.slice(-50).join('\n') + '\n');
|
|
107
|
-
}
|
|
108
|
-
} catch {}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Clear error state
|
|
112
|
-
function clearError() {
|
|
113
|
-
try { writeFileSync(ERROR_FILE, ''); } catch {}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
96
|
// Spawn background refresh process
|
|
117
97
|
function refreshInBackground(token) {
|
|
118
98
|
const child = spawn(process.execPath, [
|
|
@@ -187,13 +167,22 @@ function toLocalTime(isoString) {
|
|
|
187
167
|
}
|
|
188
168
|
}
|
|
189
169
|
|
|
190
|
-
// Get current git branch name
|
|
170
|
+
// Get current git branch name (cached per cwd, 30s TTL)
|
|
191
171
|
function getGitBranch() {
|
|
172
|
+
const cwd = process.cwd();
|
|
192
173
|
try {
|
|
193
|
-
|
|
174
|
+
const cache = readJsonFile(GIT_BRANCH_CACHE) || {};
|
|
175
|
+
const entry = cache[cwd];
|
|
176
|
+
if (entry && (Date.now() - entry.ts) < GIT_BRANCH_MAX_AGE * 1000) {
|
|
177
|
+
return entry.branch;
|
|
178
|
+
}
|
|
179
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
194
180
|
timeout: 1000,
|
|
195
181
|
stdio: ['ignore', 'pipe', 'ignore']
|
|
196
182
|
}).toString().trim();
|
|
183
|
+
cache[cwd] = { branch, ts: Date.now() };
|
|
184
|
+
try { writeFileSync(GIT_BRANCH_CACHE, JSON.stringify(cache)); } catch {}
|
|
185
|
+
return branch;
|
|
197
186
|
} catch {
|
|
198
187
|
return null;
|
|
199
188
|
}
|
|
@@ -210,10 +199,18 @@ async function main() {
|
|
|
210
199
|
// Parse Claude Code input
|
|
211
200
|
let model = 'Unknown';
|
|
212
201
|
let contextUsed = 0;
|
|
202
|
+
let totalCostUsd = null;
|
|
203
|
+
let projectName = null;
|
|
213
204
|
try {
|
|
214
205
|
const data = JSON.parse(input);
|
|
215
206
|
model = data.model?.display_name || 'Unknown';
|
|
216
207
|
contextUsed = data.context_window?.used_percentage || 0;
|
|
208
|
+
if (typeof data.cost?.total_cost_usd === 'number') {
|
|
209
|
+
totalCostUsd = data.cost.total_cost_usd;
|
|
210
|
+
}
|
|
211
|
+
if (data.workspace?.project_dir) {
|
|
212
|
+
projectName = basename(data.workspace.project_dir).toUpperCase();
|
|
213
|
+
}
|
|
217
214
|
} catch {}
|
|
218
215
|
|
|
219
216
|
// Get OAuth token
|
|
@@ -237,12 +234,17 @@ async function main() {
|
|
|
237
234
|
}
|
|
238
235
|
}
|
|
239
236
|
|
|
240
|
-
// Parse quota data
|
|
237
|
+
// Parse quota data (show "--" if cache is too stale)
|
|
241
238
|
let fiveHourPct = '?';
|
|
242
239
|
let sevenDayPct = '?';
|
|
243
240
|
let resetLocal = '--:--';
|
|
241
|
+
const cacheIsStale = !quotaData || getFileAge(CACHE_FILE) > CACHE_STALE_AGE;
|
|
244
242
|
|
|
245
|
-
if (
|
|
243
|
+
if (cacheIsStale) {
|
|
244
|
+
fiveHourPct = '--';
|
|
245
|
+
sevenDayPct = '--';
|
|
246
|
+
resetLocal = '--:--';
|
|
247
|
+
} else if (quotaData) {
|
|
246
248
|
if (quotaData.five_hour === null || quotaData.seven_day === null) {
|
|
247
249
|
// Organization/team plan without individual quota
|
|
248
250
|
fiveHourPct = 'N/A';
|
|
@@ -262,16 +264,29 @@ async function main() {
|
|
|
262
264
|
hasError = errContent.length > 0;
|
|
263
265
|
} catch {}
|
|
264
266
|
|
|
267
|
+
// Get rig profile (claude-rig sets CLAUDE_CONFIG_DIR to ~/.claude-rig/profiles/<name>)
|
|
268
|
+
const rigProfile = (() => {
|
|
269
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
270
|
+
if (!configDir) return null;
|
|
271
|
+
const match = configDir.match(/\.claude-rig\/profiles\/([^/]+)\/?$/);
|
|
272
|
+
return match ? match[1] : null;
|
|
273
|
+
})();
|
|
274
|
+
|
|
265
275
|
// Get git branch
|
|
266
276
|
const branch = getGitBranch();
|
|
267
277
|
|
|
268
278
|
// Build output
|
|
269
|
-
|
|
279
|
+
const projectSegment = projectName
|
|
280
|
+
? `${WHITE_BOLD}${projectName}${branch ? ` ${YELLOW_BOLD}[${branch}]` : ''}${RESET}`
|
|
281
|
+
: (branch ? `${YELLOW_BOLD}${branch}${RESET}` : '');
|
|
282
|
+
let output = `${projectSegment ? `${projectSegment} | ` : ''}${rigProfile ? `${MAGENTA_BOLD}${rigProfile}${RESET} | ` : ''}${CYAN}${model}${RESET} | ${colorPct(contextUsed)}`;
|
|
270
283
|
if (token) {
|
|
271
284
|
output += ` | ${resetLocal} | 5h:${colorPct(fiveHourPct)} | 7d:${colorPct(sevenDayPct)}`;
|
|
272
285
|
if (hasError) {
|
|
273
286
|
output += ` | ${RED}ERR${RESET}`;
|
|
274
287
|
}
|
|
288
|
+
} else if (totalCostUsd !== null) {
|
|
289
|
+
output += ` | ${GREEN}$${totalCostUsd.toFixed(2)}${RESET}`;
|
|
275
290
|
}
|
|
276
291
|
|
|
277
292
|
process.stdout.write(output);
|