claude-git-hooks 2.14.1 → 2.15.5

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +118 -3
  2. package/LICENSE +20 -20
  3. package/README.md +4 -1
  4. package/bin/claude-hooks +84 -84
  5. package/lib/commands/analyze-diff.js +0 -1
  6. package/lib/commands/bump-version.js +23 -22
  7. package/lib/commands/create-pr.js +157 -15
  8. package/lib/commands/debug.js +1 -1
  9. package/lib/commands/helpers.js +11 -6
  10. package/lib/commands/install.js +7 -8
  11. package/lib/commands/migrate-config.js +24 -2
  12. package/lib/commands/setup-github.js +1 -1
  13. package/lib/commands/update.js +1 -1
  14. package/lib/config.js +0 -4
  15. package/lib/hooks/prepare-commit-msg.js +2 -2
  16. package/lib/utils/changelog-generator.js +6 -8
  17. package/lib/utils/claude-client.js +7 -6
  18. package/lib/utils/claude-diagnostics.js +14 -21
  19. package/lib/utils/file-operations.js +1 -1
  20. package/lib/utils/file-utils.js +0 -1
  21. package/lib/utils/git-operations.js +0 -1
  22. package/lib/utils/github-api.js +3 -3
  23. package/lib/utils/github-client.js +2 -2
  24. package/lib/utils/installation-diagnostics.js +1 -1
  25. package/lib/utils/prompt-builder.js +4 -6
  26. package/lib/utils/sanitize.js +13 -14
  27. package/lib/utils/task-id.js +18 -20
  28. package/lib/utils/telemetry.js +5 -7
  29. package/package.json +13 -5
  30. package/templates/config.advanced.example.json +113 -113
  31. package/templates/pre-commit +7 -0
  32. package/templates/presets/ai/config.json +5 -5
  33. package/templates/presets/backend/config.json +5 -5
  34. package/templates/presets/backend/preset.json +49 -49
  35. package/templates/presets/database/config.json +5 -5
  36. package/templates/presets/database/preset.json +38 -38
  37. package/templates/presets/default/config.json +5 -5
  38. package/templates/presets/default/preset.json +53 -53
  39. package/templates/presets/frontend/config.json +5 -5
  40. package/templates/presets/frontend/preset.json +50 -50
  41. package/templates/presets/fullstack/config.json +5 -5
  42. package/templates/presets/fullstack/preset.json +55 -55
@@ -43,7 +43,7 @@ export async function runCreatePr(args) {
43
43
 
44
44
  // Import GitHub API module
45
45
  logger.debug('create-pr', 'Importing GitHub API modules');
46
- const { createPullRequest, GitHubAPIError, validateToken, findExistingPR } = await import('../utils/github-api.js');
46
+ const { createPullRequest, validateToken, findExistingPR } = await import('../utils/github-api.js');
47
47
  const { parseGitHubRepo } = await import('../utils/github-client.js');
48
48
 
49
49
  showInfo('🚀 Creating Pull Request...');
@@ -300,39 +300,181 @@ export async function runCreatePr(args) {
300
300
  }
301
301
  }
302
302
 
303
- // Step 5.7: Check for unpushed tags (Issue #44)
304
- logger.debug('create-pr', 'Step 5.7: Checking for unpushed tags');
305
- const { compareLocalAndRemoteTags, pushTags: pushTagsUtil } = await import('../utils/git-tag-manager.js');
303
+ // Step 5.7: Smart tag pushing (Issue #44)
304
+ logger.debug('create-pr', 'Step 5.7: Checking and pushing unpushed tags');
305
+ const {
306
+ compareLocalAndRemoteTags,
307
+ pushTags: pushTagsUtil,
308
+ getLatestLocalTag,
309
+ getLatestRemoteTag,
310
+ parseTagVersion
311
+ } = await import('../utils/git-tag-manager.js');
312
+ const { compareVersions } = await import('../utils/version-manager.js');
313
+
306
314
  const tagComparison = await compareLocalAndRemoteTags();
307
315
 
