claude-git-hooks 1.5.5 → 2.1.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.
- package/CHANGELOG.md +87 -0
- package/README.md +102 -46
- package/bin/claude-hooks +121 -226
- package/lib/hooks/pre-commit.js +335 -0
- package/lib/hooks/prepare-commit-msg.js +283 -0
- package/lib/utils/claude-client.js +373 -0
- package/lib/utils/file-operations.js +422 -0
- package/lib/utils/git-operations.js +341 -0
- package/lib/utils/logger.js +141 -0
- package/lib/utils/prompt-builder.js +283 -0
- package/lib/utils/resolution-prompt.js +291 -0
- package/package.json +52 -40
- package/templates/pre-commit +58 -445
- package/templates/prepare-commit-msg +61 -151
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: claude-client.js
|
|
3
|
+
* Purpose: Interface with Claude CLI for code analysis
|
|
4
|
+
*
|
|
5
|
+
* Key responsibilities:
|
|
6
|
+
* - Execute Claude CLI with prompts
|
|
7
|
+
* - Parse JSON responses from Claude
|
|
8
|
+
* - Handle errors and timeouts
|
|
9
|
+
* - Optional debug output
|
|
10
|
+
*
|
|
11
|
+
* Dependencies:
|
|
12
|
+
* - child_process: For executing Claude CLI
|
|
13
|
+
* - fs/promises: For debug file writing
|
|
14
|
+
* - logger: Debug and error logging
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawn, execSync } from 'child_process';
|
|
18
|
+
import fs from 'fs/promises';
|
|
19
|
+
import os from 'os';
|
|
20
|
+
import logger from './logger.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Custom error for Claude client failures
|
|
24
|
+
*/
|
|
25
|
+
class ClaudeClientError extends Error {
|
|
26
|
+
constructor(message, { cause, output, context } = {}) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'ClaudeClientError';
|
|
29
|
+
this.cause = cause;
|
|
30
|
+
this.output = output;
|
|
31
|
+
this.context = context;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Detect if running on Windows
|
|
37
|
+
* Why: Need to use 'wsl claude' instead of 'claude' on Windows
|
|
38
|
+
*/
|
|
39
|
+
const isWindows = () => {
|
|
40
|
+
return os.platform() === 'win32' || process.env.OS === 'Windows_NT';
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if WSL is available on Windows
|
|
45
|
+
* Why: Windows users need WSL to run Claude CLI, verify it exists before attempting
|
|
46
|
+
*
|
|
47
|
+
* @returns {boolean} True if WSL is available
|
|
48
|
+
*/
|
|
49
|
+
const isWSLAvailable = () => {
|
|
50
|
+
if (!isWindows()) {
|
|
51
|
+
return true; // Not Windows, WSL check not needed
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Try to run wsl --version to check if WSL is installed
|
|
56
|
+
execSync('wsl --version', { stdio: 'ignore', timeout: 3000 });
|
|
57
|
+
return true;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logger.debug('claude-client - isWSLAvailable', 'WSL not available', error);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get Claude command configuration for current platform
|
|
66
|
+
* Why: On Windows, Claude CLI runs in WSL, so we need 'wsl' as command and 'claude' as arg
|
|
67
|
+
*
|
|
68
|
+
* @returns {Object} { command, args } - Command and base arguments
|
|
69
|
+
* @throws {ClaudeClientError} If Windows without WSL
|
|
70
|
+
*/
|
|
71
|
+
const getClaudeCommand = () => {
|
|
72
|
+
if (isWindows()) {
|
|
73
|
+
if (!isWSLAvailable()) {
|
|
74
|
+
throw new ClaudeClientError('WSL is required on Windows but not found', {
|
|
75
|
+
context: {
|
|
76
|
+
platform: 'Windows',
|
|
77
|
+
suggestion: 'Install WSL: https://docs.microsoft.com/en-us/windows/wsl/install'
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return { command: 'wsl', args: ['claude'] };
|
|
82
|
+
}
|
|
83
|
+
return { command: 'claude', args: [] };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Executes Claude CLI with a prompt
|
|
88
|
+
* Why: Centralized Claude CLI execution with error handling and timeout
|
|
89
|
+
* Why platform detection: On Windows, must use 'wsl claude' to access Claude in WSL
|
|
90
|
+
*
|
|
91
|
+
* @param {string} prompt - Prompt text to send to Claude
|
|
92
|
+
* @param {Object} options - Execution options
|
|
93
|
+
* @param {number} options.timeout - Timeout in milliseconds (default: 120000 = 2 minutes)
|
|
94
|
+
* @returns {Promise<string>} Claude's response
|
|
95
|
+
* @throws {ClaudeClientError} If execution fails or times out
|
|
96
|
+
*/
|
|
97
|
+
const executeClaude = (prompt, { timeout = 120000 } = {}) => {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
// Get platform-specific command
|
|
100
|
+
const { command, args } = getClaudeCommand();
|
|
101
|
+
const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command;
|
|
102
|
+
|
|
103
|
+
logger.debug(
|
|
104
|
+
'claude-client - executeClaude',
|
|
105
|
+
'Executing Claude CLI',
|
|
106
|
+
{ promptLength: prompt.length, timeout, command: fullCommand, isWindows: isWindows() }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const startTime = Date.now();
|
|
110
|
+
|
|
111
|
+
// Why: Use spawn instead of exec to handle large prompts and responses
|
|
112
|
+
// spawn streams data, exec buffers everything in memory
|
|
113
|
+
const claude = spawn(command, args, {
|
|
114
|
+
stdio: ['pipe', 'pipe', 'pipe'] // stdin, stdout, stderr
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
let stdout = '';
|
|
118
|
+
let stderr = '';
|
|
119
|
+
|
|
120
|
+
// Collect stdout
|
|
121
|
+
claude.stdout.on('data', (data) => {
|
|
122
|
+
stdout += data.toString();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Collect stderr
|
|
126
|
+
claude.stderr.on('data', (data) => {
|
|
127
|
+
stderr += data.toString();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Handle process completion
|
|
131
|
+
claude.on('close', (code) => {
|
|
132
|
+
const duration = Date.now() - startTime;
|
|
133
|
+
|
|
134
|
+
if (code === 0) {
|
|
135
|
+
logger.debug(
|
|
136
|
+
'claude-client - executeClaude',
|
|
137
|
+
'Claude CLI execution successful',
|
|
138
|
+
{ duration, outputLength: stdout.length }
|
|
139
|
+
);
|
|
140
|
+
resolve(stdout);
|
|
141
|
+
} else {
|
|
142
|
+
logger.error(
|
|
143
|
+
'claude-client - executeClaude',
|
|
144
|
+
`Claude CLI failed with exit code ${code}`,
|
|
145
|
+
new ClaudeClientError('Claude CLI execution failed', {
|
|
146
|
+
output: { stdout, stderr },
|
|
147
|
+
context: { exitCode: code, duration }
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
reject(new ClaudeClientError('Claude CLI execution failed', {
|
|
152
|
+
output: { stdout, stderr },
|
|
153
|
+
context: { exitCode: code, duration }
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Handle errors
|
|
159
|
+
claude.on('error', (error) => {
|
|
160
|
+
logger.error(
|
|
161
|
+
'claude-client - executeClaude',
|
|
162
|
+
'Failed to spawn Claude CLI process',
|
|
163
|
+
error
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
reject(new ClaudeClientError('Failed to spawn Claude CLI', {
|
|
167
|
+
cause: error,
|
|
168
|
+
context: { command, args }
|
|
169
|
+
}));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Set up timeout
|
|
173
|
+
const timeoutId = setTimeout(() => {
|
|
174
|
+
claude.kill();
|
|
175
|
+
logger.error(
|
|
176
|
+
'claude-client - executeClaude',
|
|
177
|
+
'Claude CLI execution timed out',
|
|
178
|
+
new ClaudeClientError('Claude CLI timeout', {
|
|
179
|
+
context: { timeout }
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
reject(new ClaudeClientError('Claude CLI execution timed out', {
|
|
184
|
+
context: { timeout }
|
|
185
|
+
}));
|
|
186
|
+
}, timeout);
|
|
187
|
+
|
|
188
|
+
// Clear timeout if process completes
|
|
189
|
+
claude.on('close', () => clearTimeout(timeoutId));
|
|
190
|
+
|
|
191
|
+
// Write prompt to stdin
|
|
192
|
+
// Why: Claude CLI reads prompt from stdin, not command arguments
|
|
193
|
+
try {
|
|
194
|
+
claude.stdin.write(prompt);
|
|
195
|
+
claude.stdin.end();
|
|
196
|
+
} catch (error) {
|
|
197
|
+
logger.error(
|
|
198
|
+
'claude-client - executeClaude',
|
|
199
|
+
'Failed to write prompt to Claude CLI stdin',
|
|
200
|
+
error
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
reject(new ClaudeClientError('Failed to write prompt', {
|
|
204
|
+
cause: error
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extracts JSON from Claude's response
|
|
212
|
+
* Why: Claude may include markdown formatting or explanatory text around JSON
|
|
213
|
+
*
|
|
214
|
+
* @param {string} response - Raw response from Claude
|
|
215
|
+
* @returns {Object} Parsed JSON object
|
|
216
|
+
* @throws {ClaudeClientError} If no valid JSON found
|
|
217
|
+
*/
|
|
218
|
+
const extractJSON = (response) => {
|
|
219
|
+
logger.debug(
|
|
220
|
+
'claude-client - extractJSON',
|
|
221
|
+
'Extracting JSON from response',
|
|
222
|
+
{ responseLength: response.length }
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Why: Try multiple patterns to find JSON
|
|
226
|
+
// Pattern 1: JSON in markdown code block
|
|
227
|
+
const markdownMatch = response.match(/```json\s*([\s\S]*?)\s*```/);
|
|
228
|
+
if (markdownMatch) {
|
|
229
|
+
try {
|
|
230
|
+
const json = JSON.parse(markdownMatch[1]);
|
|
231
|
+
logger.debug('claude-client - extractJSON', 'JSON extracted from markdown block');
|
|
232
|
+
return json;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
// Continue to next pattern
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Pattern 2: JSON object (curly braces)
|
|
239
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
240
|
+
if (jsonMatch) {
|
|
241
|
+
try {
|
|
242
|
+
const json = JSON.parse(jsonMatch[0]);
|
|
243
|
+
logger.debug('claude-client - extractJSON', 'JSON extracted from response body');
|
|
244
|
+
return json;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
// Continue to next pattern
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Pattern 3: Extract lines starting with { until matching }
|
|
251
|
+
const lines = response.split('\n');
|
|
252
|
+
let jsonStartIndex = -1;
|
|
253
|
+
let braceCount = 0;
|
|
254
|
+
let jsonLines = [];
|
|
255
|
+
|
|
256
|
+
for (let i = 0; i < lines.length; i++) {
|
|
257
|
+
const line = lines[i].trim();
|
|
258
|
+
|
|
259
|
+
if (jsonStartIndex === -1 && line.startsWith('{')) {
|
|
260
|
+
jsonStartIndex = i;
|
|
261
|
+
braceCount = (line.match(/{/g) || []).length - (line.match(/}/g) || []).length;
|
|
262
|
+
jsonLines.push(line);
|
|
263
|
+
} else if (jsonStartIndex !== -1) {
|
|
264
|
+
jsonLines.push(line);
|
|
265
|
+
braceCount += (line.match(/{/g) || []).length - (line.match(/}/g) || []).length;
|
|
266
|
+
|
|
267
|
+
if (braceCount === 0) {
|
|
268
|
+
const jsonText = jsonLines.join('\n');
|
|
269
|
+
try {
|
|
270
|
+
const json = JSON.parse(jsonText);
|
|
271
|
+
logger.debug('claude-client - extractJSON', 'JSON extracted using line matching');
|
|
272
|
+
return json;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
// Try next occurrence
|
|
275
|
+
jsonStartIndex = -1;
|
|
276
|
+
jsonLines = [];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// No valid JSON found
|
|
283
|
+
logger.error(
|
|
284
|
+
'claude-client - extractJSON',
|
|
285
|
+
'No valid JSON found in response',
|
|
286
|
+
new ClaudeClientError('No valid JSON in response', {
|
|
287
|
+
output: response.substring(0, 500) // First 500 chars for debugging
|
|
288
|
+
})
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
throw new ClaudeClientError('No valid JSON found in Claude response', {
|
|
292
|
+
output: response.substring(0, 500)
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Saves response to debug file
|
|
298
|
+
* Why: Helps troubleshoot issues with Claude responses
|
|
299
|
+
*
|
|
300
|
+
* @param {string} response - Raw response to save
|
|
301
|
+
* @param {string} filename - Debug filename (default: 'debug-claude-response.json')
|
|
302
|
+
*/
|
|
303
|
+
const saveDebugResponse = async (response, filename = 'debug-claude-response.json') => {
|
|
304
|
+
try {
|
|
305
|
+
await fs.writeFile(filename, response, 'utf8');
|
|
306
|
+
logger.debug(
|
|
307
|
+
'claude-client - saveDebugResponse',
|
|
308
|
+
`Debug response saved to ${filename}`
|
|
309
|
+
);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
logger.error(
|
|
312
|
+
'claude-client - saveDebugResponse',
|
|
313
|
+
'Failed to save debug response',
|
|
314
|
+
error
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Analyzes code using Claude CLI
|
|
321
|
+
* Why: High-level interface that handles execution, parsing, and debug
|
|
322
|
+
*
|
|
323
|
+
* @param {string} prompt - Analysis prompt
|
|
324
|
+
* @param {Object} options - Analysis options
|
|
325
|
+
* @param {number} options.timeout - Timeout in milliseconds
|
|
326
|
+
* @param {boolean} options.saveDebug - Save response to debug file (default: from DEBUG env)
|
|
327
|
+
* @returns {Promise<Object>} Parsed analysis result
|
|
328
|
+
* @throws {ClaudeClientError} If analysis fails
|
|
329
|
+
*/
|
|
330
|
+
const analyzeCode = async (prompt, { timeout = 120000, saveDebug = process.env.DEBUG === 'true' } = {}) => {
|
|
331
|
+
logger.debug(
|
|
332
|
+
'claude-client - analyzeCode',
|
|
333
|
+
'Starting code analysis',
|
|
334
|
+
{ promptLength: prompt.length, timeout, saveDebug }
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
// Execute Claude CLI
|
|
339
|
+
const response = await executeClaude(prompt, { timeout });
|
|
340
|
+
|
|
341
|
+
// Save debug if requested
|
|
342
|
+
if (saveDebug) {
|
|
343
|
+
await saveDebugResponse(response);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Extract and parse JSON
|
|
347
|
+
const result = extractJSON(response);
|
|
348
|
+
|
|
349
|
+
logger.debug(
|
|
350
|
+
'claude-client - analyzeCode',
|
|
351
|
+
'Analysis complete',
|
|
352
|
+
{
|
|
353
|
+
hasApproved: 'approved' in result,
|
|
354
|
+
hasQualityGate: 'QUALITY_GATE' in result,
|
|
355
|
+
blockingIssuesCount: result.blockingIssues?.length ?? 0
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
return result;
|
|
360
|
+
|
|
361
|
+
} catch (error) {
|
|
362
|
+
logger.error('claude-client - analyzeCode', 'Code analysis failed', error);
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
export {
|
|
368
|
+
ClaudeClientError,
|
|
369
|
+
executeClaude,
|
|
370
|
+
extractJSON,
|
|
371
|
+
saveDebugResponse,
|
|
372
|
+
analyzeCode
|
|
373
|
+
};
|