claude-git-hooks 2.4.1 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,7 @@ import os from 'os';
22
22
  import logger from './logger.js';
23
23
  import config from '../config.js';
24
24
  import { detectClaudeError, formatClaudeError, ClaudeErrorType } from './claude-diagnostics.js';
25
+ import { which } from './which-command.js';
25
26
 
26
27
  /**
27
28
  * Custom error for Claude client failures
@@ -40,9 +41,7 @@ class ClaudeClientError extends Error {
40
41
  * Detect if running on Windows
41
42
  * Why: Need to use 'wsl claude' instead of 'claude' on Windows
42
43
  */
43
- const isWindows = () => {
44
- return os.platform() === 'win32' || process.env.OS === 'Windows_NT';
45
- };
44
+ const isWindows = () => os.platform() === 'win32' || process.env.OS === 'Windows_NT';
46
45
 
47
46
  /**
48
47
  * Check if WSL is available on Windows
@@ -68,6 +67,7 @@ const isWSLAvailable = () => {
68
67
  /**
69
68
  * Get Claude command configuration for current platform
70
69
  * Why: On Windows, try native Claude first, then WSL as fallback
70
+ * Node 24 Fix: Uses which() to resolve absolute paths, avoiding shell: true
71
71
  *
72
72
  * @returns {Object} { command, args } - Command and base arguments
73
73
  * @throws {ClaudeClientError} If Claude not available on any method
@@ -75,42 +75,68 @@ const isWSLAvailable = () => {
75
75
  const getClaudeCommand = () => {
76
76
  if (isWindows()) {
77
77
  // Try native Windows Claude first (e.g., installed via npm/scoop/choco)
78
- try {
79
- execSync('claude --version', { stdio: 'ignore', timeout: 3000 });
80
- logger.debug('claude-client - getClaudeCommand', 'Using native Windows Claude CLI');
81
- return { command: 'claude', args: [] };
82
- } catch (nativeError) {
83
- logger.debug('claude-client - getClaudeCommand', 'Native Claude not found, trying WSL');
84
-
85
- // Fallback to WSL
86
- if (!isWSLAvailable()) {
87
- throw new ClaudeClientError('Claude CLI not found. Install Claude CLI natively on Windows or via WSL', {
88
- context: {
89
- platform: 'Windows',
90
- suggestions: [
91
- 'Native Windows: npm install -g @anthropic-ai/claude-cli',
92
- 'WSL: wsl --install, then install Claude in WSL'
93
- ]
94
- }
95
- });
96
- }
78
+ // Node 24: Use which() instead of execSync to get absolute path
79
+ const nativePath = which('claude');
80
+ if (nativePath) {
81
+ logger.debug('claude-client - getClaudeCommand', 'Using native Windows Claude CLI', { path: nativePath });
82
+ return { command: nativePath, args: [] };
83
+ }
84
+
85
+ logger.debug('claude-client - getClaudeCommand', 'Native Claude not found, trying WSL');
86
+
87
+ // Fallback to WSL
88
+ if (!isWSLAvailable()) {
89
+ throw new ClaudeClientError('Claude CLI not found. Install Claude CLI natively on Windows or via WSL', {
90
+ context: {
91
+ platform: 'Windows',
92
+ suggestions: [
93
+ 'Native Windows: npm install -g @anthropic-ai/claude-cli',
94
+ 'WSL: wsl --install, then install Claude in WSL'
95
+ ]
96
+ }
97
+ });
98
+ }
97
99
 
98
- // Check if Claude is available in WSL
100
+ // Check if Claude is available in WSL
101
+ // Node 24: Resolve wsl.exe absolute path to avoid shell: true
102
+ const wslPath = which('wsl');
103
+ if (wslPath) {
99
104
  try {
100
- execSync('wsl claude --version', { stdio: 'ignore', timeout: 5000 });
101
- logger.debug('claude-client - getClaudeCommand', 'Using WSL Claude CLI');
102
- return { command: 'wsl', args: ['claude'] };
105
+ // Verify Claude exists in WSL
106
+ execSync(`"${wslPath}" claude --version`, { stdio: 'ignore', timeout: 5000 });
107
+ logger.debug('claude-client - getClaudeCommand', 'Using WSL Claude CLI', { wslPath });
108
+ return { command: wslPath, args: ['claude'] };
103
109
  } catch (wslError) {
104
- throw new ClaudeClientError('Claude CLI not found in Windows or WSL', {
110
+ throw new ClaudeClientError('Claude CLI not found in WSL', {
105
111
  context: {
106
112
  platform: 'Windows',
107
- nativeError: nativeError.message,
113
+ wslPath,
108
114
  wslError: wslError.message
109
115
  }
110
116
  });
111
117
  }
112
118
  }
119
+
120
+ throw new ClaudeClientError('Claude CLI not found in Windows or WSL', {
121
+ context: {
122
+ platform: 'Windows',
123
+ suggestions: [
124
+ 'Install Claude CLI: npm install -g @anthropic-ai/claude-cli',
125
+ 'Or install WSL and Claude in WSL'
126
+ ]
127
+ }
128
+ });
129
+ }
130
+
131
+ // Unix (Linux, macOS): Use which() to get absolute path
132
+ const claudePath = which('claude');
133
+ if (claudePath) {
134
+ logger.debug('claude-client - getClaudeCommand', 'Using Claude CLI', { path: claudePath });
135
+ return { command: claudePath, args: [] };
113
136
  }
137
+
138
+ // Fallback to 'claude' if which fails (will error later if not found)
139
+ logger.debug('claude-client - getClaudeCommand', 'which() failed, using fallback', { command: 'claude' });
114
140
  return { command: 'claude', args: [] };
115
141
  };
116
142
 
@@ -125,127 +151,222 @@ const getClaudeCommand = () => {
125
151
  * @returns {Promise<string>} Claude's response
126
152
  * @throws {ClaudeClientError} If execution fails or times out
127
153
  */
