feat: Add authentication error handling to UI flow

Extend auth error detection to the web UI flow:

server/main.py:
- Fix setup_status() endpoint to check ~/.claude directory instead of
  non-existent .credentials.json file
- Add explanatory comments about Claude CLI credential storage changes

server/services/process_manager.py:
- Add AUTH_ERROR_PATTERNS for detecting auth errors in agent output
- Add is_auth_error() helper function
- Add AUTH_ERROR_HELP message template
- Update _stream_output() to detect auth errors in real-time
- Buffer last 20 lines to catch auth errors on process exit
- Broadcast clear help message to WebSocket clients when auth fails

start_ui.sh:
- Add Claude CLI installation check with helpful guidance
- Add ~/.claude directory check with login reminder
- Non-blocking warnings that don't prevent UI from starting

This ensures users get clear, actionable feedback when authentication
fails, whether using the CLI or the web UI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
mantarayDigital
2026-01-08 07:37:04 +02:00
parent 780cfd343f
commit b2c19b0c4c
3 changed files with 88 additions and 3 deletions

View File

@@ -120,9 +120,11 @@ async def setup_status():
# Check for Claude CLI
claude_cli = shutil.which("claude") is not None
# Check for credentials file
credentials_path = Path.home() / ".claude" / ".credentials.json"
credentials = credentials_path.exists()
# Check for Claude CLI configuration directory
# Note: Claude CLI no longer stores credentials in ~/.claude/.credentials.json
# The existence of ~/.claude indicates the CLI has been configured
claude_dir = Path.home() / ".claude"
credentials = claude_dir.exists() and claude_dir.is_dir()
# Check for Node.js and npm
node = shutil.which("node") is not None

View File

@@ -36,6 +36,47 @@ SENSITIVE_PATTERNS = [
r'aws[_-]?secret[=:][^\s]+',
]
# Patterns that indicate Claude CLI authentication errors
AUTH_ERROR_PATTERNS = [
r"not\s+logged\s+in",
r"not\s+authenticated",
r"authentication\s+(failed|required|error)",
r"login\s+required",
r"please\s+(run\s+)?['\"]?claude\s+login",
r"unauthorized",
r"invalid\s+(token|credential|api.?key)",
r"expired\s+(token|session|credential)",
r"could\s+not\s+authenticate",
r"sign\s+in\s+(to|required)",
]
def is_auth_error(text: str) -> bool:
"""Check if text contains Claude CLI authentication error messages."""
if not text:
return False
text_lower = text.lower()
for pattern in AUTH_ERROR_PATTERNS:
if re.search(pattern, text_lower):
return True
return False
AUTH_ERROR_HELP = """
================================================================================
AUTHENTICATION ERROR DETECTED
================================================================================
Claude CLI requires authentication to work.
To fix this, run:
claude login
This will open a browser window to sign in.
After logging in, try starting the agent again.
================================================================================
"""
def sanitize_output(line: str) -> str:
"""Remove sensitive information from output lines."""
@@ -185,6 +226,9 @@ class AgentProcessManager:
if not self.process or not self.process.stdout:
return
auth_error_detected = False
output_buffer = [] # Buffer recent lines for auth error detection
try:
loop = asyncio.get_running_loop()
while True:
@@ -198,6 +242,18 @@ class AgentProcessManager:
decoded = line.decode("utf-8", errors="replace").rstrip()
sanitized = sanitize_output(decoded)
# Buffer recent output for auth error detection
output_buffer.append(decoded)
if len(output_buffer) > 20:
output_buffer.pop(0)
# Check for auth errors
if not auth_error_detected and is_auth_error(decoded):
auth_error_detected = True
# Broadcast auth error help message
for help_line in AUTH_ERROR_HELP.strip().split('\n'):
await self._broadcast_output(help_line)
await self._broadcast_output(sanitized)
except asyncio.CancelledError:
@@ -209,6 +265,12 @@ class AgentProcessManager:
if self.process and self.process.poll() is not None:
exit_code = self.process.returncode
if exit_code != 0 and self.status == "running":
# Check buffered output for auth errors if we haven't detected one yet
if not auth_error_detected:
combined_output = '\n'.join(output_buffer)
if is_auth_error(combined_output):
for help_line in AUTH_ERROR_HELP.strip().split('\n'):
await self._broadcast_output(help_line)
self.status = "crashed"
elif self.status == "running":
self.status = "stopped"