UNPKG

claude-git-hooks

Version:

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

940 lines (829 loc) 35.2 kB
/** * File: claude-client.js * Purpose: Interface with Claude CLI for code analysis * * Key responsibilities: * - Execute Claude CLI with prompts * - Parse JSON responses from Claude * - Handle errors and timeouts * - Optional debug output * * Dependencies: * - child_process: For executing Claude CLI * - fs/promises: For debug file writing * - logger: Debug and error logging * - claude-diagnostics: Error detection and formatting */ import { spawn, execSync } from 'child_process'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import logger from './logger.js'; import config from '../config.js'; import { detectClaudeError, formatClaudeError, ClaudeErrorType, isRecoverableError } from './claude-diagnostics.js'; import { which } from './which-command.js'; import { recordJsonParseFailure, recordBatchSuccess, rotateTelemetry } from './telemetry.js'; /** * Custom error for Claude client failures */ class ClaudeClientError extends Error { constructor(message, { cause, output, context } = {}) { super(message); this.name = 'ClaudeClientError'; this.cause = cause; this.output = output; this.context = context; } } /** * Detect if running on Windows * Why: Need to use 'wsl claude' instead of 'claude' on Windows */ const isWindows = () => os.platform() === 'win32' || process.env.OS === 'Windows_NT'; /** * Check if WSL is available on Windows * Why: Windows users need WSL to run Claude CLI, verify it exists before attempting * * @returns {boolean} True if WSL is available */ const isWSLAvailable = () => { if (!isWindows()) { return true; // Not Windows, WSL check not needed } try { // Try to run wsl --version to check if WSL is installed execSync('wsl --version', { stdio: 'ignore', timeout: config.system.wslCheckTimeout }); return true; } catch (error) { logger.debug('claude-client - isWSLAvailable', 'WSL not available', error); return false; } }; /** * Get Claude command configuration for current platform * Why: On Windows, try native Claude first, then WSL as fallback * Node 24 Fix: Uses which() to resolve absolute paths, avoiding shell: true * * @returns {Object} { command, args } - Command and base arguments * @throws {ClaudeClientError} If Claude not available on any method */ const getClaudeCommand = () => { if (isWindows()) { // Try native Windows Claude first (e.g., installed via npm/scoop/choco) // Node 24: Use which() instead of execSync to get absolute path const nativePath = which('claude'); if (nativePath) { logger.debug('claude-client - getClaudeCommand', 'Using native Windows Claude CLI', { path: nativePath }); return { command: nativePath, args: [] }; } logger.debug('claude-client - getClaudeCommand', 'Native Claude not found, trying WSL'); // Fallback to WSL if (!isWSLAvailable()) { throw new ClaudeClientError('Claude CLI not found. Install Claude CLI natively on Windows or via WSL', { context: { platform: 'Windows', suggestions: [ 'Native Windows: npm install -g @anthropic-ai/claude-cli', 'WSL: wsl --install, then install Claude in WSL' ] } }); } // Check if Claude is available in WSL // Node 24: Resolve wsl.exe absolute path to avoid shell: true const wslPath = which('wsl'); if (wslPath) { try { // Verify Claude exists in WSL // Increased timeout from 5s to 15s to handle system load better const wslCheckTimeout = config.system.wslCheckTimeout || 15000; execSync(`"${wslPath}" claude --version`, { stdio: 'ignore', timeout: wslCheckTimeout }); logger.debug('claude-client - getClaudeCommand', 'Using WSL Claude CLI', { wslPath }); return { command: wslPath, args: ['claude'] }; } catch (wslError) { // Differentiate error types for accurate user feedback const errorMsg = wslError.message || ''; const wslCheckTimeout = config.system.wslCheckTimeout || 15000; // Timeout: Transient system load issue if (errorMsg.includes('ETIMEDOUT')) { throw new ClaudeClientError('Timeout connecting to WSL - system under heavy load', { context: { platform: 'Windows', wslPath, error: 'ETIMEDOUT', timeoutValue: wslCheckTimeout, suggestion: 'System is busy. Wait a moment and try again, or skip analysis: git commit --no-verify' } }); } // Not found: Claude CLI missing in WSL if (errorMsg.includes('ENOENT') || errorMsg.includes('command not found')) { throw new ClaudeClientError('Claude CLI not found in WSL', { context: { platform: 'Windows', wslPath, error: 'ENOENT', suggestion: 'Install Claude in WSL: wsl -e bash -c "npm install -g @anthropic-ai/claude-cli"' } }); } // Generic error: Other WSL issues throw new ClaudeClientError('Failed to verify Claude CLI in WSL', { context: { platform: 'Windows', wslPath, error: errorMsg, suggestion: 'Check WSL is functioning: wsl --version, or skip analysis: git commit --no-verify' } }); } } throw new ClaudeClientError('Claude CLI not found in Windows or WSL', { context: { platform: 'Windows', suggestions: [ 'Install Claude CLI: npm install -g @anthropic-ai/claude-cli', 'Or install WSL and Claude in WSL' ] } }); } // Unix (Linux, macOS): Use which() to get absolute path const claudePath = which('claude'); if (claudePath) { logger.debug('claude-client - getClaudeCommand', 'Using Claude CLI', { path: claudePath }); return { command: claudePath, args: [] }; } // Fallback to 'claude' if which fails (will error later if not found) logger.debug('claude-client - getClaudeCommand', 'which() failed, using fallback', { command: 'claude' }); return { command: 'claude', args: [] }; }; /** * Executes Claude CLI with a prompt * Why: Centralized Claude CLI execution with error handling and timeout * Why platform detection: On Windows, must use 'wsl claude' to access Claude in WSL * * @param {string} prompt - Prompt text to send to Claude * @param {Object} options - Execution options * @param {number} options.timeout - Timeout in milliseconds (default: 120000 = 2 minutes) * @returns {Promise<string>} Claude's response * @throws {ClaudeClientError} If execution fails or times out */ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) => new Promise((resolve, reject) => { // Get platform-specific command const { command, args } = getClaudeCommand(); // Add allowed tools if specified (for MCP tools) const finalArgs = [...args]; if (allowedTools.length > 0) { // Format: --allowedTools "mcp__github__create_pull_request,mcp__github__get_file_contents" finalArgs.push('--allowedTools', allowedTools.join(',')); } // CRITICAL FIX: Windows .cmd/.bat file handling // Why: spawn() cannot execute .cmd/.bat files directly on Windows (ENOENT error) // Solution: Wrap with cmd.exe /c when command ends with .cmd or .bat // Impact: Only affects Windows npm-installed CLI tools, no impact on other platforms let spawnCommand = command; let spawnArgs = finalArgs; if (isWindows() && (command.endsWith('.cmd') || command.endsWith('.bat'))) { logger.debug('claude-client - executeClaude', 'Wrapping .cmd/.bat with cmd.exe', { originalCommand: command, originalArgs: finalArgs }); spawnCommand = 'cmd.exe'; spawnArgs = ['/c', command, ...finalArgs]; } const fullCommand = spawnArgs.length > 0 ? `${spawnCommand} ${spawnArgs.join(' ')}` : spawnCommand; logger.debug( 'claude-client - executeClaude', 'Executing Claude CLI', { promptLength: prompt.length, timeout, command: fullCommand, isWindows: isWindows(), allowedTools } ); const startTime = Date.now(); // Why: Use spawn instead of exec to handle large prompts and responses // spawn streams data, exec buffers everything in memory // Node 24 Fix: Removed shell: true to avoid DEP0190 deprecation warning // We now use absolute paths from which(), so shell is not needed // Windows .cmd/.bat fix: Wrapped with cmd.exe /c (see above) const claude = spawn(spawnCommand, spawnArgs, { stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr }); let stdout = ''; let stderr = ''; // Collect stdout claude.stdout.on('data', (data) => { stdout += data.toString(); }); // Collect stderr claude.stderr.on('data', (data) => { stderr += data.toString(); }); // Handle process completion claude.on('close', (code) => { const elapsedTime = Date.now() - startTime; // Check for "Execution error" even when exit code is 0 // Why: Claude CLI sometimes returns "Execution error" with exit code 0 // This occurs during API rate limiting or temporary backend issues // IMPORTANT: Only check for EXACT match to avoid false positives if (stdout.trim() === 'Execution error') { const errorInfo = detectClaudeError(stdout, stderr, code); logger.error( 'claude-client - executeClaude', `Claude CLI returned execution error (exit ${code})`, new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, { output: { stdout, stderr }, context: { exitCode: code, elapsedTime, timeoutValue: timeout, errorType: errorInfo.type } }) ); // Merge timing info into errorInfo for formatting const errorInfoWithTiming = { ...errorInfo, elapsedTime, timeoutValue: timeout }; // Show formatted error to user const formattedError = formatClaudeError(errorInfoWithTiming); console.error(`\n${ formattedError }\n`); reject(new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, { output: { stdout, stderr }, context: { exitCode: code, elapsedTime, timeoutValue: timeout, errorInfo } })); return; } if (code === 0) { logger.debug( 'claude-client - executeClaude', 'Claude CLI execution successful', { elapsedTime, outputLength: stdout.length } ); resolve(stdout); } else { // Detect specific error type const errorInfo = detectClaudeError(stdout, stderr, code); logger.error( 'claude-client - executeClaude', `Claude CLI failed: ${errorInfo.type}`, new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, { output: { stdout, stderr }, context: { exitCode: code, elapsedTime, timeoutValue: timeout, errorType: errorInfo.type } }) ); // Merge timing info into errorInfo for formatting const errorInfoWithTiming = { ...errorInfo, elapsedTime, timeoutValue: timeout }; // Show formatted error to user const formattedError = formatClaudeError(errorInfoWithTiming); console.error(`\n${ formattedError }\n`); reject(new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, { output: { stdout, stderr }, context: { exitCode: code, elapsedTime, timeoutValue: timeout, errorInfo } })); } }); // Handle errors claude.on('error', (error) => { const elapsedTime = Date.now() - startTime; logger.error( 'claude-client - executeClaude', 'Failed to spawn Claude CLI process', error ); reject(new ClaudeClientError('Failed to spawn Claude CLI', { cause: error, context: { command, args, elapsedTime, timeoutValue: timeout } })); }); // Set up timeout const timeoutId = setTimeout(() => { const elapsedTime = Date.now() - startTime; claude.kill(); logger.error( 'claude-client - executeClaude', 'Claude CLI execution timed out', new ClaudeClientError('Claude CLI timeout', { context: { elapsedTime, timeoutValue: timeout } }) ); reject(new ClaudeClientError('Claude CLI execution timed out', { context: { elapsedTime, timeoutValue: timeout } })); }, timeout); // Clear timeout if process completes claude.on('close', () => clearTimeout(timeoutId)); // Write prompt to stdin // Why: Claude CLI reads prompt from stdin, not command arguments // Handle stdin errors (e.g., EOF when process terminates unexpectedly) // Why: write() failures can emit 'error' events asynchronously // Common in parallel execution with large prompts claude.stdin.on('error', (error) => { logger.error( 'claude-client - executeClaude', 'stdin stream error (process may have terminated early)', { error: error.message, code: error.code, promptLength: prompt.length, duration: Date.now() - startTime } ); reject(new ClaudeClientError('Failed to write to Claude stdin - process terminated unexpectedly', { cause: error, context: { promptLength: prompt.length, errorCode: error.code, errorMessage: error.message, suggestion: 'Try reducing batch size or number of files per commit' } })); }); try { claude.stdin.write(prompt); claude.stdin.end(); } catch (error) { logger.error( 'claude-client - executeClaude', 'Failed to write prompt to Claude CLI stdin (synchronous error)', error ); reject(new ClaudeClientError('Failed to write prompt', { cause: error })); } }); /** * Executes Claude CLI fully interactively * Why: Allows user to interact with Claude and approve MCP permissions * * @param {string} prompt - Prompt text to send to Claude * @param {Object} options - Execution options * @returns {Promise<string>} - Returns 'interactive' since we can't capture output * @throws {ClaudeClientError} If execution fails */ const executeClaudeInteractive = (prompt, { timeout = 300000 } = {}) => new Promise((resolve, reject) => { const { command, args } = getClaudeCommand(); const { spawnSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const os = require('os'); // Save prompt to temp file that Claude can read const tempDir = os.tmpdir(); const tempFile = path.join(tempDir, 'claude-pr-instructions.md'); try { fs.writeFileSync(tempFile, prompt); } catch (err) { logger.error('claude-client - executeClaudeInteractive', 'Failed to write temp file', err); reject(new ClaudeClientError('Failed to write prompt file', { cause: err })); return; } logger.debug( 'claude-client - executeClaudeInteractive', 'Starting interactive Claude session', { promptLength: prompt.length, tempFile, command, args } ); console.log(''); console.log('╔══════════════════════════════════════════════════════════════════╗'); console.log('║ 🤖 INTERACTIVE CLAUDE SESSION ║'); console.log('╠══════════════════════════════════════════════════════════════════╣'); console.log('║ ║'); console.log('║ Instructions saved to: ║'); console.log(`║ ${tempFile.padEnd(62)}║`); console.log('║ ║'); console.log('║ When Claude starts, tell it: ║'); console.log('║ "Read and execute the instructions in the file above" ║'); console.log('║ ║'); console.log('║ • Type "y" if prompted for MCP permissions ║'); console.log('║ • Type "/exit" when done ║'); console.log('║ ║'); console.log('╚══════════════════════════════════════════════════════════════════╝'); console.log(''); console.log('Starting Claude...'); console.log(''); // Run Claude fully interactively (no flags - pure interactive mode) const result = spawnSync(command, args, { stdio: 'inherit', // Full terminal access shell: true, timeout }); // Clean up temp file try { fs.unlinkSync(tempFile); } catch (e) { logger.debug('claude-client - executeClaudeInteractive', 'Temp file cleanup', { error: e.message }); } if (result.error) { logger.error('claude-client - executeClaudeInteractive', 'Spawn error', result.error); reject(new ClaudeClientError('Failed to start Claude', { cause: result.error })); return; } if (result.status === 0 || result.status === null) { console.log(''); resolve('interactive-session-completed'); } else if (result.signal === 'SIGTERM') { reject(new ClaudeClientError('Claude session timed out', { context: { timeout } })); } else { logger.error('claude-client - executeClaudeInteractive', 'Claude exited with error', { status: result.status, signal: result.signal }); reject(new ClaudeClientError(`Claude exited with code ${result.status}`, { context: { exitCode: result.status, signal: result.signal } })); } }); /** * Extracts JSON from Claude's response * Why: Claude may include markdown formatting or explanatory text around JSON * * @param {string} response - Raw response from Claude * @param {Object} telemetryContext - Context for telemetry recording (optional) * @returns {Object} Parsed JSON object * @throws {ClaudeClientError} If no valid JSON found */ const extractJSON = (response, telemetryContext = {}) => { logger.debug( 'claude-client - extractJSON', 'Extracting JSON from response', { responseLength: response.length } ); // Why: Try multiple patterns to find JSON // Pattern 1: JSON in markdown code block const markdownMatch = response.match(/```json\s*([\s\S]*?)\s*```/); if (markdownMatch) { try { const json = JSON.parse(markdownMatch[1]); logger.debug('claude-client - extractJSON', 'JSON extracted from markdown block'); return json; } catch (error) { // Continue to next pattern } } // Pattern 2: JSON object (curly braces) const jsonMatch = response.match(/\{[\s\S]*\}/); if (jsonMatch) { try { const json = JSON.parse(jsonMatch[0]); logger.debug('claude-client - extractJSON', 'JSON extracted from response body'); return json; } catch (error) { // Continue to next pattern } } // Pattern 3: Extract lines starting with { until matching } const lines = response.split('\n'); let jsonStartIndex = -1; let braceCount = 0; let jsonLines = []; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (jsonStartIndex === -1 && line.startsWith('{')) { jsonStartIndex = i; braceCount = (line.match(/{/g) || []).length - (line.match(/}/g) || []).length; jsonLines.push(line); } else if (jsonStartIndex !== -1) { jsonLines.push(line); braceCount += (line.match(/{/g) || []).length - (line.match(/}/g) || []).length; if (braceCount === 0) { const jsonText = jsonLines.join('\n'); try { const json = JSON.parse(jsonText); logger.debug('claude-client - extractJSON', 'JSON extracted using line matching'); return json; } catch (error) { // Try next occurrence jsonStartIndex = -1; jsonLines = []; } } } } // No valid JSON found const responsePreview = response.substring(0, 500); logger.error( 'claude-client - extractJSON', 'No valid JSON found in response', new ClaudeClientError('No valid JSON in response', { output: responsePreview }) ); // Telemetry is now recorded by withRetry wrapper throw new ClaudeClientError('No valid JSON found in Claude response', { context: { response: response, errorInfo: { type: 'JSON_PARSE_ERROR' } } }); }; /** * Saves prompt and response to debug file * Why: Helps troubleshoot issues with Claude responses and verify prompts * * @param {string} prompt - Prompt sent to Claude * @param {string} response - Raw response from Claude * @param {string} filename - Debug filename (default: from config) */ const saveDebugResponse = async (prompt, response, filename = config.output.debugFile) => { try { // Ensure output directory exists const outputDir = path.dirname(filename); await fs.mkdir(outputDir, { recursive: true }); // Save full debug information const debugData = { timestamp: new Date().toISOString(), promptLength: prompt.length, responseLength: response.length, prompt, response }; await fs.writeFile(filename, JSON.stringify(debugData, null, 2), 'utf8'); // Display batch optimization status try { if (prompt.includes('OPTIMIZATION')) { console.log(`\n${ '='.repeat(70)}`); console.log('✅ BATCH OPTIMIZATION ENABLED'); console.log('='.repeat(70)); console.log('Multi-file analysis organized for efficient processing'); console.log('Check debug file for full prompt and response details'); console.log(`${'='.repeat(70) }\n`); } } catch (parseError) { // Ignore parsing errors, just skip the display } logger.info(`📝 Debug output saved to ${filename}`); logger.debug( 'claude-client - saveDebugResponse', `Debug response saved to ${filename}` ); } catch (error) { logger.error( 'claude-client - saveDebugResponse', 'Failed to save debug response', error ); } }; /** * Wraps an async function with retry logic for recoverable errors and telemetry tracking * Why: Reusable retry logic for Claude CLI operations with per-attempt telemetry * * @param {Function} fn - Async function to execute with retry * @param {Object} options - Retry options * @param {number} options.maxRetries - Maximum retry attempts (default: 3) * @param {number} options.baseRetryDelay - Base delay in ms (default: 2000) * @param {number} options.retryCount - Current retry attempt (internal, default: 0) * @param {string} options.operationName - Name for logging (default: 'operation') * @param {Object} options.telemetryContext - Context for telemetry recording (optional) * @returns {Promise<any>} Result from fn * @throws {Error} If fn fails after all retries */ const withRetry = async (fn, { maxRetries = 3, baseRetryDelay = 2000, retryCount = 0, operationName = 'operation', telemetryContext = null } = {}) => { const retryDelay = baseRetryDelay * Math.pow(2, retryCount); const startTime = Date.now(); try { const result = await fn(); // Record success telemetry if context provided if (telemetryContext) { const duration = Date.now() - startTime; await recordBatchSuccess({ ...telemetryContext, duration, retryAttempt: retryCount, totalRetries: maxRetries }).catch(err => { logger.debug('claude-client - withRetry', 'Failed to record success telemetry', err); }); } return result; } catch (error) { // Record failure telemetry if context provided (before retry decision) if (telemetryContext) { const errorType = error.context?.errorInfo?.type || error.name || 'UNKNOWN_ERROR'; const errorMessage = error.message || 'Unknown error'; await recordJsonParseFailure({ ...telemetryContext, errorType, errorMessage, retryAttempt: retryCount, totalRetries: maxRetries, responseLength: error.context?.response?.length || 0, responsePreview: error.context?.response?.substring(0, 100) || '' }).catch(err => { logger.debug('claude-client - withRetry', 'Failed to record failure telemetry', err); }); } // Check if error is recoverable and we haven't exceeded retry limit const hasContext = !!error.context; const hasErrorInfo = !!error.context?.errorInfo; const isRecoverable = hasErrorInfo ? isRecoverableError(error.context.errorInfo) : false; const canRetry = retryCount < maxRetries; logger.debug( 'claude-client - withRetry', `Retry check for ${operationName}`, { retryCount, maxRetries, hasContext, hasErrorInfo, isRecoverable, canRetry, errorType: error.context?.errorInfo?.type, errorName: error.name } ); const shouldRetry = canRetry && hasContext && hasErrorInfo && isRecoverable; if (shouldRetry) { logger.debug( 'claude-client - withRetry', `Recoverable error detected, retrying ${operationName} in ${retryDelay}ms (attempt ${retryCount + 1}/${maxRetries})`, { errorType: error.context.errorInfo.type } ); console.log(`\n⏳ Retrying in ${retryDelay / 1000} seconds due to ${error.context.errorInfo.type}...\n`); // Wait before retry await new Promise(resolve => setTimeout(resolve, retryDelay)); // Retry with incremented count and same telemetry context return withRetry(fn, { maxRetries, baseRetryDelay, retryCount: retryCount + 1, operationName, telemetryContext }); } // Add retry attempt to error context if not already present if (error.context && !error.context.retryAttempt) { error.context.retryAttempt = retryCount; error.context.maxRetries = maxRetries; } logger.error('claude-client - withRetry', `${operationName} failed after retries`, error); throw error; } }; /** * Executes Claude CLI with retry logic and telemetry tracking * Why: Provides retry capability for executeClaude calls with per-attempt telemetry * * @param {string} prompt - Prompt text * @param {Object} options - Execution options * @param {number} options.timeout - Timeout in milliseconds * @param {Array<string>} options.allowedTools - Allowed tools for Claude * @param {Object} options.telemetryContext - Context for telemetry (fileCount, hook, etc.) * @returns {Promise<string>} Claude's response */ const executeClaudeWithRetry = async (prompt, options = {}) => { const { telemetryContext = {}, ...executeOptions } = options; return withRetry( () => executeClaude(prompt, executeOptions), { operationName: 'executeClaude', telemetryContext: telemetryContext } ); }; /** * Analyzes code using Claude CLI * Why: High-level interface that handles execution, parsing, and debug * * @param {string} prompt - Analysis prompt * @param {Object} options - Analysis options * @param {number} options.timeout - Timeout in milliseconds * @param {boolean} options.saveDebug - Save response to debug file (default: from config) * @param {Object} options.telemetryContext - Context for telemetry (fileCount, batchSize, etc.) * @returns {Promise<Object>} Parsed analysis result * @throws {ClaudeClientError} If analysis fails */ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = config.system.debug, telemetryContext = {} } = {}) => { const startTime = Date.now(); logger.debug( 'claude-client - analyzeCode', 'Starting code analysis', { promptLength: prompt.length, timeout, saveDebug } ); // Rotate telemetry files periodically rotateTelemetry().catch(err => { logger.debug('claude-client - analyzeCode', 'Failed to rotate telemetry', err); }); // Use withRetry to wrap the entire analysis flow (with telemetry tracking) return withRetry( async () => { // Execute Claude CLI const response = await executeClaude(prompt, { timeout }); // Save debug if requested if (saveDebug) { await saveDebugResponse(prompt, response); } // Extract and parse JSON const result = extractJSON(response, telemetryContext); const duration = Date.now() - startTime; logger.debug( 'claude-client - analyzeCode', 'Analysis complete', { hasApproved: 'approved' in result, hasQualityGate: 'QUALITY_GATE' in result, blockingIssuesCount: result.blockingIssues?.length ?? 0, duration } ); // Telemetry is now recorded by withRetry wrapper return result; }, { operationName: 'analyzeCode', telemetryContext: { responseLength: 0, // Will be updated in withRetry ...telemetryContext } } ); }; /** * Splits array into chunks * @param {Array} array - Array to split * @param {number} size - Chunk size * @returns {Array<Array>} Array of chunks */ const chunkArray = (array, size) => { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; }; /** * Runs multiple analyzeCode calls in parallel * @param {Array<string>} prompts - Array of prompts to analyze * @param {Object} options - Same options as analyzeCode * @param {Object} options.telemetryContext - Base telemetry context (will be augmented per batch) * @returns {Promise<Array<Object>>} Array of results */ const analyzeCodeParallel = async (prompts, options = {}) => { const startTime = Date.now(); console.log(`\n${ '='.repeat(70)}`); console.log(`🚀 PARALLEL EXECUTION: ${prompts.length} Claude processes`); console.log('='.repeat(70)); logger.info(`Starting parallel analysis: ${prompts.length} prompts`); const promises = prompts.map((prompt, index) => { console.log(` ⚡ Launching batch ${index + 1}/${prompts.length}...`); logger.debug('claude-client - analyzeCodeParallel', `Starting batch ${index + 1}`); // Augment telemetry context with batch info const batchTelemetryContext = { ...(options.telemetryContext || {}), batchIndex: index, totalBatches: prompts.length }; return analyzeCode(prompt, { ...options, telemetryContext: batchTelemetryContext }); }); console.log(' ⏳ Waiting for all batches to complete...\n'); const results = await Promise.all(promises); const duration = ((Date.now() - startTime) / 1000).toFixed(2); console.log('='.repeat(70)); console.log(`✅ PARALLEL EXECUTION COMPLETE: ${results.length} results in ${duration}s`); console.log(`${'='.repeat(70) }\n`); logger.info(`Parallel analysis complete: ${results.length} results in ${duration}s`); return results; }; export { ClaudeClientError, executeClaude, executeClaudeWithRetry, executeClaudeInteractive, extractJSON, saveDebugResponse, analyzeCode, analyzeCodeParallel, chunkArray, isWindows, isWSLAvailable, getClaudeCommand };