diff --git a/backend/main.py b/backend/main.py index 73c6a23..6201970 100644 --- a/backend/main.py +++ b/backend/main.py @@ -487,48 +487,62 @@ _auth_status = "unknown" # unknown | logged_in | logged_out | pending async def _check_claude_auth() -> str: - """Check if claude is authenticated by running a quick no-op""" + """Check if claude is authenticated by running claude auth status""" global _auth_status try: proc = await asyncio.create_subprocess_exec( - "claude", "--version", + "claude", "auth", "status", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) - await asyncio.wait_for(proc.communicate(), timeout=5) - # Check for saved credentials file - creds = Path("/root/.claude/.credentials.json") - if creds.exists(): + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=10) + output = stdout.decode(errors="replace").strip() + logger.info(f"claude auth status output: {output}") + try: import json as _json - data = _json.loads(creds.read_text()) - if data: + data = _json.loads(output) + if data.get("loggedIn"): _auth_status = "logged_in" return "logged_in" + except Exception: + pass _auth_status = "logged_out" return "logged_out" - except Exception: + except Exception as e: + logger.error(f"Auth check failed: {e}") _auth_status = "unknown" return "unknown" @app.get("/api/auth/status") async def auth_status(): - """Check Claude auth status""" + """Check Claude auth status using claude auth status JSON""" global _auth_status, _auth_url - status = await _check_claude_auth() - # Try to get account info if logged in account = None + auth_method = None try: - creds_file = Path("/root/.claude/.credentials.json") - if creds_file.exists(): - import json as _json - creds = _json.loads(creds_file.read_text()) - account = creds.get("account", {}).get("emailAddress") or creds.get("email") - except Exception: + proc = await asyncio.create_subprocess_exec( + "claude", "auth", "status", + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=10) + output = stdout.decode(errors="replace").strip() + import json as _json + data = _json.loads(output) + if data.get("loggedIn"): + _auth_status = "logged_in" + account = data.get("email") or data.get("account", {}).get("emailAddress") + auth_method = data.get("authMethod") + else: + _auth_status = "logged_out" + except Exception as e: + logger.error(f"Auth status check failed: {e}") + # Fall back to existing status pass return { - "status": status, + "status": _auth_status, "account": account, - "auth_url": _auth_url if status == "pending" else None + "auth_method": auth_method, + "auth_url": _auth_url if _auth_status == "pending" else None } @@ -540,41 +554,56 @@ async def auth_login(): _auth_url = None try: - # Run claude auth login and capture the URL it prints + # Run claude auth login --claudeai and capture the OAuth URL proc = await asyncio.create_subprocess_exec( - "claude", "auth", "login", + "claude", "auth", "login", "--claudeai", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, stdin=asyncio.subprocess.DEVNULL ) _auth_process = proc - # Read output lines looking for the OAuth URL (30s timeout) + # Read output looking for the OAuth URL url = None try: async def _read_url(): + collected = "" while True: line = await proc.stdout.readline() if not line: break text = line.decode(errors="replace") + collected += text logger.info(f"claude auth: {text.strip()}") - # Claude prints something like: "Open this URL: https://..." + # The URL contains /oauth/authorize and starts with https:// for part in text.split(): - if part.startswith("https://claude.ai") or part.startswith("https://anthropic"): - return part.strip() + cleaned = part.strip().rstrip("\\n") + if cleaned.startswith("https://") and "oauth" in cleaned: + return cleaned + if cleaned.startswith("https://claude.com"): + return cleaned + if cleaned.startswith("https://console.anthropic.com"): + return cleaned + logger.info(f"Full auth output: {collected}") return None - url = await asyncio.wait_for(_read_url(), timeout=30) + url = await asyncio.wait_for(_read_url(), timeout=15) except asyncio.TimeoutError: - pass + logger.warning("Timed out waiting for auth URL") + + # Don't wait for the process to finish — it blocks waiting for browser callback + # It will complete in the background when user finishes OAuth in browser if url: _auth_url = url - return {"status": "pending", "auth_url": url, "message": "Open this URL in your browser to log in"} + return {"status": "pending", "auth_url": url, "message": "Open this URL in your browser to log in with Claude Max"} else: - return {"status": "error", "message": "Could not extract login URL from claude CLI. Try running 'claude auth login' manually in the container."} + return { + "status": "error", + "message": "Could not extract login URL. Try running manually: docker exec -it claude-persistent-agent claude auth login --claudeai" + } except Exception as e: + logger.error(f"Auth login failed: {e}") return {"status": "error", "message": str(e)}