claude-git-hooks
Version:
Git hooks with Claude CLI for code analysis and automatic commit messages
358 lines (312 loc) • 11.6 kB
JavaScript
/**
* 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;
};