claude-simple-status 1.4.0 → 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 +3 -1
- package/package.json +2 -1
- package/statusline.mjs +48 -9
package/README.md
CHANGED
|
@@ -99,7 +99,8 @@ Create `~/.config/claude-simple-status.json` to enable optional features:
|
|
|
99
99
|
```json
|
|
100
100
|
{
|
|
101
101
|
"contextVelocity": true,
|
|
102
|
-
"quotaBurnRate": true
|
|
102
|
+
"quotaBurnRate": true,
|
|
103
|
+
"refreshInterval": 300
|
|
103
104
|
}
|
|
104
105
|
```
|
|
105
106
|
|
|
@@ -107,6 +108,7 @@ Create `~/.config/claude-simple-status.json` to enable optional features:
|
|
|
107
108
|
|--------|---------|-------------|
|
|
108
109
|
| `contextVelocity` | `false` | Show estimated turns remaining until context compaction (`42% →~8t`) |
|
|
109
110
|
| `quotaBurnRate` | `false` | Color reset time and 7d quota based on projected burn rate |
|
|
111
|
+
| `refreshInterval` | `300` | Seconds between quota API refreshes (min 60) |
|
|
110
112
|
|
|
111
113
|
Without this file, you get a clean statusline showing just project, branch, model, context %, reset time, and quota percentages.
|
|
112
114
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-simple-status",
|
|
3
|
-
"version": "1.4.
|
|
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');
|
|
@@ -86,6 +87,8 @@ const CONFIG_FILE = join(homedir(), '.config', 'claude-simple-status.json');
|
|
|
86
87
|
const userConfig = readJsonFile(CONFIG_FILE) || {};
|
|
87
88
|
const SHOW_CONTEXT_VELOCITY = userConfig.contextVelocity === true;
|
|
88
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);
|
|
89
92
|
|
|
90
93
|
// Clean up stale lock (older than 30s)
|
|
91
94
|
function cleanStaleLock() {
|
|
@@ -114,6 +117,7 @@ function refreshInBackground(token) {
|
|
|
114
117
|
const CACHE_FILE = ${JSON.stringify(CACHE_FILE)};
|
|
115
118
|
const LOCK_DIR = ${JSON.stringify(LOCK_DIR)};
|
|
116
119
|
const ERROR_FILE = ${JSON.stringify(ERROR_FILE)};
|
|
120
|
+
const BACKOFF_FILE = ${JSON.stringify(BACKOFF_FILE)};
|
|
117
121
|
const LOG_FILE = ${JSON.stringify(LOG_FILE)};
|
|
118
122
|
|
|
119
123
|
function logError(msg) {
|
|
@@ -131,10 +135,10 @@ function refreshInBackground(token) {
|
|
|
131
135
|
path: '/api/oauth/usage',
|
|
132
136
|
method: 'GET',
|
|
133
137
|
headers: {
|
|
134
|
-
'Authorization': 'Bearer
|
|
138
|
+
'Authorization': 'Bearer ' + process.env._CLAUDE_SS_TOKEN,
|
|
135
139
|
'anthropic-beta': 'oauth-2025-04-20',
|
|
136
140
|
'Accept': 'application/json',
|
|
137
|
-
'User-Agent': 'claude-
|
|
141
|
+
'User-Agent': 'claude-simple-status/1.5.0'
|
|
138
142
|
},
|
|
139
143
|
timeout: 10000
|
|
140
144
|
}, (res) => {
|
|
@@ -146,9 +150,14 @@ function refreshInBackground(token) {
|
|
|
146
150
|
JSON.parse(data);
|
|
147
151
|
writeFileSync(CACHE_FILE, data);
|
|
148
152
|
try { writeFileSync(ERROR_FILE, ''); } catch {}
|
|
153
|
+
try { writeFileSync(BACKOFF_FILE, '{}'); } catch {}
|
|
149
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)');
|
|
150
160
|
} else if (res.statusCode !== 401) {
|
|
151
|
-
// Skip 401 - token not ready yet at startup, will retry next cycle
|
|
152
161
|
logError('HTTP ' + res.statusCode);
|
|
153
162
|
}
|
|
154
163
|
try { rmdirSync(LOCK_DIR); } catch {}
|
|
@@ -160,7 +169,8 @@ function refreshInBackground(token) {
|
|
|
160
169
|
`
|
|
161
170
|
], {
|
|
162
171
|
detached: true,
|
|
163
|
-
stdio: 'ignore'
|
|
172
|
+
stdio: 'ignore',
|
|
173
|
+
env: { _CLAUDE_SS_TOKEN: token }
|
|
164
174
|
});
|
|
165
175
|
child.unref();
|
|
166
176
|
}
|
|
@@ -351,6 +361,25 @@ async function main() {
|
|
|
351
361
|
}
|
|
352
362
|
} catch {}
|
|
353
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
|
+
|
|
354
383
|
// Get OAuth token
|
|
355
384
|
let token = null;
|
|
356
385
|
const creds = readJsonFile(CREDS_FILE);
|
|
@@ -366,8 +395,10 @@ async function main() {
|
|
|
366
395
|
cleanStaleLock();
|
|
367
396
|
const cacheAge = getFileAge(CACHE_FILE);
|
|
368
397
|
const needRefresh = !quotaData || cacheAge >= CACHE_MAX_AGE;
|
|
398
|
+
const backoffUntil = Number(readJsonFile(BACKOFF_FILE)?.until || 0);
|
|
399
|
+
const inBackoff = Date.now() < backoffUntil;
|
|
369
400
|
|
|
370
|
-
if (needRefresh && acquireLock()) {
|
|
401
|
+
if (needRefresh && !inBackoff && acquireLock()) {
|
|
371
402
|
refreshInBackground(token);
|
|
372
403
|
}
|
|
373
404
|
}
|
|
@@ -458,4 +489,12 @@ async function main() {
|
|
|
458
489
|
process.stdout.write(output);
|
|
459
490
|
}
|
|
460
491
|
|
|
461
|
-
|
|
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 };
|