claude-git-hooks 2.4.1 → 2.5.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.
@@ -125,16 +125,24 @@ const getClaudeCommand = () => {
125
125
  * @returns {Promise<string>} Claude's response
126
126
  * @throws {ClaudeClientError} If execution fails or times out
127
127
  */
128
- const executeClaude = (prompt, { timeout = 120000 } = {}) => {
128
+ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) => {
129
129
  return new Promise((resolve, reject) => {
130
130
  // Get platform-specific command
131
131
  const { command, args } = getClaudeCommand();
132
- const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command;
132
+
133
+ // Add allowed tools if specified (for MCP tools)
134
+ const finalArgs = [...args];
135
+ if (allowedTools.length > 0) {
136
+ // Format: --allowedTools "mcp__github__create_pull_request,mcp__github__get_file_contents"
137
+ finalArgs.push('--allowedTools', allowedTools.join(','));
138
+ }
139
+
140
+ const fullCommand = finalArgs.length > 0 ? `${command} ${finalArgs.join(' ')}` : command;
133
141
 
134
142
  logger.debug(
135
143
  'claude-client - executeClaude',
136
144
  'Executing Claude CLI',
137
- { promptLength: prompt.length, timeout, command: fullCommand, isWindows: isWindows() }
145
+ { promptLength: prompt.length, timeout, command: fullCommand, isWindows: isWindows(), allowedTools }
138
146
  );
139
147
 
140
148
  const startTime = Date.now();
@@ -142,7 +150,7 @@ const executeClaude = (prompt, { timeout = 120000 } = {}) => {
142
150
  // Why: Use spawn instead of exec to handle large prompts and responses
143
151
  // spawn streams data, exec buffers everything in memory
144
152
  // shell: true needed on Windows to resolve .cmd/.bat executables
145
- const claude = spawn(command, args, {
153
+ const claude = spawn(command, finalArgs, {
146
154
  stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr
147
155
  shell: true // Required for Windows to find .cmd/.bat files
148
156
  });
@@ -247,6 +255,97 @@ const executeClaude = (prompt, { timeout = 120000 } = {}) => {
247
255
  });
248
256
  };
249
257
 
258
+ /**
259
+ * Executes Claude CLI fully interactively
260
+ * Why: Allows user to interact with Claude and approve MCP permissions
261
+ *
262
+ * @param {string} prompt - Prompt text to send to Claude
263
+ * @param {Object} options - Execution options
264
+ * @returns {Promise<string>} - Returns 'interactive' since we can't capture output
265
+ * @throws {ClaudeClientError} If execution fails
266
+ */
267
+ const executeClaudeInteractive = (prompt, { timeout = 300000 } = {}) => {
268
+ return new Promise((resolve, reject) => {
269
+ const { command, args } = getClaudeCommand();
270
+ const { spawnSync } = require('child_process');
271
+ const fs = require('fs');
272
+ const path = require('path');
273
+ const os = require('os');
274
+
275
+ // Save prompt to temp file that Claude can read
276
+ const tempDir = os.tmpdir();
277
+ const tempFile = path.join(tempDir, `claude-pr-instructions.md`);
278
+
279
+ try {
280
+ fs.writeFileSync(tempFile, prompt);
281
+ } catch (err) {
282
+ logger.error('claude-client - executeClaudeInteractive', 'Failed to write temp file', err);
283
+ reject(new ClaudeClientError('Failed to write prompt file', { cause: err }));
284
+ return;
285
+ }
286
+
287
+ logger.debug(
288
+ 'claude-client - executeClaudeInteractive',
289
+ 'Starting interactive Claude session',
290
+ { promptLength: prompt.length, tempFile, command, args }
291
+ );
292
+
293
+ console.log('');
294
+ console.log('╔══════════════════════════════════════════════════════════════════╗');
295
+ console.log('║ 🤖 INTERACTIVE CLAUDE SESSION ║');
296
+ console.log('╠══════════════════════════════════════════════════════════════════╣');
297
+ console.log('║ ║');
298
+ console.log('║ Instructions saved to: ║');
299
+ console.log(`║ ${tempFile.padEnd(62)}║`);
300
+ console.log('║ ║');
301
+ console.log('║ When Claude starts, tell it: ║');
302
+ console.log('║ "Read and execute the instructions in the file above" ║');
303
+ console.log('║ ║');
304
+ console.log('║ • Type "y" if prompted for MCP permissions ║');
305
+ console.log('║ • Type "/exit" when done ║');
306
+ console.log('║ ║');
307
+ console.log('╚══════════════════════════════════════════════════════════════════╝');
308
+ console.log('');
309
+ console.log('Starting Claude...');
310
+ console.log('');
311
+
312
+ // Run Claude fully interactively (no flags - pure interactive mode)
313
+ const result = spawnSync(command, args, {
314
+ stdio: 'inherit', // Full terminal access
315
+ shell: true,
316
+ timeout
317
+ });
318
+
319
+ // Clean up temp file
320
+ try {
321
+ fs.unlinkSync(tempFile);
322
+ } catch (e) {
323
+ logger.debug('claude-client - executeClaudeInteractive', 'Temp file cleanup', { error: e.message });
324
+ }
325
+
326
+ if (result.error) {
327
+ logger.error('claude-client - executeClaudeInteractive', 'Spawn error', result.error);
328
+ reject(new ClaudeClientError('Failed to start Claude', { cause: result.error }));
329
+ return;
330
+ }
331
+
332
+ if (result.status === 0 || result.status === null) {
333
+ console.log('');
334
+ resolve('interactive-session-completed');
335
+ } else if (result.signal === 'SIGTERM') {
336
+ reject(new ClaudeClientError('Claude session timed out', { context: { timeout } }));
337
+ } else {
338
+ logger.error('claude-client - executeClaudeInteractive', 'Claude exited with error', {
339
+ status: result.status,
340
+ signal: result.signal
341
+ });
342
+ reject(new ClaudeClientError(`Claude exited with code ${result.status}`, {
343
+ context: { exitCode: result.status, signal: result.signal }
344
+ }));
345
+ }
346
+ });
347
+ };
348
+
250
349
  /**
251
350
  * Extracts JSON from Claude's response
252
351
  * Why: Claude may include markdown formatting or explanatory text around JSON
@@ -485,9 +584,13 @@ const analyzeCodeParallel = async (prompts, options = {}) => {
485
584
  export {
486
585
  ClaudeClientError,
487
586
  executeClaude,
587
+ executeClaudeInteractive,
488
588
  extractJSON,
489
589
  saveDebugResponse,
490
590
  analyzeCode,
491
591
  analyzeCodeParallel,
492
- chunkArray
592
+ chunkArray,
593
+ isWindows,
594
+ isWSLAvailable,
595
+ getClaudeCommand
493
596
  };
@@ -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
  };