128
- const executeClaude = (prompt, { timeout = 120000 } = {}) => {
129
- return new Promise((resolve, reject) => {
130
- // Get platform-specific command
131
- const { command, args } = getClaudeCommand();
132
- const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command;
154
+ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) => new Promise((resolve, reject) => {
155
+ // Get platform-specific command
156
+ const { command, args } = getClaudeCommand();
157
+
158
+ // Add allowed tools if specified (for MCP tools)
159
+ const finalArgs = [...args];
160
+ if (allowedTools.length > 0) {
161
+ // Format: --allowedTools "mcp__github__create_pull_request,mcp__github__get_file_contents"
162
+ finalArgs.push('--allowedTools', allowedTools.join(','));
163
+ }
133
164
 
134
- logger.debug(
135
- 'claude-client - executeClaude',
136
- 'Executing Claude CLI',
137
- { promptLength: prompt.length, timeout, command: fullCommand, isWindows: isWindows() }
138
- );
165
+ const fullCommand = finalArgs.length > 0 ? `${command} ${finalArgs.join(' ')}` : command;
139
166
 
140
- const startTime = Date.now();
167
+ logger.debug(
168
+ 'claude-client - executeClaude',
169
+ 'Executing Claude CLI',
170
+ { promptLength: prompt.length, timeout, command: fullCommand, isWindows: isWindows(), allowedTools }
171
+ );
141
172
 
142
- // Why: Use spawn instead of exec to handle large prompts and responses
143
- // spawn streams data, exec buffers everything in memory
144
- // shell: true needed on Windows to resolve .cmd/.bat executables
145
- const claude = spawn(command, args, {
146
- stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr
147
- shell: true // Required for Windows to find .cmd/.bat files
148
- });
173
+ const startTime = Date.now();
149
174
 
150
- let stdout = '';
151
- let stderr = '';
175
+ // Why: Use spawn instead of exec to handle large prompts and responses
176
+ // spawn streams data, exec buffers everything in memory
177
+ // Node 24 Fix: Removed shell: true to avoid DEP0190 deprecation warning
178
+ // We now use absolute paths from which(), so shell is not needed
179
+ const claude = spawn(command, finalArgs, {
180
+ stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr
181
+ });
152
182
 
153
- // Collect stdout
154
- claude.stdout.on('data', (data) => {
155
- stdout += data.toString();
156
- });
183
+ let stdout = '';
184
+ let stderr = '';
157
185
 
158
- // Collect stderr
159
- claude.stderr.on('data', (data) => {
160
- stderr += data.toString();
161
- });
186
+ // Collect stdout
187
+ claude.stdout.on('data', (data) => {
188
+ stdout += data.toString();
189
+ });
162
190
 
163
- // Handle process completion
164
- claude.on('close', (code) => {
165
- const duration = Date.now() - startTime;
166
-
167
- if (code === 0) {
168
- logger.debug(
169
- 'claude-client - executeClaude',
170
- 'Claude CLI execution successful',
171
- { duration, outputLength: stdout.length }
172
- );
173
- resolve(stdout);
174
- } else {
175
- // Detect specific error type
176
- const errorInfo = detectClaudeError(stdout, stderr, code);
177
-
178
- logger.error(
179
- 'claude-client - executeClaude',
180
- `Claude CLI failed: ${errorInfo.type}`,
181
- new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
182
- output: { stdout, stderr },
183
- context: { exitCode: code, duration, errorType: errorInfo.type }
184
- })
185
- );
186
-
187
- // Show formatted error to user
188
- const formattedError = formatClaudeError(errorInfo);
189
- console.error('\n' + formattedError + '\n');
190
-
191
- reject(new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
192
- output: { stdout, stderr },
193
- context: { exitCode: code, duration, errorInfo }
194
- }));
195
- }
196
- });
191
+ // Collect stderr
192
+ claude.stderr.on('data', (data) => {
193
+ stderr += data.toString();
194
+ });
197
195
 
