claude-git-hooks 2.18.1 → 2.20.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/CLAUDE.md +85 -38
  3. package/README.md +52 -18
  4. package/bin/claude-hooks +75 -89
  5. package/lib/cli-metadata.js +306 -0
  6. package/lib/commands/analyze-diff.js +12 -10
  7. package/lib/commands/analyze.js +9 -5
  8. package/lib/commands/bump-version.js +56 -40
  9. package/lib/commands/create-pr.js +71 -34
  10. package/lib/commands/debug.js +4 -7
  11. package/lib/commands/diff-batch-info.js +105 -0
  12. package/lib/commands/generate-changelog.js +3 -2
  13. package/lib/commands/help.js +47 -27
  14. package/lib/commands/helpers.js +66 -43
  15. package/lib/commands/hooks.js +15 -13
  16. package/lib/commands/install.js +546 -49
  17. package/lib/commands/migrate-config.js +8 -11
  18. package/lib/commands/presets.js +6 -13
  19. package/lib/commands/setup-github.js +12 -3
  20. package/lib/commands/telemetry-cmd.js +8 -6
  21. package/lib/commands/update.js +1 -2
  22. package/lib/config.js +36 -52
  23. package/lib/hooks/pre-commit.js +70 -64
  24. package/lib/hooks/prepare-commit-msg.js +35 -75
  25. package/lib/utils/analysis-engine.js +77 -54
  26. package/lib/utils/changelog-generator.js +63 -37
  27. package/lib/utils/claude-client.js +447 -438
  28. package/lib/utils/claude-diagnostics.js +20 -10
  29. package/lib/utils/diff-analysis-orchestrator.js +332 -0
  30. package/lib/utils/file-operations.js +51 -79
  31. package/lib/utils/file-utils.js +6 -7
  32. package/lib/utils/git-operations.js +140 -123
  33. package/lib/utils/git-tag-manager.js +24 -23
  34. package/lib/utils/github-api.js +85 -61
  35. package/lib/utils/github-client.js +12 -14
  36. package/lib/utils/installation-diagnostics.js +4 -4
  37. package/lib/utils/interactive-ui.js +29 -17
  38. package/lib/utils/judge.js +195 -0
  39. package/lib/utils/logger.js +4 -1
  40. package/lib/utils/package-info.js +0 -11
  41. package/lib/utils/pr-metadata-engine.js +67 -33
  42. package/lib/utils/preset-loader.js +20 -62
  43. package/lib/utils/prompt-builder.js +57 -68
  44. package/lib/utils/resolution-prompt.js +34 -52
  45. package/lib/utils/sanitize.js +20 -19
  46. package/lib/utils/task-id.js +27 -40
  47. package/lib/utils/telemetry.js +73 -25
  48. package/lib/utils/version-manager.js +147 -70
  49. package/lib/utils/which-command.js +23 -12
  50. package/package.json +1 -1
  51. package/templates/CLAUDE_RESOLUTION_PROMPT.md +17 -9
  52. package/templates/DIFF_ANALYSIS_ORCHESTRATION_PROMPT.md +70 -0
  53. package/templates/config.advanced.example.json +15 -31
  54. package/templates/config.example.json +0 -11
