claude-simple-status 1.3.2 → 1.4.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 +22 -2
- package/package.json +2 -1
- package/statusline.mjs +60 -15
package/README.md
CHANGED
|
@@ -21,8 +21,8 @@ A simple, no-frills statusline for [Claude Code](https://docs.anthropic.com/en/d
|
|
|
21
21
|
- **Cross-platform** — works on macOS, Linux, and Windows
|
|
22
22
|
- **Non-blocking** — returns cached data instantly, refreshes quota in the background
|
|
23
23
|
- **Color-coded** — green/orange/red percentages at a glance
|
|
24
|
-
- **Context velocity** — estimates remaining turns until context compaction (`42% →~8t`), with directional arrows showing if burn rate is accelerating (↑), steady (→), or decelerating (↓)
|
|
25
|
-
- **Quota pressure** — reset time changes color based on projected burn rate: green (safe), orange (cutting it close), red (will hit the limit before reset). The 7d percentage color is also overridden when the projection says danger
|
|
24
|
+
- **Context velocity** *(opt-in)* — estimates remaining turns until context compaction (`42% →~8t`), with directional arrows showing if burn rate is accelerating (↑), steady (→), or decelerating (↓)
|
|
25
|
+
- **Quota pressure** *(opt-in)* — reset time changes color based on projected burn rate: green (safe), orange (cutting it close), red (will hit the limit before reset). The 7d percentage color is also overridden when the projection says danger
|
|
26
26
|
- **Project name** — bold uppercase project directory name so you never mix up sessions
|
|
27
27
|
- **Git-aware** — shows the current branch name in repos (cached 30s to reduce overhead)
|
|
28
28
|
- **API cost tracking** — pay-as-you-go API users see cumulative session cost instead of quota
|
|
@@ -92,6 +92,26 @@ To uninstall, remove `~/.claude/statusline/` and the `"statusLine"` block from s
|
|
|
92
92
|
|
|
93
93
|
</details>
|
|
94
94
|
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
Create `~/.config/claude-simple-status.json` to enable optional features:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"contextVelocity": true,
|
|
102
|
+
"quotaBurnRate": true,
|
|
103
|
+
"refreshInterval": 300
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| Option | Default | Description |
|
|
108
|
+
|--------|---------|-------------|
|
|
109
|
+
| `contextVelocity` | `false` | Show estimated turns remaining until context compaction (`42% →~8t`) |
|
|
110
|
+
| `quotaBurnRate` | `false` | Color reset time and 7d quota based on projected burn rate |
|
|
111
|
+
| `refreshInterval` | `300` | Seconds between quota API refreshes (min 60) |
|
|
112
|
+
|
|
113
|
+
Without this file, you get a clean statusline showing just project, branch, model, context %, reset time, and quota percentages.
|
|
114
|
+
|
|
95
115
|
## Requirements
|
|
96
116
|
|
|
97
117
|
- Claude Code CLI
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-simple-status",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
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"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
+
"test": "node --test statusline.test.mjs",
|
|
10
11
|
"postinstall": "node scripts/setup.mjs install",
|
|
11
12
|
"preuninstall": "node scripts/setup.mjs uninstall"
|
|
12
13
|
},
|
package/statusline.mjs
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
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 } from 'fs';
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, rmdirSync, statSync, existsSync, realpathSync, readdirSync, unlinkSync } from 'fs';
|
|
6
6
|
import { homedir, tmpdir } from 'os';
|
|
7
7
|
import { join, basename } from 'path';
|
|
8
8
|
import { spawn, execSync } from 'child_process';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
9
10
|
|
|
10
11
|
// Handle --uninstall flag (workaround: npm doesn't run preuninstall for global packages)
|
|
11
12
|
if (process.argv.includes('--uninstall')) {
|
|
@@ -40,9 +41,9 @@ const CREDS_FILE = join(homedir(), '.claude', '.credentials.json');
|
|
|
40
41
|
const CACHE_FILE = join(tmpdir(), 'claude-statusline-quota.json');
|
|
41
42
|
const LOCK_DIR = join(tmpdir(), 'claude-statusline-quota.lock');
|
|
42
43
|
const ERROR_FILE = join(tmpdir(), 'claude-statusline-error');
|
|
44
|
+
const BACKOFF_FILE = join(tmpdir(), 'claude-statusline-backoff');
|
|
43
45
|
const LOG_FILE = join(tmpdir(), 'claude-statusline.log');
|
|
44
|
-
|
|
45
|
-
const CACHE_STALE_AGE = 300; // seconds - when to show "--" instead of old values
|
|
46
|
+
let CACHE_STALE_AGE = 300;
|
|
46
47
|
const GIT_BRANCH_CACHE = join(tmpdir(), 'claude-statusline-branches.json');
|
|
47
48
|
const GIT_BRANCH_MAX_AGE = 30; // seconds
|
|
48
49
|
const CONTEXT_HISTORY_FILE = join(tmpdir(), 'claude-statusline-context.json');
|
|
@@ -81,6 +82,14 @@ function readJsonFile(filepath) {
|
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
// User config — features off by default, opt-in via ~/.config/claude-simple-status.json
|
|
86
|
+
const CONFIG_FILE = join(homedir(), '.config', 'claude-simple-status.json');
|
|
87
|
+
const userConfig = readJsonFile(CONFIG_FILE) || {};
|
|
88
|
+
const SHOW_CONTEXT_VELOCITY = userConfig.contextVelocity === true;
|
|
89
|
+
const SHOW_BURN_RATE = userConfig.quotaBurnRate === true;
|
|
90
|
+
const CACHE_MAX_AGE = Math.max(60, Number(userConfig.refreshInterval) || 300);
|
|
91
|
+
CACHE_STALE_AGE = Math.max(CACHE_STALE_AGE, CACHE_MAX_AGE * 2);
|
|
92
|
+
|
|
84
93
|
// Clean up stale lock (older than 30s)
|
|
85
94
|
function cleanStaleLock() {
|
|
86
95
|
if (existsSync(LOCK_DIR) && getFileAge(LOCK_DIR) > 30) {
|
|
@@ -108,6 +117,7 @@ function refreshInBackground(token) {
|
|
|
108
117
|
const CACHE_FILE = ${JSON.stringify(CACHE_FILE)};
|
|
109
118
|
const LOCK_DIR = ${JSON.stringify(LOCK_DIR)};
|
|
110
119
|
const ERROR_FILE = ${JSON.stringify(ERROR_FILE)};
|
|
120
|
+
const BACKOFF_FILE = ${JSON.stringify(BACKOFF_FILE)};
|
|
111
121
|
const LOG_FILE = ${JSON.stringify(LOG_FILE)};
|
|
112
122
|
|
|
113
123
|
function logError(msg) {
|
|
@@ -125,10 +135,10 @@ function refreshInBackground(token) {
|
|
|
125
135
|
path: '/api/oauth/usage',
|
|
126
136
|
method: 'GET',
|
|
127
137
|
headers: {
|
|
128
|
-
'Authorization': 'Bearer
|
|
138
|
+
'Authorization': 'Bearer ' + process.env._CLAUDE_SS_TOKEN,
|
|
129
139
|
'anthropic-beta': 'oauth-2025-04-20',
|
|
130
140
|
'Accept': 'application/json',
|
|
131
|
-
'User-Agent': 'claude-
|
|
141
|
+
'User-Agent': 'claude-simple-status/1.5.0'
|
|
132
142
|
},
|
|
133
143
|
timeout: 10000
|
|
134
144
|
}, (res) => {
|
|
@@ -140,9 +150,14 @@ function refreshInBackground(token) {
|
|
|
140
150
|
JSON.parse(data);
|
|
141
151
|
writeFileSync(CACHE_FILE, data);
|
|
142
152
|
try { writeFileSync(ERROR_FILE, ''); } catch {}
|
|
153
|
+
try { writeFileSync(BACKOFF_FILE, '{}'); } catch {}
|
|
143
154
|
} catch { logError('Invalid JSON'); }
|
|
155
|
+
} else if (res.statusCode === 429) {
|
|
156
|
+
const retrySec = parseInt(res.headers['retry-after'], 10) || 300;
|
|
157
|
+
const until = Date.now() + retrySec * 1000;
|
|
158
|
+
try { writeFileSync(BACKOFF_FILE, JSON.stringify({ until })); } catch {}
|
|
159
|
+
logError('HTTP 429 (backoff ' + retrySec + 's)');
|
|
144
160
|
} else if (res.statusCode !== 401) {
|
|
145
|
-
// Skip 401 - token not ready yet at startup, will retry next cycle
|
|
146
161
|
logError('HTTP ' + res.statusCode);
|
|
147
162
|
}
|
|
148
163
|
try { rmdirSync(LOCK_DIR); } catch {}
|
|
@@ -154,7 +169,8 @@ function refreshInBackground(token) {
|
|
|
154
169
|
`
|
|
155
170
|
], {
|
|
156
171
|
detached: true,
|
|
157
|
-
stdio: 'ignore'
|
|
172
|
+
stdio: 'ignore',
|
|
173
|
+
env: { _CLAUDE_SS_TOKEN: token }
|
|
158
174
|
});
|
|
159
175
|
child.unref();
|
|
160
176
|
}
|
|
@@ -345,6 +361,25 @@ async function main() {
|
|
|
345
361
|
}
|
|
346
362
|
} catch {}
|
|
347
363
|
|
|
364
|
+
// Persist session snapshot for external tools (/tmp/claude-sessions/<id>.json)
|
|
365
|
+
try {
|
|
366
|
+
const sessionId = JSON.parse(input).session_id;
|
|
367
|
+
if (sessionId) {
|
|
368
|
+
const sessDir = join(tmpdir(), 'claude-sessions');
|
|
369
|
+
mkdirSync(sessDir, { recursive: true });
|
|
370
|
+
writeFileSync(join(sessDir, `${sessionId}.json`), input);
|
|
371
|
+
// Clean up stale sessions (not updated in 5 min = dead)
|
|
372
|
+
for (const f of readdirSync(sessDir)) {
|
|
373
|
+
if (f.endsWith('.json')) {
|
|
374
|
+
try {
|
|
375
|
+
const age = Date.now() - statSync(join(sessDir, f)).mtimeMs;
|
|
376
|
+
if (age > 5 * 60 * 1000) unlinkSync(join(sessDir, f));
|
|
377
|
+
} catch {}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
} catch {}
|
|
382
|
+
|
|
348
383
|
// Get OAuth token
|
|
349
384
|
let token = null;
|
|
350
385
|
const creds = readJsonFile(CREDS_FILE);
|
|
@@ -360,8 +395,10 @@ async function main() {
|
|
|
360
395
|
cleanStaleLock();
|
|
361
396
|
const cacheAge = getFileAge(CACHE_FILE);
|
|
362
397
|
const needRefresh = !quotaData || cacheAge >= CACHE_MAX_AGE;
|
|
398
|
+
const backoffUntil = Number(readJsonFile(BACKOFF_FILE)?.until || 0);
|
|
399
|
+
const inBackoff = Date.now() < backoffUntil;
|
|
363
400
|
|
|
364
|
-
if (needRefresh && acquireLock()) {
|
|
401
|
+
if (needRefresh && !inBackoff && acquireLock()) {
|
|
365
402
|
refreshInBackground(token);
|
|
366
403
|
}
|
|
367
404
|
}
|
|
@@ -400,8 +437,8 @@ async function main() {
|
|
|
400
437
|
hasError = errContent.length > 0;
|
|
401
438
|
} catch {}
|
|
402
439
|
|
|
403
|
-
// Get context velocity estimate
|
|
404
|
-
const velocity = getContextVelocity(projectDir, contextUsed);
|
|
440
|
+
// Get context velocity estimate (opt-in)
|
|
441
|
+
const velocity = SHOW_CONTEXT_VELOCITY ? getContextVelocity(projectDir, contextUsed) : null;
|
|
405
442
|
|
|
406
443
|
// Get rig name (claude-rig sets CLAUDE_CONFIG_DIR to ~/.claude-rig/rigs/<name>)
|
|
407
444
|
const rigProfile = (() => {
|
|
@@ -426,15 +463,15 @@ async function main() {
|
|
|
426
463
|
contextDisplay += ` ${turnsColor}${velocity.arrow}${turnsStr}${RESET}`;
|
|
427
464
|
}
|
|
428
465
|
|
|
429
|
-
// Color the reset time based on 5h quota burn rate projection
|
|
430
|
-
const fiveHourPressure = getQuotaPressure('5h', fiveHourPct, fiveHourResetsAt);
|
|
466
|
+
// Color the reset time based on 5h quota burn rate projection (opt-in)
|
|
467
|
+
const fiveHourPressure = SHOW_BURN_RATE ? getQuotaPressure('5h', fiveHourPct, fiveHourResetsAt) : null;
|
|
431
468
|
let resetDisplay = resetLocal;
|
|
432
469
|
if (fiveHourPressure === 'danger') resetDisplay = `${RED}${resetLocal}${RESET}`;
|
|
433
470
|
else if (fiveHourPressure === 'tight') resetDisplay = `${ORANGE}${resetLocal}${RESET}`;
|
|
434
471
|
else if (fiveHourPressure === 'safe') resetDisplay = `${GREEN}${resetLocal}${RESET}`;
|
|
435
472
|
|
|
436
|
-
// Override 7d percentage color when burn rate projects exhaustion before reset
|
|
437
|
-
const sevenDayPressure = getQuotaPressure('7d', sevenDayPct, sevenDayResetsAt);
|
|
473
|
+
// Override 7d percentage color when burn rate projects exhaustion before reset (opt-in)
|
|
474
|
+
const sevenDayPressure = SHOW_BURN_RATE ? getQuotaPressure('7d', sevenDayPct, sevenDayResetsAt) : null;
|
|
438
475
|
let sevenDayDisplay = colorPct(sevenDayPct);
|
|
439
476
|
if (sevenDayPressure === 'danger') sevenDayDisplay = `${RED}${sevenDayPct}%${RESET}`;
|
|
440
477
|
else if (sevenDayPressure === 'tight') sevenDayDisplay = `${ORANGE}${sevenDayPct}%${RESET}`;
|
|
@@ -452,4 +489,12 @@ async function main() {
|
|
|
452
489
|
process.stdout.write(output);
|
|
453
490
|
}
|
|
454
491
|
|
|
455
|
-
|
|
492
|
+
// Only run when executed directly (not imported for testing)
|
|
493
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
494
|
+
const _isMain = (() => {
|
|
495
|
+
try { return process.argv[1] && realpathSync(process.argv[1]) === realpathSync(__filename); }
|
|
496
|
+
catch { return false; }
|
|
497
|
+
})();
|
|
498
|
+
if (_isMain) main().catch(() => process.exit(1));
|
|
499
|
+
|
|
500
|
+
export { colorPct, getFileAge, readJsonFile, toLocalTime, getContextVelocity, getQuotaPressure, main };
|