- Add cortex/tools/scratch.py with scratch_read/write/append/clear tools - Register all four scratch tools in the orchestrator tool registry - Create inara/SCRATCH.md as the backing file (never distilled/archived) - Fix auth.py: expiresAt reflects short-lived access token (~8h) not the 1-year refresh token — suppress expiry warning when refreshToken is present Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
77 lines
2.9 KiB
Python
77 lines
2.9 KiB
Python
"""
|
|
CLI auth status for both Claude and Gemini backends.
|
|
|
|
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
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from fastapi import APIRouter
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/auth")
|
|
|
|
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
|
|
|
|
|
|
def _claude_status() -> dict:
|
|
try:
|
|
data = json.loads(CLAUDE_CREDS.read_text())
|
|
oauth = data["claudeAiOauth"]
|
|
has_refresh = bool(oauth.get("refreshToken"))
|
|
expires_dt = datetime.fromtimestamp(oauth["expiresAt"] / 1000, tz=timezone.utc)
|
|
now = datetime.now(tz=timezone.utc)
|
|
hours_remaining = (expires_dt - now).total_seconds() / 3600
|
|
# If a refresh token is present the session is long-lived (~1 year).
|
|
# expiresAt only reflects the current access token window (~8 h) and
|
|
# rotates automatically — do not warn based on it when a refresh token exists.
|
|
warning = not has_refresh and hours_remaining < WARN_HOURS
|
|
expired = hours_remaining <= 0 and not has_refresh
|
|
return {
|
|
"ok": True,
|
|
"has_refresh_token": has_refresh,
|
|
"access_token_expires_at": expires_dt.isoformat(),
|
|
"access_token_hours_remaining": round(hours_remaining, 1),
|
|
"warning": warning,
|
|
"expired": expired,
|
|
}
|
|
except Exception as 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(),
|
|
}
|