@@ -88,11 +88,9 @@ const getCurrentLogFile = () => {
88
88
  *
89
89
  * @returns {boolean} True if telemetry is enabled (default: true)
90
90
  */
91
- const isTelemetryEnabled = () =>
91
+ const isTelemetryEnabled = () =>
92
92
  // Enabled by default - only disabled if explicitly set to false
93
- config.system?.telemetry !== false
94
- ;
95
-
93
+ config.system?.telemetry !== false;
96
94
  /**
97
95
  * Ensure telemetry directory exists
98
96
  * Why: Create on first use
@@ -115,7 +113,7 @@ const ensureTelemetryDir = async () => {
115
113
  const appendEvent = async (event) => {
116
114
  try {
117
115
  const logFile = getCurrentLogFile();
118
- const line = `${JSON.stringify(event) }\n`;
116
+ const line = `${JSON.stringify(event)}\n`;
119
117
 
120
118
  // Append to file (create if doesn't exist)
121
119
  await fs.appendFile(logFile, line, 'utf8');
@@ -204,6 +202,8 @@ export const recordJsonParseFailure = async (options) => {
204
202
  * @param {Object} options - Success context
205
203
  * @param {number} options.retryAttempt - Current retry attempt (0-based)
206
204
  * @param {number} options.totalRetries - Total retry attempts configured
205
+ * @param {string} options.batchModel - Model used for this specific batch (orchestrated mode)
206
+ * @param {number} options.orchestrationTime - Time spent in orchestration (ms, first batch only)
207
207
  */
208
208
  export const recordBatchSuccess = async (options) => {
209
209
  await recordEvent(
@@ -214,7 +214,9 @@ export const recordBatchSuccess = async (options) => {
214
214
  batchIndex: options.batchIndex ?? -1,
215
215
  totalBatches: options.totalBatches || 0,
216
216
  model: options.model || 'unknown',
217
+ batchModel: options.batchModel || options.model || 'unknown',
217
218
  duration: options.duration || 0,
219
+ orchestrationTime: options.orchestrationTime || 0,
218
220
  responseLength: options.responseLength || 0,
219
221
  parallelMode: (options.totalBatches || 0) > 1,
220
222
  hook: options.hook || 'unknown'
@@ -240,7 +242,7 @@ const readTelemetryEvents = async (maxDays = 7) => {
240
242
  const files = await fs.readdir(dir);
241
243
 
242
244
  // Filter to .jsonl files only
243
- const logFiles = files.filter(f => f.endsWith('.jsonl'));
245
+ const logFiles = files.filter((f) => f.endsWith('.jsonl'));
244
246
 
245
247
  // Sort by date (newest first)
246
248
  logFiles.sort().reverse();
@@ -262,7 +264,11 @@ const readTelemetryEvents = async (maxDays = 7) => {
262
264
  events.push(JSON.parse(line));
263
265
  } catch (parseError) {
264
266
  // Skip invalid lines
265
- logger.debug('telemetry - readTelemetryEvents', 'Invalid JSON line', parseError);
267
+ logger.debug(
268
+ 'telemetry - readTelemetryEvents',
269
+ 'Invalid JSON line',
270
+ parseError
271
+ );
266
272
  }
267
273
  }
268
274
  }
@@ -286,7 +292,8 @@ export const getStatistics = async (maxDays = 7) => {
286
292
  if (!isTelemetryEnabled()) {
287
293
  return {
288
294
  enabled: false,
289
- message: 'Telemetry is disabled. To enable (default), remove or set "system.telemetry: true" in .claude/config.json'
295
+ message:
296
+ 'Telemetry is disabled. To enable (default), remove or set "system.telemetry: true" in .claude/config.json'
290
297
  };
291
298
  }
292
299
 
@@ -299,26 +306,34 @@ export const getStatistics = async (maxDays = 7) => {
299
306
  totalEvents: events.length,
300
307
  jsonParseFailures: 0,
301
308
  batchSuccesses: 0,
302
- failuresByBatchSize: {},
309
+ failuresByFileCount: {},
303
310
  failuresByModel: {},
304
311
  failuresByHook: {},
305
312
  successesByHook: {},
306
313
  avgFilesPerFailure: 0,
307
314
  avgFilesPerSuccess: 0,
308
- failureRate: 0
315
+ failureRate: 0,
316
+ avgAnalysisTimeByModel: {},
317
+ avgOrchestrationTime: 0
309
318
  };
310
319
 
311
320
  let totalFilesInFailures = 0;
312
321
  let totalFilesInSuccesses = 0;
313
322
 
314
- events.forEach(event => {
323
+ // For per-model timing aggregation
324
+ const modelDurations = {}; // model -> [durations]
325
+ let totalOrchestrationTime = 0;
326
+ let orchestrationCount = 0;
327
+
328
+ events.forEach((event) => {
315
329
  if (event.type === 'json_parse_failure') {
316
330
  stats.jsonParseFailures++;
317
331
  totalFilesInFailures += event.data.fileCount || 0;
318
332
 
319
- // Group by batch size
320
- const batchSize = event.data.batchSize || 0;
321
- stats.failuresByBatchSize[batchSize] = (stats.failuresByBatchSize[batchSize] || 0) + 1;
333
+ // Group by file count
334
+ const fileCount = event.data.fileCount || 0;
335
+ stats.failuresByFileCount[fileCount] =
336
+ (stats.failuresByFileCount[fileCount] || 0) + 1;
322
337
 
323
338
  // Group by model
324
339
  const model = event.data.model || 'unknown';
@@ -327,7 +342,6 @@ export const getStatistics = async (maxDays = 7) => {
327
342
  // Group by hook
328
343
  const hook = event.data.hook || 'unknown';
329
344
  stats.failuresByHook[hook] = (stats.failuresByHook[hook] || 0) + 1;
330
-
331
345
  } else if (event.type === 'batch_success') {
332
346
  stats.batchSuccesses++;
333
347
  totalFilesInSuccesses += event.data.fileCount || 0;
@@ -335,21 +349,52 @@ export const getStatistics = async (maxDays = 7) => {
335
349
  // Group by hook
336
350
  const hook = event.data.hook || 'unknown';
337
351
  stats.successesByHook[hook] = (stats.successesByHook[hook] || 0) + 1;
352
+
353
+ // Accumulate per-model timing
354
+ const batchModel = event.data.batchModel || event.data.model || 'unknown';
355
+ const duration = event.data.duration || 0;
356
+ if (duration > 0) {
357
+ if (!modelDurations[batchModel]) modelDurations[batchModel] = [];
358
+ modelDurations[batchModel].push(duration);
359
+ }
360
+
361
+ // Accumulate orchestration time (stored on first batch only)
362
+ if (event.data.orchestrationTime > 0) {
363
+ totalOrchestrationTime += event.data.orchestrationTime;
364
+ orchestrationCount++;
365
+ }
338
366
  }
339
367
  });
340
368
 
341
369
  // Calculate averages
342
370
  if (stats.jsonParseFailures > 0) {
343
- stats.avgFilesPerFailure = parseFloat((totalFilesInFailures / stats.jsonParseFailures).toFixed(2));
371
+ stats.avgFilesPerFailure = parseFloat(
372
+ (totalFilesInFailures / stats.jsonParseFailures).toFixed(2)
373
+ );
344
374
  }
345
375
  if (stats.batchSuccesses > 0) {
346
- stats.avgFilesPerSuccess = parseFloat((totalFilesInSuccesses / stats.batchSuccesses).toFixed(2));
376
+ stats.avgFilesPerSuccess = parseFloat(
377
+ (totalFilesInSuccesses / stats.batchSuccesses).toFixed(2)
378
+ );
347
379
  }
348
380
 
349
381
  // Calculate failure rate
350
382
  const totalAnalyses = stats.jsonParseFailures + stats.batchSuccesses;
351
383
  if (totalAnalyses > 0) {
352
- stats.failureRate = parseFloat((stats.jsonParseFailures / totalAnalyses * 100).toFixed(2));
384
+ stats.failureRate = parseFloat(
385
+ ((stats.jsonParseFailures / totalAnalyses) * 100).toFixed(2)
386
+ );
387
+ }
388
+
389
+ // Calculate avg analysis time per model
390
+ for (const [model, durations] of Object.entries(modelDurations)) {
391
+ const avg = durations.reduce((a, b) => a + b, 0) / durations.length;
392
+ stats.avgAnalysisTimeByModel[model] = Math.round(avg);
393
+ }
394
+
395
+ // Calculate avg orchestration time
396
+ if (orchestrationCount > 0) {
397
+ stats.avgOrchestrationTime = Math.round(totalOrchestrationTime / orchestrationCount);
353
398
  }
354
399
 
355
400
  return stats;
@@ -379,7 +424,7 @@ export const rotateTelemetry = async (maxDays = 30) => {
379
424
  const files = await fs.readdir(dir);
380
425
 
381
426
  // Filter to .jsonl files
382
- const logFiles = files.filter(f => f.endsWith('.jsonl'));
427
+ const logFiles = files.filter((f) => f.endsWith('.jsonl'));
383
428
 
384
429
  // Calculate cutoff date
385
430
  const cutoffDate = new Date();
@@ -395,7 +440,10 @@ export const rotateTelemetry = async (maxDays = 30) => {
395
440
  if (fileDate < cutoffStr) {
396
441
  const filePath = path.join(dir, file);
397
442
  await fs.unlink(filePath);
398
- logger.debug('telemetry - rotateTelemetry', `Deleted old telemetry file: ${file}`);
443
+ logger.debug(
444
+ 'telemetry - rotateTelemetry',
445
+ `Deleted old telemetry file: ${file}`
446
+ );
399
447
  }
400
448
  }
401
449
  }
@@ -420,7 +468,7 @@ export const clearTelemetry = async () => {
420
468
 
421
469
  // Delete all .jsonl files
422
470
  const files = await fs.readdir(dir);
423
- const logFiles = files.filter(f => f.endsWith('.jsonl'));
471
+ const logFiles = files.filter((f) => f.endsWith('.jsonl'));
424
472
 
425
473
  for (const file of logFiles) {
426
474
  const filePath = path.join(dir, file);
@@ -471,11 +519,11 @@ export const displayStatistics = async () => {
471
519
  console.log(`📊 Failure rate: ${stats.failureRate}%\n`);
472
520
 
473
521
  if (stats.jsonParseFailures > 0) {
474
- console.log('━━━ FAILURES BY BATCH SIZE ━━━');
475
- Object.entries(stats.failuresByBatchSize)
522
+ console.log('━━━ FAILURES BY FILE COUNT ━━━');
523
+ Object.entries(stats.failuresByFileCount)
476
524
  .sort((a, b) => b[1] - a[1])
477
- .forEach(([size, count]) => {
478
- console.log(` Batch size ${size}: ${count} failures`);
525
+ .forEach(([fileCount, count]) => {
526
+ console.log(` ${fileCount} file(s): ${count} failures`);
479
527
  });
480
528
  console.log();
481
529