From 48a6734ec38a29afbddf20c04a5528b585960af2 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Tue, 17 Mar 2026 23:06:28 -0400 Subject: [PATCH] feat: Claude CLI OAuth token expiry warning New GET /auth/status endpoint reads ~/.claude/.credentials.json and returns hours remaining + warning flag. UI shows a dismissible amber banner when < 24h remain, turning red if expired. Checked on page load and every 30 minutes. Co-Authored-By: Claude Sonnet 4.6 --- cortex/main.py | 3 ++- cortex/routers/auth.py | 37 +++++++++++++++++++++++++++++++++++++ cortex/static/app.js | 27 +++++++++++++++++++++++++++ cortex/static/index.html | 6 ++++++ cortex/static/style.css | 35 +++++++++++++++++++++++++++++++++++ inara/HELP.md | 2 +- 6 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 cortex/routers/auth.py diff --git a/cortex/main.py b/cortex/main.py index 782bd49..4958f9b 100644 --- a/cortex/main.py +++ b/cortex/main.py @@ -8,7 +8,7 @@ import uvicorn logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s: %(message)s") from config import settings -from routers import chat, google_chat, nextcloud_talk, files, distill +from routers import chat, google_chat, nextcloud_talk, files, distill, auth @asynccontextmanager @@ -28,6 +28,7 @@ app.include_router(google_chat.router) app.include_router(nextcloud_talk.router) app.include_router(files.router) app.include_router(distill.router) +app.include_router(auth.router) app.mount("/static", StaticFiles(directory="static"), name="static") diff --git a/cortex/routers/auth.py b/cortex/routers/auth.py new file mode 100644 index 0000000..8a06b19 --- /dev/null +++ b/cortex/routers/auth.py @@ -0,0 +1,37 @@ +""" +Claude CLI OAuth token status. + +GET /auth/status — returns expiry info; warns when < WARN_HOURS remain +""" +import json +import logging +from datetime import datetime, timezone +from pathlib import Path +from fastapi import APIRouter + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/auth") + +CREDENTIALS_PATH = Path.home() / ".claude" / ".credentials.json" +WARN_HOURS = 24 # show warning banner when fewer than this many hours remain + + +@router.get("/status") +async def auth_status() -> dict: + try: + data = json.loads(CREDENTIALS_PATH.read_text()) + oauth = data["claudeAiOauth"] + expires_at_ms = oauth["expiresAt"] + expires_dt = datetime.fromtimestamp(expires_at_ms / 1000, tz=timezone.utc) + now = datetime.now(tz=timezone.utc) + hours_remaining = (expires_dt - now).total_seconds() / 3600 + return { + "ok": True, + "expires_at": expires_dt.isoformat(), + "hours_remaining": round(hours_remaining, 1), + "warning": hours_remaining < WARN_HOURS, + "expired": hours_remaining <= 0, + } + except Exception as e: + logger.warning("auth status check failed: %s", e) + return {"ok": False, "error": str(e), "warning": True, "expired": False} diff --git a/cortex/static/app.js b/cortex/static/app.js index 69644b1..5755156 100644 --- a/cortex/static/app.js +++ b/cortex/static/app.js @@ -905,3 +905,30 @@ helpModal.addEventListener('click', (e) => { if (e.target === helpModal) helpModal.classList.remove('open'); }); + + // ── Auth token warning banner ───────────────────────────── + const authBanner = document.getElementById('auth-banner'); + const authBannerMsg = document.getElementById('auth-banner-msg'); + const authBannerClose = document.getElementById('auth-banner-close'); + + async function checkAuthStatus() { + try { + const res = await fetch('/auth/status'); + if (!res.ok) return; + const d = await res.json(); + if (!d.warning) return; + + const msg = d.expired + ? '✕ Claude CLI token has expired — run `claude` in a terminal to re-authenticate' + : `⚠ Claude CLI token expires in ${d.hours_remaining}h — run \`claude\` in a terminal to re-authenticate`; + authBannerMsg.textContent = msg; + authBanner.classList.toggle('expired', !!d.expired); + authBanner.classList.add('show'); + } catch { /* silently ignore — don't break the UI */ } + } + + authBannerClose.addEventListener('click', () => authBanner.classList.remove('show')); + + checkAuthStatus(); + // Re-check every 30 minutes + setInterval(checkAuthStatus, 30 * 60 * 1000); diff --git a/cortex/static/index.html b/cortex/static/index.html index c0677c1..62d55e0 100644 --- a/cortex/static/index.html +++ b/cortex/static/index.html @@ -108,6 +108,12 @@ + +
+ + +
+
diff --git a/cortex/static/style.css b/cortex/static/style.css index 9e77831..30f44a1 100644 --- a/cortex/static/style.css +++ b/cortex/static/style.css @@ -898,6 +898,41 @@ font-weight: 600; } + /* ── Auth warning banner ─────────────────────────────────── */ + #auth-banner { + display: none; + align-items: center; + gap: 10px; + padding: 8px 20px; + background: rgba(160, 100, 0, 0.18); + border-bottom: 1px solid rgba(200, 140, 20, 0.45); + font-size: 0.82rem; + color: #c9a84c; + } + + #auth-banner.show { display: flex; } + #auth-banner.expired { + background: rgba(120, 20, 20, 0.25); + border-color: rgba(200, 60, 60, 0.45); + color: var(--error-text); + } + + #auth-banner-msg { flex: 1; } + + #auth-banner-close { + background: none; + border: 1px solid currentColor; + border-radius: 4px; + color: inherit; + font-size: 0.7rem; + padding: 2px 7px; + cursor: pointer; + opacity: 0.7; + flex-shrink: 0; + } + + #auth-banner-close:hover { opacity: 1; } + /* ── Mobile responsive ───────────────────────────────────── */ @media (max-width: 520px) { header { padding: 8px 12px; gap: 8px; } diff --git a/inara/HELP.md b/inara/HELP.md index 14fa1a2..de6a4cb 100644 --- a/inara/HELP.md +++ b/inara/HELP.md @@ -206,7 +206,7 @@ Chat request body (`POST /chat`): - **Auto memory distillation (long)** — short and mid run automatically; long-term integration is off by default (set `AUTO_DISTILL_LONG=true` in `.env` to enable) - **Ollama local model backend** — direct Ollama API support (no CLI wrapper) -- **OAuth token auto-refresh notifications** — alert when Claude CLI token is near expiry +- **OAuth token auto-refresh notifications** — ✓ implemented: amber banner shown when Claude CLI token is within 24h of expiry (check `GET /auth/status`) - **Multi-user support** — per-user identity/memory files; currently single-user (Scott) ---