308
316
  if (tagComparison.localNewer.length > 0) {
309
- showWarning(`Local tags not pushed: ${tagComparison.localNewer.join(', ')}`);
310
- console.log('');
317
+ // Get latest local and remote tags for comparison
318
+ const latestLocalTag = getLatestLocalTag();
319
+ const latestRemoteTag = await getLatestRemoteTag();
320
+
321
+ const localVersion = latestLocalTag ? parseTagVersion(latestLocalTag) : null;
322
+ const remoteVersion = latestRemoteTag ? parseTagVersion(latestRemoteTag) : null;
323
+
324
+ logger.debug('create-pr', 'Tag comparison details', {
325
+ localTag: latestLocalTag,
326
+ remoteTag: latestRemoteTag,
327
+ localVersion,
328
+ remoteVersion,
329
+ unpushedTags: tagComparison.localNewer
330
+ });
311
331
 
312
- const shouldPushTags = await promptConfirmation(
313
- 'Push tags to remote?',
314
- true // default yes
315
- );
332
+ let shouldPush = false;
333
+ let userChoice = null;
334
+
335
+ // Case 1: Local tag > Remote tag → Auto-push
336
+ if (localVersion && remoteVersion && compareVersions(localVersion, remoteVersion) > 0) {
337
+ logger.debug('create-pr', 'Local version > remote version, auto-pushing', {
338
+ localVersion,
339
+ remoteVersion
340
+ });
341
+
342
+ showInfo(`Local tag ${latestLocalTag} is newer than remote ${latestRemoteTag}`);
343
+ showInfo('Auto-pushing tag to remote...');
344
+ shouldPush = true;
345
+
346
+ // Case 2: Local tag = Remote tag → Prompt with warning
347
+ } else if (localVersion && remoteVersion && compareVersions(localVersion, remoteVersion) === 0) {
348
+ logger.debug('create-pr', 'Local version = remote version, prompting user', {
349
+ localVersion,
350
+ remoteVersion
351
+ });
352
+
353
+ showWarning('Local and remote tags have the same version:');
354
+ console.log(` Local: ${latestLocalTag} (${localVersion})`);
355
+ console.log(` Remote: ${latestRemoteTag} (${remoteVersion})`);
356
+ console.log('');
357
+ console.log('This might indicate the tag was already pushed.');
358
+ console.log('');
359
+
360
+ userChoice = await promptMenu(
361
+ 'What would you like to do?',
362
+ [
363
+ { key: 'c', label: 'Continue without pushing' },
364
+ { key: 'p', label: 'Force push tag' },
365
+ { key: 'a', label: 'Abort PR creation' }
366
+ ],
367
+ 'c'
368
+ );
369
+
370
+ if (userChoice === 'a') {
371
+ showInfo('PR creation cancelled');
372
+ process.exit(0);
373
+ } else if (userChoice === 'p') {
374
+ shouldPush = true;
375
+ }
376
+
377
+ // Case 3: Local tag < Remote tag → Prompt with error
378
+ } else if (localVersion && remoteVersion && compareVersions(localVersion, remoteVersion) < 0) {
379
+ logger.debug('create-pr', 'Local version < remote version, prompting user', {
380
+ localVersion,
381
+ remoteVersion
382
+ });
383
+
384
+ showError('Local tag is older than remote tag:');
385
+ console.log(` Local: ${latestLocalTag} (${localVersion})`);
386
+ console.log(` Remote: ${latestRemoteTag} (${remoteVersion})`);
387
+ console.log('');
388
+ console.log('This usually means you need to pull latest tags:');
389
+ console.log(' git fetch --tags');
390
+ console.log('');
391
+
392
+ userChoice = await promptMenu(
393
+ 'What would you like to do?',
394
+ [
395
+ { key: 'a', label: 'Abort PR creation' },
396
+ { key: 'c', label: 'Continue anyway (not recommended)' }
397
+ ],
398
+ 'a'
399
+ );
400
+
401
+ if (userChoice === 'a') {
402
+ showInfo('PR creation cancelled');
403
+ process.exit(0);
404
+ }
405
+
406
+ // Case 4: No version comparison possible → Prompt with info
407
+ } else {
408
+ logger.debug('create-pr', 'Cannot compare versions, prompting user', {
409
+ localTag: latestLocalTag,
410
+ remoteTag: latestRemoteTag,
411
+ hasLocalVersion: !!localVersion,
412
+ hasRemoteVersion: !!remoteVersion
413
+ });
414
+
415
+ showWarning('Unable to compare tag versions:');
416
+ console.log(` Local tag: ${latestLocalTag || 'none'}`);
417
+ console.log(` Remote tag: ${latestRemoteTag || 'none'}`);
418
+ console.log('');
419
+ console.log('Unpushed tags:', tagComparison.localNewer.join(', '));
420
+ console.log('');
421
+
422
+ userChoice = await promptMenu(
423
+ 'What would you like to do?',
424
+ [
425
+ { key: 'p', label: 'Push tags to remote' },
426
+ { key: 'c', label: 'Continue without pushing' },
427
+ { key: 'a', label: 'Abort PR creation' }
428
+ ],
429
+ 'p'
430
+ );
431
+
432
+ if (userChoice === 'a') {
433
+ showInfo('PR creation cancelled');
434
+ process.exit(0);
435
+ } else if (userChoice === 'p') {
436
+ shouldPush = true;
437
+ }
438
+ }
439
+
440
+ // Execute push if decided
441
+ if (shouldPush) {
442
+ console.log('');
443
+ showInfo('Pushing tags to remote...');
316
444
 
317
- if (shouldPushTags) {
318
- showInfo('Pushing tags...');
319
445
  const pushResult = pushTagsUtil(null, tagComparison.localNewer);
320
446
 
321
447
  if (pushResult.success) {
322
- showSuccess('Tags pushed successfully');
448
+ showSuccess('Tags pushed successfully');
449
+ logger.debug('create-pr', 'Tags pushed', {
450
+ pushed: tagComparison.localNewer
451
+ });
323
452
  } else {
324
453
  showError(`Failed to push some tags: ${pushResult.error}`);
325
454
  if (pushResult.failed.length > 0) {
326
455
  console.log('Failed tags:');
327
456
  pushResult.failed.forEach(f => console.log(` - ${f.tag}: ${f.error}`));
328
457
  }
458
+
459
+ const shouldContinue = await promptConfirmation(
460
+ 'Continue creating PR despite push failure?',
461
+ false
462
+ );
463
+
464
+ if (!shouldContinue) {
465
+ showInfo('PR creation cancelled');
466
+ process.exit(0);
467
+ }
329
468
  }
330
469
 
331
470
  console.log('');
332
- } else {
333
- showInfo('Tag push skipped');
471
+ } else if (userChoice !== 'c') {
472
+ // Auto-push failed or user chose to continue
473
+ logger.debug('create-pr', 'Tag push skipped', { reason: 'user choice or condition' });
334
474
  console.log('');
335
475
  }
476
+ } else {
477
+ logger.debug('create-pr', 'No unpushed tags found, continuing');
336
478
  }
337
479
 
338
480
  // Step 6: Generate PR metadata using engine
@@ -30,7 +30,7 @@ export async function runSetDebug(value) {
30
30
  const config = await getConfig();
31
31
  const isEnabled = config.system.debug || false;
32
32
  console.log('');
33
- info(`Debug mode: ${isEnabled ? colors.green + 'enabled' + colors.reset : colors.red + 'disabled' + colors.reset}`);
33
+ info(`Debug mode: ${isEnabled ? `${colors.green }enabled${ colors.reset}` : `${colors.red }disabled${ colors.reset}`}`);
34
34
  console.log('');
35
35
  } catch (err) {
36
36
  error(`Failed to check debug status: ${err.message}`);
@@ -193,10 +193,15 @@ export async function updateConfig(propertyPath, value, options = {}) {
193
193
  }
194
194
 
195
195
  // Set value at propertyPath (support dot notation like 'system.debug')
196
- // For v2.8.0 format, write inside 'overrides' so loadUserConfig picks it up
196
+ // For v2.8.0 format, write inside 'overrides' EXCEPT for top-level fields
197
197
  const pathParts = propertyPath.split('.');
198
198
  let current = config;
199
- if (config.version === '2.8.0') {
199
+
200
+ // Top-level fields that should NOT go into overrides
201
+ const topLevelFields = ['preset', 'version'];
202
+ const isTopLevelField = pathParts.length === 1 && topLevelFields.includes(pathParts[0]);
203
+
204
+ if (config.version === '2.8.0' && !isTopLevelField) {
200
205
  if (!config.overrides || typeof config.overrides !== 'object') {
201
206
  config.overrides = {};
202
207
  }
@@ -263,12 +268,12 @@ export function getLatestVersion(packageName) {
263
268
  */
264
269
  export class Entertainment {
265
270
  static jokes = [
266
- "Why do programmers prefer dark mode? Because light attracts bugs!",
267
- "A QA engineer walks into a bar. Orders 1 beer. Orders 0 beers. Orders -1 beers.",
271
+ 'Why do programmers prefer dark mode? Because light attracts bugs!',
272
+ 'A QA engineer walks into a bar. Orders 1 beer. Orders 0 beers. Orders -1 beers.',
268
273
  "What's a pirate's favorite programming language? R!",
269
274
  "There are 10 types of people: those who understand binary and those who don't.",
270
- "Why do programmers confuse Halloween with Christmas? Because Oct 31 = Dec 25",
271
- "What does one bit say to another? See you on the bus!",
275
+ 'Why do programmers confuse Halloween with Christmas? Because Oct 31 = Dec 25',
276
+ 'What does one bit say to another? See you on the bus!',
272
277
  "Why don't Java and C++ get along? Because they have different views on pointers.",
273
278
  "My code doesn't have bugs, just undocumented features."
274
279
  ];
@@ -63,7 +63,7 @@ async function checkVersionAndPromptUpdate() {
63
63
  success('Update completed. Please run your command again.');
64
64
  process.exit(0); // Exit so user restarts the process
65
65
  } catch (e) {
66
- error('Error updating: ' + e.message);
66
+ error(`Error updating: ${ e.message}`);
67
67
  resolve(false);
68
68
  }
69
69
  } else {
@@ -158,10 +158,9 @@ async function checkClaudeAuth() {
158
158
 
159
159
  /**
160
160
  * Check complete dependencies (like setup-wsl.sh)
161
- * @param {string|null} sudoPassword - Not used in v2.0.0+
162
161
  * @param {boolean} skipAuth - Skip Claude verification
163
162
  */
164
- async function checkAndInstallDependencies(sudoPassword = null, skipAuth = false) {
163
+ async function checkAndInstallDependencies(skipAuth = false) {
165
164
  info('Checking system dependencies...');
166
165
 
167
166
  // Check Node.js
@@ -252,7 +251,7 @@ function updateGitignore() {
252
251
  }
253
252
 
254
253
  // Add the missing entries
255
- gitignoreContent += missingEntries.join('\n') + '\n';
254
+ gitignoreContent += `${missingEntries.join('\n') }\n`;
256
255
 
257
256
  // Write the updated file
258
257
  fs.writeFileSync(gitignorePath, gitignoreContent);
@@ -294,7 +293,7 @@ function configureGit() {
294
293
  }
295
294
 
296
295
  } catch (e) {
297
- warning('Error configuring Git: ' + e.message);
296
+ warning(`Error configuring Git: ${ e.message}`);
298
297
  }
299
298
  }
300
299
 
@@ -399,7 +398,7 @@ export async function runInstall(args) {
399
398
 
400
399
  // v2.0.0+: No sudo needed (pure Node.js, no system packages required)
401
400
  // Check dependencies
402
- await checkAndInstallDependencies(null, skipAuth);
401
+ await checkAndInstallDependencies(skipAuth);
403
402
 
404
403
  const templatesPath = getTemplatesPath();
405
404
  const hooksPath = '.git/hooks';
@@ -641,8 +640,8 @@ export async function runInstall(args) {
641
640
  const settingsLocalPath = path.join(claudeDir, 'settings.local.json');
642
641
  if (!fs.existsSync(settingsLocalPath)) {
643
642
  const settingsLocalContent = {
644
- "_comment": "Local settings - DO NOT COMMIT. This file is gitignored.",
645
- "githubToken": ""
643
+ '_comment': 'Local settings - DO NOT COMMIT. This file is gitignored.',
644
+ 'githubToken': ''
646
645
  };
647
646
  fs.writeFileSync(settingsLocalPath, JSON.stringify(settingsLocalContent, null, 2));
648
647
  info('settings.local.json created (add your GitHub token here)');
@@ -36,7 +36,29 @@ export async function runMigrateConfig() {
36
36
 
37
37
  // Check if already in v2.8.0 format
38
38
  if (rawConfig.version === '2.8.0') {
39
- success('✅ Config is already in v2.8.0 format.');
39
+ // Bug fix: Check if preset was incorrectly placed in overrides
40
+ let fixed = false;
41
+ if (rawConfig.overrides?.preset) {
42
+ info('🔧 Fixing incorrectly placed preset field...');
43
+
44
+ // If top-level preset doesn't exist or differs, use the one from overrides
45
+ if (!rawConfig.preset || rawConfig.preset !== rawConfig.overrides.preset) {
46
+ info(` Moving preset '${rawConfig.overrides.preset}' from overrides to top level`);
47
+ rawConfig.preset = rawConfig.overrides.preset;
48
+ }
49
+
50
+ // Remove from overrides
51
+ delete rawConfig.overrides.preset;
52
+
53
+ // Write back
54
+ fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 4));
55
+ fixed = true;
56
+ success('✅ Config fixed: preset moved to correct location.');
57
+ }
58
+
59
+ if (!fixed) {
60
+ success('✅ Config is already in v2.8.0 format.');
61
+ }
40
62
  return;
41
63
  }
42
64
 
@@ -87,7 +109,7 @@ export async function runMigrateConfig() {
87
109
  info('📖 See .claude/config.advanced.example.json for documentation');
88
110
  }
89
111
 
90
- console.log(`\n✨ New config:`);
112
+ console.log('\n✨ New config:');
91
113
  console.log(JSON.stringify(newConfig, null, 2));
92
114
  console.log(`\n💾 Old config backed up to: ${backupPath}`);
93
115
  console.log('\n💡 Many parameters are now hardcoded with sensible defaults');
@@ -71,7 +71,7 @@ export async function runSetupGitHub() {
71
71
  // Save token
72
72
  const saveResult = saveGitHubToken(trimmedToken);
73
73
  if (!saveResult.success) {
74
- showError('Failed to save token: ' + saveResult.error);
74
+ showError(`Failed to save token: ${ saveResult.error}`);
75
75
  return;
76
76
  }
77
77
  showSuccess(`Token saved to ${saveResult.path}`);
@@ -61,7 +61,7 @@ export async function runUpdate() {
61
61
  success('Update completed');
62
62
  await runInstall(['--force']);
63
63
  } catch (updateError) {
64
- error('Error updating: ' + updateError.message);
64
+ error(`Error updating: ${ updateError.message}`);
65
65
  }
66
66
  }
67
67
  }
package/lib/config.js CHANGED
@@ -26,12 +26,8 @@
26
26
 
27
27
  import fs from 'fs';
28
28
  import path from 'path';
29
- import { fileURLToPath } from 'url';
30
29
  import logger from './utils/logger.js';
31
30
 
32
- const __filename = fileURLToPath(import.meta.url);
33
- const __dirname = path.dirname(__filename);
34
-
35
31
  /**
36
32
  * Hardcoded defaults (v3.0.0)
37
33
  * These are NOT user-configurable - sensible defaults that work for everyone
@@ -184,7 +184,7 @@ const main = async () => {
184
184
  const taskId = await getOrPromptTaskId({
185
185
  prompt: false, // Don't prompt - just detect from branch
186
186
  required: false,
187
- config: config // Pass config for custom pattern
187
+ config // Pass config for custom pattern
188
188
  });
189
189
 
190
190
  // Note: getOrPromptTaskId() already logs the task ID if found
@@ -302,7 +302,7 @@ const main = async () => {
302
302
  }
303
303
 
304
304
  // Write to commit message file
305
- await fs.writeFile(commitMsgFile, message + '\n', 'utf8');
305
+ await fs.writeFile(commitMsgFile, `${message }\n`, 'utf8');
306
306
 
307
307
  const duration = ((Date.now() - startTime) / 1000).toFixed(2);
308
308
  console.error(`⏱️ Message generation completed in ${duration}s`);
@@ -175,7 +175,7 @@ export function formatChangelogSection(version, date, categories, isUnreleased =
175
175
  });
176
176
 
177
177
  const header = isUnreleased
178
- ? `## [Unreleased]\n`
178
+ ? '## [Unreleased]\n'
179
179
  : `## [${version}] - ${date}\n`;
180
180
 
181
181
  const sections = [];
@@ -200,7 +200,7 @@ export function formatChangelogSection(version, date, categories, isUnreleased =
200
200
  }
201
201
  }
202
202
 
203
- const content = header + '\n' + sections.join('');
203
+ const content = `${header }\n${ sections.join('')}`;
204
204
 
205
205
  logger.debug('changelog-generator - formatChangelogSection', 'Section formatted');
206
206
 
@@ -215,7 +215,6 @@ export function formatChangelogSection(version, date, categories, isUnreleased =
215
215
  * @param {string} options.version - Version string
216
216
  * @param {boolean} options.isReleaseVersion - True if final version (no suffix)
217
217
  * @param {string} options.baseBranch - Base branch for diff (default: 'main')
218
- * @param {Object} options.config - Configuration object
219
218
  * @returns {Promise<Object>} Generated entry: { content, categories }
220
219
  */
221
220
  export async function generateChangelogEntry(options) {
@@ -223,7 +222,6 @@ export async function generateChangelogEntry(options) {
223
222
  version,
224
223
  isReleaseVersion = false,
225
224
  baseBranch = 'main',
226
- config = {}
227
225
  } = options;
228
226
 
229
227
  logger.debug('changelog-generator - generateChangelogEntry', 'Generating CHANGELOG entry', {
@@ -270,7 +268,7 @@ export async function generateChangelogEntry(options) {
270
268
 
271
269
  // Truncate diff if too large
272
270
  const truncatedDiff = fullDiff.length > 30000
273
- ? fullDiff.substring(0, 30000) + '\n... (truncated)'
271
+ ? `${fullDiff.substring(0, 30000) }\n... (truncated)`
274
272
  : fullDiff;
275
273
 
276
274
  // Load prompt template
@@ -359,14 +357,14 @@ y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.h
359
357
  if (headerEnd !== -1) {
360
358
  const before = existingContent.substring(0, match.index + headerEnd);
361
359
  const after = existingContent.substring(match.index + headerEnd);
362
- updatedContent = before + newContent + '\n' + after;
360
+ updatedContent = `${before + newContent }\n${ after}`;
363
361
  } else {
364
362
  // No existing versions, append after header
365
- updatedContent = existingContent + '\n' + newContent;
363
+ updatedContent = `${existingContent }\n${ newContent}`;
366
364
  }
367
365
  } else {
368
366
  // No header found, prepend content
369
- updatedContent = newContent + '\n' + existingContent;
367
+ updatedContent = `${newContent }\n${ existingContent}`;
370
368
  }
371
369
 
372
370
  fs.writeFileSync(changelogPath, updatedContent, 'utf8');
@@ -246,6 +246,7 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) =>
246
246
 
247
247
  // Handle process completion
248
248
  claude.on('close', (code) => {
249
+ clearTimeout(timeoutId);
249
250
  const elapsedTime = Date.now() - startTime;
250
251
 
251
252
  // Check for "Execution error" even when exit code is 0
@@ -342,6 +343,7 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) =>
342
343
 
343
344
  // Handle errors
344
345
  claude.on('error', (error) => {
346
+ clearTimeout(timeoutId);
345
347
  const elapsedTime = Date.now() - startTime;
346
348
 
347
349
  logger.error(
@@ -390,9 +392,6 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) =>
390
392
  }));
391
393
  }, timeout);
392
394
 
393
- // Clear timeout if process completes
394
- claude.on('close', () => clearTimeout(timeoutId));
395
-
396
395
  // Write prompt to stdin
397
396
  // Why: Claude CLI reads prompt from stdin, not command arguments
398
397
 
@@ -400,6 +399,7 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) =>
400
399
  // Why: write() failures can emit 'error' events asynchronously
401
400
  // Common in parallel execution with large prompts
402
401
  claude.stdin.on('error', (error) => {
402
+ clearTimeout(timeoutId);
403
403
  logger.error(
404
404
  'claude-client - executeClaude',
405
405
  'stdin stream error (process may have terminated early)',
@@ -426,6 +426,7 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) =>
426
426
  claude.stdin.write(prompt);
427
427
  claude.stdin.end();
428
428
  } catch (error) {
429
+ clearTimeout(timeoutId);
429
430
  logger.error(
430
431
  'claude-client - executeClaude',
431
432
  'Failed to write prompt to Claude CLI stdin (synchronous error)',
@@ -536,7 +537,7 @@ const executeClaudeInteractive = (prompt, { timeout = 300000 } = {}) => new Prom
536
537
  * @returns {Object} Parsed JSON object
537
538
  * @throws {ClaudeClientError} If no valid JSON found
538
539
  */
539
- const extractJSON = (response, telemetryContext = {}) => {
540
+ const extractJSON = (response) => {
540
541
  logger.debug(
541
542
  'claude-client - extractJSON',
542
543
  'Extracting JSON from response',
@@ -614,7 +615,7 @@ const extractJSON = (response, telemetryContext = {}) => {
614
615
  // Telemetry is now recorded by withRetry wrapper
615
616
  throw new ClaudeClientError('No valid JSON found in Claude response', {
616
617
  context: {
617
- response: response,
618
+ response,
618
619
  errorInfo: {
619
620
  type: 'JSON_PARSE_ERROR'
620
621
  }
@@ -797,7 +798,7 @@ const executeClaudeWithRetry = async (prompt, options = {}) => {
797
798
  () => executeClaude(prompt, executeOptions),
798
799
  {
799
800
  operationName: 'executeClaude',
800
- telemetryContext: telemetryContext
801
+ telemetryContext
801
802
  }
802
803
  );
803
804
  };
@@ -150,27 +150,26 @@ const formatTimingInfo = (errorInfo) => {
150
150
  * @returns {string} Formatted error message
151
151
  */
152
152
  export const formatClaudeError = (errorInfo) => {
153
- const lines = [];
154
153
 
155
154
  switch (errorInfo.type) {
156
- case ClaudeErrorType.EXECUTION_ERROR:
157
- return formatExecutionError(errorInfo);
155
+ case ClaudeErrorType.EXECUTION_ERROR:
156
+ return formatExecutionError(errorInfo);
158
157
 
159
- case ClaudeErrorType.RATE_LIMIT:
160
- return formatRateLimitError(errorInfo);
158
+ case ClaudeErrorType.RATE_LIMIT:
159
+ return formatRateLimitError(errorInfo);
161
160
 
162
- case ClaudeErrorType.AUTH_FAILED:
163
- return formatAuthError(errorInfo);
161
+ case ClaudeErrorType.AUTH_FAILED:
162
+ return formatAuthError(errorInfo);
164
163
 
165
- case ClaudeErrorType.NETWORK:
166
- return formatNetworkError(errorInfo);
164
+ case ClaudeErrorType.NETWORK:
165
+ return formatNetworkError(errorInfo);
167
166
 
168
- case ClaudeErrorType.INVALID_RESPONSE:
169
- return formatInvalidResponseError(errorInfo);
167
+ case ClaudeErrorType.INVALID_RESPONSE:
168
+ return formatInvalidResponseError(errorInfo);
170
169
 
171
- case ClaudeErrorType.GENERIC:
172
- default:
173
- return formatGenericError(errorInfo);
170
+ case ClaudeErrorType.GENERIC:
171
+ default:
172
+ return formatGenericError(errorInfo);
174
173
  }
175
174
  };
176
175
 
@@ -178,7 +177,6 @@ export const formatClaudeError = (errorInfo) => {
178
177
  * Format execution error
179
178
  */
180
179
  const formatExecutionError = (errorInfo) => {
181
- const { exitCode, stdout } = errorInfo;
182
180
  const lines = [];
183
181
 
184
182
  lines.push('❌ Claude CLI execution error');
@@ -243,7 +241,6 @@ const formatRateLimitError = (errorInfo) => {
243
241
  * Format authentication error
244
242
  */
245
243
  const formatAuthError = (errorInfo) => {
246
- const { exitCode } = errorInfo;
247
244
  const lines = [];
248
245
 
249
246
  lines.push('❌ Claude CLI authentication failed');
@@ -266,7 +263,6 @@ const formatAuthError = (errorInfo) => {
266
263
  * Format network error
267
264
  */
268
265
  const formatNetworkError = (errorInfo) => {
269
- const { exitCode } = errorInfo;
270
266
  const lines = [];
271
267
 
272
268
  lines.push('❌ Network error connecting to Claude API');
@@ -290,7 +286,6 @@ const formatNetworkError = (errorInfo) => {
290
286
  * Format invalid response error
291
287
  */
292
288
  const formatInvalidResponseError = (errorInfo) => {
293
- const { exitCode } = errorInfo;
294
289
  const lines = [];
295
290
 
296
291
  lines.push('❌ Claude returned invalid response');
@@ -350,9 +345,7 @@ const formatGenericError = (errorInfo) => {
350
345
  * @param {Object} errorInfo - Output from detectClaudeError()
351
346
  * @returns {boolean} True if error might resolve with retry
352
347
  */
353
- export const isRecoverableError = (errorInfo) => {
354
- return errorInfo.type === ClaudeErrorType.EXECUTION_ERROR ||
348
+ export const isRecoverableError = (errorInfo) => errorInfo.type === ClaudeErrorType.EXECUTION_ERROR ||
355
349
  errorInfo.type === ClaudeErrorType.RATE_LIMIT ||
356
350
  errorInfo.type === ClaudeErrorType.NETWORK ||
357
351
  errorInfo.type === ClaudeErrorType.TIMEOUT;
358
- };
@@ -174,7 +174,7 @@ const filterFiles = async (files, { maxSize = 100000, extensions = [] } = {}) =>
174
174
  maxSize,
175
175
  extensions,
176
176
  'process.cwd()': process.cwd(),
177
- files: files
177
+ files
178
178
  }
179
179
  );
180
180
 
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import fs from 'fs/promises';
7
- import fsSync from 'fs';
8
7
  import path from 'path';
9
8
  import { getRepoRoot } from './git-operations.js';
10
9
  import logger from './logger.js';
@@ -374,7 +374,6 @@ const getStagedStats = () => {
374
374
 
375
375
  try {
376
376
  const shortstat = execGitCommand('diff --cached --shortstat');
377
- const numstat = execGitCommand('diff --cached --numstat');
378
377
  const nameStatus = execGitCommand('diff --cached --name-status');
379
378
 
380
379
  // Parse shortstat: "X files changed, Y insertions(+), Z deletions(-)"
@@ -322,7 +322,7 @@ export const createPullRequest = async ({
322
322
  logger.debug('github-api - createPullRequest', 'Starting PR creation', {
323
323
  owner,
324
324
  repo,
325
- titlePreview: title.substring(0, 50) + '...',
325
+ titlePreview: `${title.substring(0, 50) }...`,
326
326
  titleLength: title.length,
327
327
  bodyLength: body.length,
328
328
  head,
@@ -424,8 +424,8 @@ export const createPullRequest = async ({
424
424
  title: pr.title,
425
425
  head: pr.head.ref,
426
426
  base: pr.base.ref,
427
- labels: labels,
428
- reviewers: reviewers
427
+ labels,
428
+ reviewers
429
429
  };
430
430
 
431
431
  logger.debug('github-api - createPullRequest', 'PR creation completed', result);