From fe6561bf6a1f32f6ae72d0821ab299e0ed5dd365 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Tue, 17 Mar 2026 23:29:25 -0400 Subject: [PATCH] feat: add Gemini auth check to token warning banner /auth/status now returns per-backend status: Claude warns on <24h expiry, Gemini warns only when oauth_creds.json is missing or has no refresh_token (access token rotates automatically so expiry_date is not a useful signal). Banner shows warnings for both backends when needed, and the hint text names the specific CLI commands to run. Co-Authored-By: Claude Sonnet 4.6 --- cortex/routers/auth.py | 52 ++++++++++++++++++++++++++++++++-------- cortex/static/app.js | 36 +++++++++++++++++++++++----- cortex/static/index.html | 2 +- 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/cortex/routers/auth.py b/cortex/routers/auth.py index 8a06b19..9f51380 100644 --- a/cortex/routers/auth.py +++ b/cortex/routers/auth.py @@ -1,7 +1,12 @@ """ -Claude CLI OAuth token status. +CLI auth status for both Claude and Gemini backends. -GET /auth/status — returns expiry info; warns when < WARN_HOURS remain +GET /auth/status — returns per-backend auth info and warning flags + +Claude: warns when OAuth token is < WARN_HOURS from expiry (requires + user to re-run `claude` to refresh via browser flow). +Gemini: warns only when oauth_creds.json is missing or has no + refresh_token (access token rotates automatically every ~1h). """ import json import logging @@ -12,17 +17,17 @@ 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 +CLAUDE_CREDS = Path.home() / ".claude" / ".credentials.json" +GEMINI_CREDS = Path.home() / ".gemini" / "oauth_creds.json" +GEMINI_ACCTS = Path.home() / ".gemini" / "google_accounts.json" +WARN_HOURS = 24 -@router.get("/status") -async def auth_status() -> dict: +def _claude_status() -> dict: try: - data = json.loads(CREDENTIALS_PATH.read_text()) + data = json.loads(CLAUDE_CREDS.read_text()) oauth = data["claudeAiOauth"] - expires_at_ms = oauth["expiresAt"] - expires_dt = datetime.fromtimestamp(expires_at_ms / 1000, tz=timezone.utc) + expires_dt = datetime.fromtimestamp(oauth["expiresAt"] / 1000, tz=timezone.utc) now = datetime.now(tz=timezone.utc) hours_remaining = (expires_dt - now).total_seconds() / 3600 return { @@ -33,5 +38,32 @@ async def auth_status() -> dict: "expired": hours_remaining <= 0, } except Exception as e: - logger.warning("auth status check failed: %s", e) + logger.warning("claude auth check failed: %s", e) return {"ok": False, "error": str(e), "warning": True, "expired": False} + + +def _gemini_status() -> dict: + try: + creds = json.loads(GEMINI_CREDS.read_text()) + if not creds.get("refresh_token"): + return {"ok": True, "authenticated": False, "warning": True, "account": None} + account = None + try: + accts = json.loads(GEMINI_ACCTS.read_text()) + account = accts.get("active") + except Exception: + pass + return {"ok": True, "authenticated": True, "warning": False, "account": account} + except FileNotFoundError: + return {"ok": True, "authenticated": False, "warning": True, "account": None} + except Exception as e: + logger.warning("gemini auth check failed: %s", e) + return {"ok": False, "error": str(e), "warning": True, "authenticated": False} + + +@router.get("/status") +async def auth_status() -> dict: + return { + "claude": _claude_status(), + "gemini": _gemini_status(), + } diff --git a/cortex/static/app.js b/cortex/static/app.js index 14d17eb..1fa49af 100644 --- a/cortex/static/app.js +++ b/cortex/static/app.js @@ -967,6 +967,7 @@ // ── Auth token warning banner ───────────────────────────── const authBanner = document.getElementById('auth-banner'); const authBannerMsg = document.getElementById('auth-banner-msg'); + const authBannerHint = document.getElementById('auth-banner-hint'); const authBannerClose = document.getElementById('auth-banner-close'); async function checkAuthStatus() { @@ -974,13 +975,36 @@ 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' - : `⚠ Claude CLI token expires in ${d.hours_remaining}h`; - authBannerMsg.textContent = msg; - authBanner.classList.toggle('expired', !!d.expired); + const warnings = []; + const fixes = []; + let anyExpired = false; + + if (d.claude?.warning) { + if (d.claude.expired) { + warnings.push('✕ Claude CLI token has expired'); + anyExpired = true; + } else { + warnings.push(`⚠ Claude CLI token expires in ${d.claude.hours_remaining}h`); + } + fixes.push('claude'); + } + + if (d.gemini?.warning) { + warnings.push('⚠ Gemini CLI not authenticated'); + fixes.push('gemini'); + } + + if (!warnings.length) { + authBanner.classList.remove('show'); + return; + } + + authBannerMsg.innerHTML = warnings.join('
'); + authBannerHint.innerHTML = + `To fix: SSH into the Cortex host and run ${fixes.join(' and/or ')} — ` + + 'follow the login prompt, then restart Cortex.'; + authBanner.classList.toggle('expired', anyExpired); authBanner.classList.add('show'); } catch { /* silently ignore — don't break the UI */ } } diff --git a/cortex/static/index.html b/cortex/static/index.html index 1e47385..8a9a8a6 100644 --- a/cortex/static/index.html +++ b/cortex/static/index.html @@ -113,7 +113,7 @@
- To fix: SSH into the Cortex host and run claude — follow the login prompt, then restart Cortex. +