From 7b75630e67d330fdc6f896071d8b4fc3f4544219 Mon Sep 17 00:00:00 2001 From: Axel Meyer Date: Sat, 14 Feb 2026 15:49:39 +0000 Subject: [PATCH] Show session expiry warning in statusline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the fetcher gets a 401/403, it writes an error state to the cache file instead of silently failing. The statusline reads this and shows "Token: session expired — refresh cookie" so the user knows to re-extract the sessionKey cookie from claude.ai. Co-Authored-By: Claude Opus 4.6 --- fetch-usage.js | 26 ++++++++++++++++++-------- statusline.js | 11 ++++++++++- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/fetch-usage.js b/fetch-usage.js index 423b974..c62facd 100755 --- a/fetch-usage.js +++ b/fetch-usage.js @@ -30,6 +30,13 @@ const CACHE_FILE = process.env.CLAUDE_USAGE_CACHE || path.join(process.env.TMPDIR || '/tmp', 'claude_usage.json'); const ORG_ID = process.env.CLAUDE_ORG_ID || ''; +// --- Cache writer --- +function writeCache(data) { + const cacheDir = path.dirname(CACHE_FILE); + if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true }); + fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2)); +} + // --- Session Key --- function getSessionKey() { // env var first @@ -52,7 +59,12 @@ async function getOrgId(sessionKey) { headers: headers(sessionKey), signal: AbortSignal.timeout(10000), }); - if (!resp.ok) throw new Error('Failed to list orgs: ' + resp.status); + if (!resp.ok) { + if (resp.status === 401 || resp.status === 403) { + writeCache({ _error: 'auth_expired', _status: resp.status }); + } + throw new Error('Failed to list orgs: ' + resp.status); + } const orgs = await resp.json(); if (!orgs.length) throw new Error('No organizations found'); return orgs[0].uuid; @@ -85,19 +97,17 @@ async function main() { }); if (!resp.ok) { + writeCache({ _error: resp.status === 401 || resp.status === 403 + ? 'auth_expired' : 'api_error', _status: resp.status }); console.error('API error: ' + resp.status); process.exit(1); } const data = await resp.json(); - - // Ensure cache dir exists - const cacheDir = path.dirname(CACHE_FILE); - if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true }); - - fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2)); - console.log('OK — wrote ' + CACHE_FILE); + writeCache(data); + console.log('OK \u2014 wrote ' + CACHE_FILE); } catch (e) { + writeCache({ _error: 'fetch_failed', _message: e.message }); console.error('Fetch error: ' + e.message); process.exit(1); } diff --git a/statusline.js b/statusline.js index 2c889f5..c0936e9 100755 --- a/statusline.js +++ b/statusline.js @@ -41,8 +41,12 @@ function readCache() { if (!fs.existsSync(CACHE_FILE)) return null; const stat = fs.statSync(CACHE_FILE); const ageS = (Date.now() - stat.mtimeMs) / 1000; + const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); + // Error state written by fetcher — always show regardless of age + if (data._error) return data; + // Normal data — respect max age if (ageS > CACHE_MAX_AGE_S) return null; - return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); + return data; } catch { return null; } @@ -60,6 +64,11 @@ function formatMinutes(isoStr) { function getUsagePart(data) { if (!data) return ''; + + // Error state from fetcher + if (data._error === 'auth_expired') return 'Token: session expired — refresh cookie'; + if (data._error) return 'Token: fetch error'; + const parts = []; if (data.five_hour && data.five_hour.utilization > 0) {