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 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.0",
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
- const CACHE_MAX_AGE = 120; // seconds - when to fetch
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 ${token}',
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-code/2.1.12'
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
- main().catch(() => process.exit(1));
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 };