198
- // Handle errors
199
- claude.on('error', (error) => {
200
- logger.error(
196
+ // Handle process completion
197
+ claude.on('close', (code) => {
198
+ const duration = Date.now() - startTime;
199
+
200
+ if (code === 0) {
201
+ logger.debug(
201
202
  'claude-client - executeClaude',
202
- 'Failed to spawn Claude CLI process',
203
- error
203
+ 'Claude CLI execution successful',
204
+ { duration, outputLength: stdout.length }
204
205
  );
206
+ resolve(stdout);
207
+ } else {
208
+ // Detect specific error type
209
+ const errorInfo = detectClaudeError(stdout, stderr, code);
205
210
 
206
- reject(new ClaudeClientError('Failed to spawn Claude CLI', {
207
- cause: error,
208
- context: { command, args }
209
- }));
210
- });
211
-
212
- // Set up timeout
213
- const timeoutId = setTimeout(() => {
214
- claude.kill();
215
211
  logger.error(
216
212
  'claude-client - executeClaude',
217
- 'Claude CLI execution timed out',
218
- new ClaudeClientError('Claude CLI timeout', {
219
- context: { timeout }
213
+ `Claude CLI failed: ${errorInfo.type}`,
214
+ new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
215
+ output: { stdout, stderr },
216
+ context: { exitCode: code, duration, errorType: errorInfo.type }
220
217
  })
221
218
  );
222
219
 
223
- reject(new ClaudeClientError('Claude CLI execution timed out', {
224
- context: { timeout }
220
+ // Show formatted error to user
221
+ const formattedError = formatClaudeError(errorInfo);
222
+ console.error(`\n${ formattedError }\n`);
223
+
224
+ reject(new ClaudeClientError(`Claude CLI error: ${errorInfo.type}`, {
225
+ output: { stdout, stderr },
226
+ context: { exitCode: code, duration, errorInfo }
225
227
  }));
226
- }, timeout);
228
+ }
229
+ });
227
230
 
228
- // Clear timeout if process completes
229
- claude.on('close', () => clearTimeout(timeoutId));
231
+ // Handle errors
232
+ claude.on('error', (error) => {
233
+ logger.error(
234
+ 'claude-client - executeClaude',
235
+ 'Failed to spawn Claude CLI process',
236
+ error
237
+ );
230
238
 
231
- // Write prompt to stdin
232
- // Why: Claude CLI reads prompt from stdin, not command arguments
233
- try {
234
- claude.stdin.write(prompt);
235
- claude.stdin.end();
236
- } catch (error) {
237
- logger.error(
238
- 'claude-client - executeClaude',
239
- 'Failed to write prompt to Claude CLI stdin',
240
- error
241
- );
239
+ reject(new ClaudeClientError('Failed to spawn Claude CLI', {
240
+ cause: error,
241
+ context: { command, args }
242
+ }));
243
+ });
242
244
 
243
- reject(new ClaudeClientError('Failed to write prompt', {
244
- cause: error
245
- }));
246
- }
245
+ // Set up timeout
246
+ const timeoutId = setTimeout(() => {
247
+ claude.kill();
248
+ logger.error(
249
+ 'claude-client - executeClaude',
250
+ 'Claude CLI execution timed out',
251
+ new ClaudeClientError('Claude CLI timeout', {
252
+ context: { timeout }
253
+ })
254
+ );
255
+
256
+ reject(new ClaudeClientError('Claude CLI execution timed out', {
257
+ context: { timeout }
258
+ }));
259
+ }, timeout);
260
+
261
+ // Clear timeout if process completes
262
+ claude.on('close', () => clearTimeout(timeoutId));
263
+
264
+ // Write prompt to stdin
265
+ // Why: Claude CLI reads prompt from stdin, not command arguments
266
+ try {
267
+ claude.stdin.write(prompt);
268
+ claude.stdin.end();
269
+ } catch (error) {
270
+ logger.error(
271
+ 'claude-client - executeClaude',
272
+ 'Failed to write prompt to Claude CLI stdin',
273
+ error
274
+ );
275
+
276
+ reject(new ClaudeClientError('Failed to write prompt', {
277
+ cause: error
278
+ }));
279
+ }
280
+ });
281
+
282
+ /**
283
+ * Executes Claude CLI fully interactively
284
+ * Why: Allows user to interact with Claude and approve MCP permissions
285
+ *
286
+ * @param {string} prompt - Prompt text to send to Claude
287
+ * @param {Object} options - Execution options
288
+ * @returns {Promise<string>} - Returns 'interactive' since we can't capture output
289
+ * @throws {ClaudeClientError} If execution fails
290
+ */
291
+ const executeClaudeInteractive = (prompt, { timeout = 300000 } = {}) => new Promise((resolve, reject) => {
292
+ const { command, args } = getClaudeCommand();
293
+ const { spawnSync } = require('child_process');
294
+ const fs = require('fs');
295
+ const path = require('path');
296
+ const os = require('os');
297
+
298
+ // Save prompt to temp file that Claude can read
299
+ const tempDir = os.tmpdir();
300
+ const tempFile = path.join(tempDir, 'claude-pr-instructions.md');
301
+
302
+ try {
303
+ fs.writeFileSync(tempFile, prompt);
304
+ } catch (err) {
305
+ logger.error('claude-client - executeClaudeInteractive', 'Failed to write temp file', err);
306
+ reject(new ClaudeClientError('Failed to write prompt file', { cause: err }));
307
+ return;
308
+ }
309
+
310
+ logger.debug(
311
+ 'claude-client - executeClaudeInteractive',
312
+ 'Starting interactive Claude session',
313
+ { promptLength: prompt.length, tempFile, command, args }
314
+ );
315
+
316
+ console.log('');
317
+ console.log('╔══════════════════════════════════════════════════════════════════╗');
318
+ console.log('║ 🤖 INTERACTIVE CLAUDE SESSION ║');
319
+ console.log('╠══════════════════════════════════════════════════════════════════╣');
320
+ console.log('║ ║');
321
+ console.log('║ Instructions saved to: ║');
322
+ console.log(`║ ${tempFile.padEnd(62)}║`);
323
+ console.log('║ ║');
324
+ console.log('║ When Claude starts, tell it: ║');
325
+ console.log('║ "Read and execute the instructions in the file above" ║');
326
+ console.log('║ ║');
327
+ console.log('║ • Type "y" if prompted for MCP permissions ║');
328
+ console.log('║ • Type "/exit" when done ║');
329
+ console.log('║ ║');
330
+ console.log('╚══════════════════════════════════════════════════════════════════╝');
331
+ console.log('');
332
+ console.log('Starting Claude...');
333
+ console.log('');
334
+
335
+ // Run Claude fully interactively (no flags - pure interactive mode)
336
+ const result = spawnSync(command, args, {
337
+ stdio: 'inherit', // Full terminal access
338
+ shell: true,
339
+ timeout
247
340
  });
248
- };
341
+
342
+ // Clean up temp file
343
+ try {
344
+ fs.unlinkSync(tempFile);
345
+ } catch (e) {
346
+ logger.debug('claude-client - executeClaudeInteractive', 'Temp file cleanup', { error: e.message });
347
+ }
348
+
349
+ if (result.error) {
350
+ logger.error('claude-client - executeClaudeInteractive', 'Spawn error', result.error);
351
+ reject(new ClaudeClientError('Failed to start Claude', { cause: result.error }));
352
+ return;
353
+ }
354
+
355
+ if (result.status === 0 || result.status === null) {
356
+ console.log('');
357
+ resolve('interactive-session-completed');
358
+ } else if (result.signal === 'SIGTERM') {
359
+ reject(new ClaudeClientError('Claude session timed out', { context: { timeout } }));
360
+ } else {
361
+ logger.error('claude-client - executeClaudeInteractive', 'Claude exited with error', {
362
+ status: result.status,
363
+ signal: result.signal
364
+ });
365
+ reject(new ClaudeClientError(`Claude exited with code ${result.status}`, {
366
+ context: { exitCode: result.status, signal: result.signal }
367
+ }));
368
+ }
369
+ });
249
370
 
250
371
  /**
251
372
  * Extracts JSON from Claude's response
@@ -352,8 +473,8 @@ const saveDebugResponse = async (prompt, response, filename = config.output.debu
352
473
  timestamp: new Date().toISOString(),
353
474
  promptLength: prompt.length,
354
475
  responseLength: response.length,
355
- prompt: prompt,
356
- response: response
476
+ prompt,
477
+ response
357
478
  };
358
479
 
359
480
  await fs.writeFile(filename, JSON.stringify(debugData, null, 2), 'utf8');
@@ -361,12 +482,12 @@ const saveDebugResponse = async (prompt, response, filename = config.output.debu
361
482
  // Display batch optimization status
362
483
  try {
363
484
  if (prompt.includes('OPTIMIZATION')) {
364
- console.log('\n' + '='.repeat(70));
485
+ console.log(`\n${ '='.repeat(70)}`);
365
486
  console.log('✅ BATCH OPTIMIZATION ENABLED');
366
487
  console.log('='.repeat(70));
367
488
  console.log('Multi-file analysis organized for efficient processing');
368
489
  console.log('Check debug file for full prompt and response details');
369
- console.log('='.repeat(70) + '\n');
490
+ console.log(`${'='.repeat(70) }\n`);
370
491
  }
371
492
  } catch (parseError) {
372
493
  // Ignore parsing errors, just skip the display
@@ -457,7 +578,7 @@ const chunkArray = (array, size) => {
457
578
  const analyzeCodeParallel = async (prompts, options = {}) => {
458
579
  const startTime = Date.now();
459
580
 
460
- console.log('\n' + '='.repeat(70));
581
+ console.log(`\n${ '='.repeat(70)}`);
461
582
  console.log(`🚀 PARALLEL EXECUTION: ${prompts.length} Claude processes`);
462
583
  console.log('='.repeat(70));
463
584
 
@@ -469,14 +590,14 @@ const analyzeCodeParallel = async (prompts, options = {}) => {
469
590
  return analyzeCode(prompt, options);
470
591
  });
471
592
 
472
- console.log(` ⏳ Waiting for all batches to complete...\n`);
593
+ console.log(' ⏳ Waiting for all batches to complete...\n');
473
594
  const results = await Promise.all(promises);
474
595
 
475
596
  const duration = ((Date.now() - startTime) / 1000).toFixed(2);
476
597
 
477
598
  console.log('='.repeat(70));
478
599
  console.log(`✅ PARALLEL EXECUTION COMPLETE: ${results.length} results in ${duration}s`);
479
- console.log('='.repeat(70) + '\n');
600
+ console.log(`${'='.repeat(70) }\n`);
480
601
 
481
602
  logger.info(`Parallel analysis complete: ${results.length} results in ${duration}s`);
482
603
  return results;
@@ -485,9 +606,13 @@ const analyzeCodeParallel = async (prompts, options = {}) => {
485
606
  export {
486
607
  ClaudeClientError,
487
608
  executeClaude,
609
+ executeClaudeInteractive,
488
610
  extractJSON,
489
611
  saveDebugResponse,
490
612
  analyzeCode,
491
613
  analyzeCodeParallel,
492
- chunkArray
614
+ chunkArray,
615
+ isWindows,
616
+ isWSLAvailable,
617
+ getClaudeCommand
493
618
  };
@@ -4,7 +4,6 @@
4
4
  *
5
5
  * Key responsibilities:
6
6
  * - Read files with size validation
7
- * - Filter SKIP-ANALYSIS patterns from code
8
7
  * - Validate file extensions
9
8
  * - Check file sizes
10
9
  *
@@ -15,7 +14,6 @@
15
14
  */
16
15
 
17
16
  import fs from 'fs/promises';
18
- import fsSync from 'fs';
19
17
  import path from 'path';
20
18
  import logger from './logger.js';
21
19
 
@@ -120,85 +118,6 @@ const readFile = async (filePath, { maxSize = 100000, encoding = 'utf8' } = {})
120
118
  }
