UNPKG

claude-git-hooks

Version:

Git hooks with Claude CLI for code analysis and automatic commit messages

358 lines (312 loc) 11.6 kB
/** * File: claude-diagnostics.js * Purpose: Reusable Claude CLI error diagnostics and formatting * * Key features: * - Detects common Claude CLI error patterns * - Provides actionable remediation steps * - Extensible for future error types * * Usage: * import { detectClaudeError, formatClaudeError } from './claude-diagnostics.js'; * * const errorInfo = detectClaudeError(stdout, stderr, exitCode); * if (errorInfo) { * console.error(formatClaudeError(errorInfo)); * } */ /** * Error types that can be detected */ export const ClaudeErrorType = { RATE_LIMIT: 'RATE_LIMIT', AUTH_FAILED: 'AUTH_FAILED', TIMEOUT: 'TIMEOUT', NETWORK: 'NETWORK', INVALID_RESPONSE: 'INVALID_RESPONSE', EXECUTION_ERROR: 'EXECUTION_ERROR', GENERIC: 'GENERIC' }; /** * Detects Claude CLI error type and extracts relevant information * Why: Centralized error detection for consistent handling * * Future enhancements: * - Network connectivity errors * - Authentication expiration * - Model availability errors * - Token limit exceeded errors * * @param {string} stdout - Claude CLI stdout * @param {string} stderr - Claude CLI stderr * @param {number} exitCode - Process exit code * @returns {Object|null} Error information or null if no specific error detected */ export const detectClaudeError = (stdout = '', stderr = '', exitCode = 1) => { // 1. Execution error detection (Claude CLI internal error) // This occurs when Claude API returns an error instead of valid response // Often caused by rate limiting, API errors, or temporary backend issues // IMPORTANT: Only match EXACT "Execution error" response, not partial matches const trimmedStdout = stdout.trim(); if (trimmedStdout === 'Execution error') { return { type: ClaudeErrorType.EXECUTION_ERROR, exitCode, stdout: stdout.substring(0, 100) }; } // 2. Rate limit detection const rateLimitMatch = stdout.match(/Claude AI usage limit reached\|(\d+)/); if (rateLimitMatch) { const resetTimestamp = parseInt(rateLimitMatch[1], 10); const resetDate = new Date(resetTimestamp * 1000); const now = new Date(); const minutesUntilReset = Math.ceil((resetDate - now) / 60000); return { type: ClaudeErrorType.RATE_LIMIT, exitCode, resetTimestamp, resetDate, minutesUntilReset }; } // 3. Authentication failure detection if (stdout.includes('not authenticated') || stderr.includes('not authenticated') || stdout.includes('authentication failed') || stderr.includes('authentication failed')) { return { type: ClaudeErrorType.AUTH_FAILED, exitCode }; } // 4. Network errors if (stderr.includes('ENOTFOUND') || stderr.includes('ECONNREFUSED') || stderr.includes('network error') || stderr.includes('connection refused')) { return { type: ClaudeErrorType.NETWORK, exitCode }; } // 5. Invalid response (JSON parsing errors) if (stdout.includes('SyntaxError') || stdout.includes('Unexpected token')) { return { type: ClaudeErrorType.INVALID_RESPONSE, exitCode }; } // 6. Generic error return { type: ClaudeErrorType.GENERIC, exitCode, stdout: stdout.substring(0, 200), // First 200 chars stderr: stderr.substring(0, 200) }; }; /** * Formats timing information for error messages * @param {Object} errorInfo - Error information with optional timing data * @returns {Array<string>} Array of formatted timing lines */ const formatTimingInfo = (errorInfo) => { const lines = []; const { elapsedTime, timeoutValue, retryAttempt, maxRetries } = errorInfo; if (elapsedTime !== undefined || timeoutValue !== undefined || retryAttempt !== undefined) { lines.push(''); lines.push('Timing information:'); if (elapsedTime !== undefined) { const seconds = (elapsedTime / 1000).toFixed(2); lines.push(` Elapsed time: ${seconds}s`); } if (timeoutValue !== undefined) { const timeoutSeconds = (timeoutValue / 1000).toFixed(0); lines.push(` Timeout limit: ${timeoutSeconds}s`); } if (retryAttempt !== undefined) { const maxRetriesText = maxRetries !== undefined ? `/${maxRetries}` : ''; lines.push(` Retry attempt: ${retryAttempt}${maxRetriesText}`); } } return lines; }; /** * Formats Claude error message with diagnostics and remediation steps * Why: Provides consistent, actionable error messages * * @param {Object} errorInfo - Output from detectClaudeError() * @returns {string} Formatted error message */ export const formatClaudeError = (errorInfo) => { const lines = []; switch (errorInfo.type) { case ClaudeErrorType.EXECUTION_ERROR: return formatExecutionError(errorInfo); case ClaudeErrorType.RATE_LIMIT: return formatRateLimitError(errorInfo); case ClaudeErrorType.AUTH_FAILED: return formatAuthError(errorInfo); case ClaudeErrorType.NETWORK: return formatNetworkError(errorInfo); case ClaudeErrorType.INVALID_RESPONSE: return formatInvalidResponseError(errorInfo); case ClaudeErrorType.GENERIC: default: return formatGenericError(errorInfo); } }; /** * Format execution error */ const formatExecutionError = (errorInfo) => { const { exitCode, stdout } = errorInfo; const lines = []; lines.push('❌ Claude CLI execution error'); lines.push(''); lines.push('Claude returned "Execution error" instead of valid response.'); lines.push(...formatTimingInfo(errorInfo)); lines.push(''); lines.push('Common causes:'); lines.push(' 1. API rate limiting (burst limits)'); lines.push(' 2. Temporary Claude API backend issues'); lines.push(' 3. Request/response size limits exceeded'); lines.push(''); lines.push('Solutions:'); lines.push(' 1. Wait 10-30 seconds and try again'); lines.push(' 2. Reduce prompt size by committing fewer files at once'); lines.push(' 3. Switch to haiku model (faster, higher rate limits):'); lines.push(' Edit .claude/config.json:'); lines.push(' { "subagents": { "model": "haiku" } }'); lines.push(' 4. Skip analysis for now:'); lines.push(' git commit --no-verify -m "your message"'); lines.push(''); lines.push('The hook will automatically retry once after 2 seconds.'); return lines.join('\n'); }; /** * Format rate limit error */ const formatRateLimitError = (errorInfo) => { const { resetDate, minutesUntilReset } = errorInfo; const lines = []; lines.push('❌ Claude API usage limit reached'); lines.push(''); lines.push('Rate limit details:'); lines.push(` Reset time: ${resetDate.toLocaleString()}`); if (minutesUntilReset > 60) { const hours = Math.ceil(minutesUntilReset / 60); lines.push(` Time until reset: ~${hours} hour${hours > 1 ? 's' : ''}`); } else if (minutesUntilReset > 0) { lines.push(` Time until reset: ~${minutesUntilReset} minute${minutesUntilReset !== 1 ? 's' : ''}`); } else { lines.push(' Limit should be reset now'); } lines.push(...formatTimingInfo(errorInfo)); lines.push(''); lines.push('Options:'); lines.push(' 1. Wait for rate limit to reset'); lines.push(' 2. Skip analysis for now:'); lines.push(' git commit --no-verify -m "your message"'); lines.push(' 3. Reduce API usage by switching to haiku model:'); lines.push(' Edit .claude/config.json:'); lines.push(' { "subagents": { "model": "haiku" } }'); return lines.join('\n'); }; /** * Format authentication error */ const formatAuthError = (errorInfo) => { const { exitCode } = errorInfo; const lines = []; lines.push('❌ Claude CLI authentication failed'); lines.push(...formatTimingInfo(errorInfo)); lines.push(''); lines.push('Possible causes:'); lines.push(' 1. Not logged in to Claude CLI'); lines.push(' 2. Authentication token expired'); lines.push(' 3. Invalid API credentials'); lines.push(''); lines.push('Solution:'); lines.push(' claude auth login'); lines.push(''); lines.push('Then try your commit again.'); return lines.join('\n'); }; /** * Format network error */ const formatNetworkError = (errorInfo) => { const { exitCode } = errorInfo; const lines = []; lines.push('❌ Network error connecting to Claude API'); lines.push(...formatTimingInfo(errorInfo)); lines.push(''); lines.push('Possible causes:'); lines.push(' 1. No internet connection'); lines.push(' 2. Firewall blocking Claude API'); lines.push(' 3. Claude API temporarily unavailable'); lines.push(''); lines.push('Solutions:'); lines.push(' 1. Check your internet connection'); lines.push(' 2. Verify firewall settings'); lines.push(' 3. Try again in a few moments'); lines.push(' 4. Skip analysis: git commit --no-verify -m "message"'); return lines.join('\n'); }; /** * Format invalid response error */ const formatInvalidResponseError = (errorInfo) => { const { exitCode } = errorInfo; const lines = []; lines.push('❌ Claude returned invalid response'); lines.push(...formatTimingInfo(errorInfo)); lines.push(''); lines.push('This usually means:'); lines.push(' - Claude did not return valid JSON'); lines.push(' - Response format does not match expected schema'); lines.push(''); lines.push('Solutions:'); lines.push(' 1. Check debug output: .claude/out/debug-claude-response.json'); lines.push(' 2. Try again (may be temporary issue)'); lines.push(' 3. Skip analysis: git commit --no-verify -m "message"'); return lines.join('\n'); }; /** * Format generic error */ const formatGenericError = (errorInfo) => { const { exitCode, stdout, stderr } = errorInfo; const lines = []; lines.push('❌ Claude CLI execution failed'); lines.push(''); lines.push(`Exit code: ${exitCode}`); lines.push(...formatTimingInfo(errorInfo)); if (stdout && stdout.trim()) { lines.push(''); lines.push('Output:'); lines.push(` ${stdout.trim()}`); } if (stderr && stderr.trim()) { lines.push(''); lines.push('Error:'); lines.push(` ${stderr.trim()}`); } lines.push(''); lines.push('Solutions:'); lines.push(' 1. Verify Claude CLI is installed: claude --version'); lines.push(' 2. Check authentication: claude auth login'); lines.push(' 3. Enable debug mode in .claude/config.json:'); lines.push(' { "system": { "debug": true } }'); lines.push(' 4. Skip analysis: git commit --no-verify -m "message"'); return lines.join('\n'); }; /** * Checks if Claude CLI error is recoverable * Why: Some errors (rate limit) should wait, others (auth) should fail immediately * * @param {Object} errorInfo - Output from detectClaudeError() * @returns {boolean} True if error might resolve with retry */ export const isRecoverableError = (errorInfo) => { return errorInfo.type === ClaudeErrorType.EXECUTION_ERROR || errorInfo.type === ClaudeErrorType.RATE_LIMIT || errorInfo.type === ClaudeErrorType.NETWORK; };