121
119
  };
122
120
 
123
- /**
124
- * Filters SKIP_ANALYSIS patterns from code content
125
- * Why: Allows developers to exclude specific code from analysis
126
- *
127
- * ⚠️ KNOWN ISSUE (EXPERIMENTAL/BROKEN):
128
- * This feature does NOT work reliably. The pre-commit hook analyzes git diff
129
- * instead of full file content. Markers added in previous commits are NOT
130
- * present in subsequent diffs, so they are not detected.
131
- *
132
- * Example of failure:
133
- * - Commit 1: Add // SKIP_ANALYSIS_BLOCK markers
134
- * - Commit 2: Modify line inside block
135
- * - Result: Diff only shows modified line, NOT the markers → filter fails
136
- *
137
- * Supports two patterns:
138
- * 1. Single line: // SKIP_ANALYSIS_LINE (excludes next line)
139
- * 2. Block: // SKIP_ANALYSIS_BLOCK ... // SKIP_ANALYSIS_BLOCK (excludes block)
140
- *
141
- * @param {string} content - File content to filter
142
- * @returns {string} Filtered content
143
- */
144
- const filterSkipAnalysis = (content) => {
145
- logger.debug(
146
- 'file-operations - filterSkipAnalysis',
147
- 'Filtering SKIP_ANALYSIS patterns',
148
- { originalLength: content.length }
149
- );
150
-
151
- const lines = content.split('\n');
152
- let skipNext = false;
153
- let inSkipBlock = false;
154
-
155
- // Why: Use map instead of filter to preserve line numbers for error reporting
156
- // Empty lines maintain original line number mapping
157
- const filteredLines = lines.map((line) => {
158
- // IMPORTANT: Check specific pattern first (SKIP_ANALYSIS_BLOCK)
159
- // before general pattern (SKIP_ANALYSIS_LINE) to avoid substring match issues
160
-
161
- // Detect SKIP_ANALYSIS_BLOCK (toggle block state)
162
- if (line.includes('// SKIP_ANALYSIS_BLOCK')) {
163
- inSkipBlock = !inSkipBlock;
164
- return ''; // Preserve line number
165
- }
166
-
167
- // Detect single-line SKIP_ANALYSIS_LINE (must check AFTER block check)
168
- if (line.includes('// SKIP_ANALYSIS_LINE')) {
169
- skipNext = true;
170
- return ''; // Preserve line number
171
- }
172
-
173
- // Skip lines inside block
174
- if (inSkipBlock) {
175
- return '';
176
- }
177
-
178
- // Skip next line after single SKIP_ANALYSIS_LINE
179
- if (skipNext) {
180
- skipNext = false;
181
- return '';
182
- }
183
-
184
- return line;
185
- });
186
-
187
- const filtered = filteredLines.join('\n');
188
-
189
- logger.debug(
190
- 'file-operations - filterSkipAnalysis',
191
- 'Filtering complete',
192
- {
193
- originalLength: content.length,
194
- filteredLength: filtered.length,
195
- linesRemoved: lines.length - filteredLines.filter(l => l !== '').length
196
- }
197
- );
198
-
199
- return filtered;
200
- };
201
-
202
121
  /**
203
122
  * Validates file extension against allowed list
204
123
  * Why: Pre-commit hook should only analyze specific file types
@@ -361,32 +280,11 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
361
280
  return fileMetadata;
362
281
  };
363
282
 
364
- /**
365
- * Reads multiple files in parallel with filtering
366
- * Why: Efficiently loads file contents for analysis
367
- *
368
- * @param {Array<string>} files - Array of file paths
369
- * @param {Object} options - Read options
370
- * @param {number} options.maxSize - Max file size
371
- * @param {boolean} options.applySkipFilter - Apply SKIP-ANALYSIS filtering
372
- * @returns {Promise<Array<Object>>} Files with content
373
- *
374
- * Returned object structure:
375
- * [
376
- * {
377
- * path: string, // File path
378
- * content: string, // File content (filtered if requested)
379
- * size: number, // Original size in bytes
380
- * error: Error // Error if read failed
381
- * }
382
- * ]
383
- */
384
283
  export {
385
284
  FileOperationError,
386
285
  getFileSize,
387
286
  fileExists,
388
287
  readFile,
389
- filterSkipAnalysis,
390
288
  hasAllowedExtension,
391
289
  filterFiles
392
290
  };