claude-git-hooks 2.8.0 → 2.9.1

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 CHANGED
@@ -5,6 +5,56 @@ Todos los cambios notables en este proyecto se documentarán en este archivo.
5
5
  El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.9.1] - 2025-12-30
9
+
10
+ ### 🐛 Fixed
11
+
12
+ - **Template path mismatch after v2.8.0 migration** - Fixed critical bug where templates were not found after config migration (#51)
13
+ - **What was broken**: After v2.8.0 moved templates from `.claude/` to `.claude/prompts/`, the code still looked in the old location
14
+ - **Root cause**: `prompt-builder.js` and `resolution-prompt.js` had hardcoded `.claude/` paths instead of `.claude/prompts/`
15
+ - **Symptom**: `ENOENT: no such file or directory` errors for `CLAUDE_ANALYSIS_PROMPT.md` and `CLAUDE_RESOLUTION_PROMPT.md`
16
+ - **Fix**: Updated default paths in template loading functions to `.claude/prompts/`
17
+ - **Files changed**:
18
+ - `lib/utils/prompt-builder.js:45,133,223,234-235,244` - Updated `loadTemplate`, `loadPrompt`, `buildAnalysisPrompt` defaults
19
+ - `lib/utils/resolution-prompt.js:183` - Updated `generateResolutionPrompt` template path
20
+ - **Impact**: Pre-commit analysis and resolution prompt generation now work correctly after v2.8.0+ installation
21
+
22
+ ### 🎯 User Experience
23
+
24
+ - **Before**: Users upgrading from v2.6.x to v2.8.0+ experienced "Template not found" errors even after successful installation
25
+ - **After**: Templates are found in correct `.claude/prompts/` directory as intended by v2.8.0 changes
26
+
27
+ ## [2.9.0] - 2025-12-26
28
+
29
+ ### ✨ Added
30
+
31
+ - **Local telemetry system with per-retry tracking** - Enabled by default for debugging JSON parsing failures (#50)
32
+ - **What it does**: Tracks JSON parsing failures and successes with full context, recording EVERY retry attempt individually
33
+ - **Key features**:
34
+ - **Granular tracking**: Records each retry attempt separately (not just final result)
35
+ - **Unique IDs**: Each event has unique ID (timestamp-counter-random) for deduplication
36
+ - **Success flag**: Boolean indicating whether attempt succeeded (true) or failed (false)
37
+ - **Retry metadata**: `retryAttempt` (0-3) and `totalRetries` (max configured)
38
+ - **Error types**: Tracks EXECUTION_ERROR, JSON_PARSE_ERROR, etc.
39
+ - **Hook coverage**: pre-commit, prepare-commit-msg, analyze-diff, create-pr
40
+ - **Privacy-first**: Local-only (`.claude/telemetry/`), no data leaves user's machine, no external transmission
41
+ - **Storage**: JSON lines format with automatic rotation (keeps last 30 days)
42
+ - **CLI commands**:
43
+ - `claude-hooks telemetry show` - Display statistics (failure rate, failures by batch size/model/hook, retry patterns)
44
+ - `claude-hooks telemetry clear` - Reset telemetry data
45
+ - **Enabled by default**: To disable, add `"system": { "telemetry": false }` to `.claude/config.json`
46
+ - **Files added**:
47
+ - `lib/utils/telemetry.js` - Telemetry collection with ID generation, retry tracking, and statistics
48
+ - **Files changed**:
49
+ - `lib/utils/claude-client.js:678-780,788-798` - Modified withRetry() to record telemetry on each attempt
50
+ - `lib/utils/claude-client.js:609-618,845-856` - Removed duplicate telemetry recordings
51
+ - `lib/hooks/pre-commit.js` - Pass telemetry context for analysis
52
+ - `lib/hooks/prepare-commit-msg.js` - Pass telemetry context for commit messages
53
+ - `lib/config.js` - Add telemetry config option (default: true)
54
+ - `bin/claude-hooks:1076-1091,1354-1366` - Add telemetry context to analyze-diff and create-pr
55
+ - `bin/claude-hooks` - Add telemetry CLI commands
56
+ - **Impact**: Enables diagnosis of retry patterns, identifies which retry attempts succeed, tracks transient vs persistent errors, helps optimize batch sizes and models
57
+
8
58
  ## [2.8.0] - 2025-12-24
9
59
 
10
60
  ### 🎨 Changed
package/README.md CHANGED
@@ -65,6 +65,12 @@ claude-hooks --debug true
65
65
 
66
66
  # Ver estado de debug
67
67
  claude-hooks --debug status
68
+
69
+ # Ver estadísticas de telemetría (v2.9.0+, habilitada por defecto)
70
+ claude-hooks telemetry show
71
+
72
+ # Limpiar datos de telemetría (v2.9.0+)
73
+ claude-hooks telemetry clear
68
74
  ```
69
75
 
70
76
  ### 📦 Instalación y Gestión
package/bin/claude-hooks CHANGED
@@ -16,6 +16,7 @@ import { getOrPromptTaskId, formatWithTaskId } from '../lib/utils/task-id.js';
16
16
  import { createPullRequest, getReviewersForFiles, parseGitHubRepo, setupGitHubMcp, getGitHubMcpStatus } from '../lib/utils/github-client.js';
17
17
  import { showPRPreview, promptConfirmation, promptMenu, showSuccess, showError, showInfo, showWarning, showSpinner, promptEditField } from '../lib/utils/interactive-ui.js';
18
18
  import { setupGitHubMCP } from '../lib/utils/mcp-setup.js';
19
+ import { displayStatistics as showTelemetryStats, clearTelemetry as clearTelemetryData } from '../lib/utils/telemetry.js';
19
20
  import logger from '../lib/utils/logger.js';
20
21
 
21
22
  // Why: ES6 modules don't have __dirname, need to recreate it
@@ -1072,9 +1073,22 @@ async function analyzeDiff(args) {
1072
1073
  info('Sending to Claude for analysis...');
1073
1074
  const startTime = Date.now();
1074
1075
 
1076
+ // Prepare telemetry context
1077
+ const filesChanged = diffFiles.split('\n').length;
1078
+ const telemetryContext = {
1079
+ fileCount: filesChanged,
1080
+ batchSize: filesChanged,
1081
+ totalBatches: 1,
1082
+ model: subagentModel || 'sonnet',
1083
+ hook: 'analyze-diff'
1084
+ };
1085
+
1075
1086
  try {
1076
- // Use cross-platform executeClaudeWithRetry from claude-client.js
1077
- const response = await executeClaudeWithRetry(prompt, { timeout: 180000 }); // 3 minutes for diff analysis
1087
+ // Use cross-platform executeClaudeWithRetry from claude-client.js with telemetry
1088
+ const response = await executeClaudeWithRetry(prompt, {
1089
+ timeout: 180000, // 3 minutes for diff analysis
1090
+ telemetryContext
1091
+ });
1078
1092
 
1079
1093
  // Extract JSON from response using claude-client utility
1080
1094
  const result = extractJSON(response);
@@ -1336,7 +1350,20 @@ async function createPr(args) {
1336
1350
 
1337
1351
  showInfo('Generating PR metadata with Claude...');
1338
1352
  logger.debug('create-pr', 'Calling Claude with prompt', { promptLength: prompt.length });
1339
- const response = await executeClaudeWithRetry(prompt, { timeout: 180000 });
1353
+
1354
+ // Prepare telemetry context for create-pr
1355
+ const telemetryContext = {
1356
+ fileCount: filesArray.length,
1357
+ batchSize: filesArray.length,
1358
+ totalBatches: 1,
1359
+ model: 'sonnet', // create-pr always uses main model
1360
+ hook: 'create-pr'
1361
+ };
1362
+
1363
+ const response = await executeClaudeWithRetry(prompt, {
1364
+ timeout: 180000,
1365
+ telemetryContext
1366
+ });
1340
1367
  logger.debug('create-pr', 'Claude response received', { responseLength: response.length });
1341
1368
 
1342
1369
  const analysisResult = extractJSON(response);
@@ -1703,6 +1730,7 @@ Commands:
1703
1730
  presets List all available presets
1704
1731
  --set-preset <name> Set the active preset
1705
1732
  preset current Show the current active preset
1733
+ telemetry [action] Telemetry management (show or clear)
1706
1734
  --debug <value> Set debug mode (true, false, or status)
1707
1735
  --version, -v Show the current version
1708
1736
  help Show this help
@@ -1725,6 +1753,8 @@ Examples:
1725
1753
  claude-hooks presets # List available presets
1726
1754
  claude-hooks --set-preset backend # Set backend preset
1727
1755
  claude-hooks preset current # Show current preset
1756
+ claude-hooks telemetry show # Show telemetry statistics
1757
+ claude-hooks telemetry clear # Clear telemetry data
1728
1758
  claude-hooks --debug true # Enable debug mode
1729
1759
  claude-hooks --debug status # Check debug status
1730
1760
 
@@ -2171,6 +2201,41 @@ async function setDebug(value) {
2171
2201
  }
2172
2202
 
2173
2203
  // Main
2204
+ /**
2205
+ * Show telemetry statistics
2206
+ * Why: Help users understand JSON parsing patterns and batch performance
2207
+ */
2208
+ async function showTelemetry() {
2209
+ await showTelemetryStats();
2210
+ }
2211
+
2212
+ /**
2213
+ * Clear telemetry data
2214
+ * Why: Allow users to reset telemetry
2215
+ */
2216
+ async function clearTelemetry() {
2217
+ const config = await getConfig();
2218
+
2219
+ if (!config.system?.telemetry && config.system?.telemetry !== undefined) {
2220
+ console.log('\n⚠️ Telemetry is currently disabled.\n');
2221
+ console.log('To re-enable (default), remove or set to true in .claude/config.json:');
2222
+ console.log('{');
2223
+ console.log(' "system": {');
2224
+ console.log(' "telemetry": true');
2225
+ console.log(' }');
2226
+ console.log('}\n');
2227
+ return;
2228
+ }
2229
+
2230
+ const confirmed = await promptConfirmation('Are you sure you want to clear all telemetry data?');
2231
+ if (confirmed) {
2232
+ await clearTelemetryData();
2233
+ success('Telemetry data cleared successfully');
2234
+ } else {
2235
+ info('Telemetry data was not cleared');
2236
+ }
2237
+ }
2238
+
2174
2239
  async function main() {
2175
2240
  const args = process.argv.slice(2);
2176
2241
  const command = args[0];
@@ -2223,6 +2288,16 @@ async function main() {
2223
2288
  case 'migrate-config':
2224
2289
  await migrateConfig();
2225
2290
  break;
2291
+ case 'telemetry':
2292
+ // Handle subcommands: telemetry show, telemetry clear
2293
+ if (args[1] === 'show' || args[1] === undefined) {
2294
+ await showTelemetry();
2295
+ } else if (args[1] === 'clear') {
2296
+ await clearTelemetry();
2297
+ } else {
2298
+ error(`Unknown telemetry subcommand: ${args[1]}`);
2299
+ }
2300
+ break;
2226
2301
  case '--debug':
2227
2302
  await setDebug(args[1]);
2228
2303
  break;
package/lib/config.js CHANGED
@@ -72,6 +72,7 @@ const HARDCODED = {
72
72
  },
73
73
  system: {
74
74
  debug: false, // Controlled by --debug flag
75
+ telemetry: true, // Opt-out telemetry for debugging (local only, set to false to disable)
75
76
  wslCheckTimeout: 15000, // System behavior
76
77
  },
77
78
  git: {
@@ -364,10 +364,19 @@ const main = async () => {
364
364
  })
365
365
  );
366
366
 
367
+ // Build telemetry context
368
+ const telemetryContext = {
369
+ fileCount: filesData.length,
370
+ batchSize: batchSize,
371
+ model: subagentModel,
372
+ hook: 'pre-commit'
373
+ };
374
+
367
375
  // Execute in parallel
368
376
  const results = await analyzeCodeParallel(prompts, {
369
377
  timeout: config.analysis.timeout,
370
- saveDebug: false // Don't save debug for individual batches
378
+ saveDebug: false, // Don't save debug for individual batches
379
+ telemetryContext
371
380
  });
372
381
 
373
382
  // Simple consolidation: merge all results
@@ -403,9 +412,19 @@ const main = async () => {
403
412
  { promptLength: prompt.length }
404
413
  );
405
414
 
415
+ // Build telemetry context for single execution
416
+ const telemetryContext = {
417
+ fileCount: filesData.length,
418
+ batchSize: filesData.length, // Single batch = all files
419
+ totalBatches: 1,
420
+ model: config.subagents?.model || 'haiku',
421
+ hook: 'pre-commit'
422
+ };
423
+
406
424
  result = await analyzeCode(prompt, {
407
425
  timeout: config.analysis.timeout,
408
- saveDebug: config.system.debug
426
+ saveDebug: config.system.debug,
427
+ telemetryContext
409
428
  });
410
429
  }
411
430
 
@@ -265,9 +265,21 @@ const main = async () => {
265
265
  // Generate message with Claude
266
266
  logger.info('Sending to Claude...');
267
267
 
268
+ // Build telemetry context
269
+ const telemetryContext = {
270
+ fileCount: filesData.length,
271
+ batchSize: config.subagents?.batchSize || 3,
272
+ totalBatches: subagentsEnabled && filesData.length >= 3
273
+ ? Math.ceil(filesData.length / (config.subagents?.batchSize || 3))
274
+ : 1,
275
+ model: subagentModel,
276
+ hook: 'prepare-commit-msg'
277
+ };
278
+
268
279
  const response = await analyzeCode(prompt, {
269
280
  timeout: config.commitMessage.timeout,
270
- saveDebug: config.system.debug
281
+ saveDebug: config.system.debug,
282
+ telemetryContext
271
283
  });
272
284
 
273
285
  logger.debug(
@@ -23,6 +23,7 @@ import logger from './logger.js';
23
23
  import config from '../config.js';
24
24
  import { detectClaudeError, formatClaudeError, ClaudeErrorType, isRecoverableError } from './claude-diagnostics.js';
25
25
  import { which } from './which-command.js';
26
+ import { recordJsonParseFailure, recordBatchSuccess, rotateTelemetry } from './telemetry.js';
26
27
 
27
28
  /**
28
29
  * Custom error for Claude client failures
@@ -526,10 +527,11 @@ const executeClaudeInteractive = (prompt, { timeout = 300000 } = {}) => new Prom
526
527
  * Why: Claude may include markdown formatting or explanatory text around JSON
527
528
  *
528
529
  * @param {string} response - Raw response from Claude
530
+ * @param {Object} telemetryContext - Context for telemetry recording (optional)
529
531
  * @returns {Object} Parsed JSON object
530
532
  * @throws {ClaudeClientError} If no valid JSON found
531
533
  */
532
- const extractJSON = (response) => {
534
+ const extractJSON = (response, telemetryContext = {}) => {
533
535
  logger.debug(
534
536
  'claude-client - extractJSON',
535
537
  'Extracting JSON from response',
@@ -594,16 +596,24 @@ const extractJSON = (response) => {
594
596
  }
595
597
 
596
598
  // No valid JSON found
599
+ const responsePreview = response.substring(0, 500);
600
+
597
601
  logger.error(
598
602
  'claude-client - extractJSON',
599
603
  'No valid JSON found in response',
600
604
  new ClaudeClientError('No valid JSON in response', {
601
- output: response.substring(0, 500) // First 500 chars for debugging
605
+ output: responsePreview
602
606
  })
603
607
  );
604
608
 
609
+ // Telemetry is now recorded by withRetry wrapper
605
610
  throw new ClaudeClientError('No valid JSON found in Claude response', {
606
- output: response.substring(0, 500)
611
+ context: {
612
+ response: response,
613
+ errorInfo: {
614
+ type: 'JSON_PARSE_ERROR'
615
+ }
616
+ }
607
617
  });
608
618
  };
609
619
 
@@ -661,8 +671,8 @@ const saveDebugResponse = async (prompt, response, filename = config.output.debu
661
671
  };
662
672
 
663
673
  /**
664
- * Wraps an async function with retry logic for recoverable errors
665
- * Why: Reusable retry logic for Claude CLI operations
674
+ * Wraps an async function with retry logic for recoverable errors and telemetry tracking
675
+ * Why: Reusable retry logic for Claude CLI operations with per-attempt telemetry
666
676
  *
667
677
  * @param {Function} fn - Async function to execute with retry
668
678
  * @param {Object} options - Retry options
@@ -670,15 +680,50 @@ const saveDebugResponse = async (prompt, response, filename = config.output.debu
670
680
  * @param {number} options.baseRetryDelay - Base delay in ms (default: 2000)
671
681
  * @param {number} options.retryCount - Current retry attempt (internal, default: 0)
672
682
  * @param {string} options.operationName - Name for logging (default: 'operation')
683
+ * @param {Object} options.telemetryContext - Context for telemetry recording (optional)
673
684
  * @returns {Promise<any>} Result from fn
674
685
  * @throws {Error} If fn fails after all retries
675
686
  */
676
- const withRetry = async (fn, { maxRetries = 3, baseRetryDelay = 2000, retryCount = 0, operationName = 'operation' } = {}) => {
687
+ const withRetry = async (fn, { maxRetries = 3, baseRetryDelay = 2000, retryCount = 0, operationName = 'operation', telemetryContext = null } = {}) => {
677
688
  const retryDelay = baseRetryDelay * Math.pow(2, retryCount);
689
+ const startTime = Date.now();
678
690
 
679
691
  try {
680
- return await fn();
692
+ const result = await fn();
693
+
694
+ // Record success telemetry if context provided
695
+ if (telemetryContext) {
696
+ const duration = Date.now() - startTime;
697
+ await recordBatchSuccess({
698
+ ...telemetryContext,
699
+ duration,
700
+ retryAttempt: retryCount,
701
+ totalRetries: maxRetries
702
+ }).catch(err => {
703
+ logger.debug('claude-client - withRetry', 'Failed to record success telemetry', err);
704
+ });
705
+ }
706
+
707
+ return result;
681
708
  } catch (error) {
709
+ // Record failure telemetry if context provided (before retry decision)
710
+ if (telemetryContext) {
711
+ const errorType = error.context?.errorInfo?.type || error.name || 'UNKNOWN_ERROR';
712
+ const errorMessage = error.message || 'Unknown error';
713
+
714
+ await recordJsonParseFailure({
715
+ ...telemetryContext,
716
+ errorType,
717
+ errorMessage,
718
+ retryAttempt: retryCount,
719
+ totalRetries: maxRetries,
720
+ responseLength: error.context?.response?.length || 0,
721
+ responsePreview: error.context?.response?.substring(0, 100) || ''
722
+ }).catch(err => {
723
+ logger.debug('claude-client - withRetry', 'Failed to record failure telemetry', err);
724
+ });
725
+ }
726
+
682
727
  // Check if error is recoverable and we haven't exceeded retry limit
683
728
  const hasContext = !!error.context;
684
729
  const hasErrorInfo = !!error.context?.errorInfo;
@@ -714,8 +759,8 @@ const withRetry = async (fn, { maxRetries = 3, baseRetryDelay = 2000, retryCount
714
759
  // Wait before retry
715
760
  await new Promise(resolve => setTimeout(resolve, retryDelay));
716
761
 
717
- // Retry with incremented count
718
- return withRetry(fn, { maxRetries, baseRetryDelay, retryCount: retryCount + 1, operationName });
762
+ // Retry with incremented count and same telemetry context
763
+ return withRetry(fn, { maxRetries, baseRetryDelay, retryCount: retryCount + 1, operationName, telemetryContext });
719
764
  }
720
765
 
721
766
  // Add retry attempt to error context if not already present
@@ -730,17 +775,25 @@ const withRetry = async (fn, { maxRetries = 3, baseRetryDelay = 2000, retryCount
730
775
  };
731
776
 
732
777
  /**
733
- * Executes Claude CLI with retry logic
734
- * Why: Provides retry capability for executeClaude calls
778
+ * Executes Claude CLI with retry logic and telemetry tracking
779
+ * Why: Provides retry capability for executeClaude calls with per-attempt telemetry
735
780
  *
736
781
  * @param {string} prompt - Prompt text
737
- * @param {Object} options - Execution options (timeout, allowedTools)
782
+ * @param {Object} options - Execution options
783
+ * @param {number} options.timeout - Timeout in milliseconds
784
+ * @param {Array<string>} options.allowedTools - Allowed tools for Claude
785
+ * @param {Object} options.telemetryContext - Context for telemetry (fileCount, hook, etc.)
738
786
  * @returns {Promise<string>} Claude's response
739
787
  */
740
788
  const executeClaudeWithRetry = async (prompt, options = {}) => {
789
+ const { telemetryContext = {}, ...executeOptions } = options;
790
+
741
791
  return withRetry(
742
- () => executeClaude(prompt, options),
743
- { operationName: 'executeClaude' }
792
+ () => executeClaude(prompt, executeOptions),
793
+ {
794
+ operationName: 'executeClaude',
795
+ telemetryContext: telemetryContext
796
+ }
744
797
  );
745
798
  };
746
799
 
@@ -752,17 +805,25 @@ const executeClaudeWithRetry = async (prompt, options = {}) => {
752
805
  * @param {Object} options - Analysis options
753
806
  * @param {number} options.timeout - Timeout in milliseconds
754
807
  * @param {boolean} options.saveDebug - Save response to debug file (default: from config)
808
+ * @param {Object} options.telemetryContext - Context for telemetry (fileCount, batchSize, etc.)
755
809
  * @returns {Promise<Object>} Parsed analysis result
756
810
  * @throws {ClaudeClientError} If analysis fails
757
811
  */
758
- const analyzeCode = async (prompt, { timeout = 120000, saveDebug = config.system.debug } = {}) => {
812
+ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = config.system.debug, telemetryContext = {} } = {}) => {
813
+ const startTime = Date.now();
814
+
759
815
  logger.debug(
760
816
  'claude-client - analyzeCode',
761
817
  'Starting code analysis',
762
818
  { promptLength: prompt.length, timeout, saveDebug }
763
819
  );
764
820
 
765
- // Use withRetry to wrap the entire analysis flow
821
+ // Rotate telemetry files periodically
822
+ rotateTelemetry().catch(err => {
823
+ logger.debug('claude-client - analyzeCode', 'Failed to rotate telemetry', err);
824
+ });
825
+
826
+ // Use withRetry to wrap the entire analysis flow (with telemetry tracking)
766
827
  return withRetry(
767
828
  async () => {
768
829
  // Execute Claude CLI
@@ -774,7 +835,9 @@ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = config.system
774
835
  }
775
836
 
776
837
  // Extract and parse JSON
777
- const result = extractJSON(response);
838
+ const result = extractJSON(response, telemetryContext);
839
+
840
+ const duration = Date.now() - startTime;
778
841
 
779
842
  logger.debug(
780
843
  'claude-client - analyzeCode',
@@ -782,13 +845,21 @@ const analyzeCode = async (prompt, { timeout = 120000, saveDebug = config.system
782
845
  {
783
846
  hasApproved: 'approved' in result,
784
847
  hasQualityGate: 'QUALITY_GATE' in result,
785
- blockingIssuesCount: result.blockingIssues?.length ?? 0
848
+ blockingIssuesCount: result.blockingIssues?.length ?? 0,
849
+ duration
786
850
  }
787
851
  );
788
852
 
853
+ // Telemetry is now recorded by withRetry wrapper
789
854
  return result;
790
855
  },
791
- { operationName: 'analyzeCode' }
856
+ {
857
+ operationName: 'analyzeCode',
858
+ telemetryContext: {
859
+ responseLength: 0, // Will be updated in withRetry
860
+ ...telemetryContext
861
+ }
862
+ }
792
863
  );
793
864
  };
794
865
 
@@ -810,6 +881,7 @@ const chunkArray = (array, size) => {
810
881
  * Runs multiple analyzeCode calls in parallel
811
882
  * @param {Array<string>} prompts - Array of prompts to analyze
812
883
  * @param {Object} options - Same options as analyzeCode
884
+ * @param {Object} options.telemetryContext - Base telemetry context (will be augmented per batch)
813
885
  * @returns {Promise<Array<Object>>} Array of results
814
886
  */
815
887
  const analyzeCodeParallel = async (prompts, options = {}) => {
@@ -824,7 +896,18 @@ const analyzeCodeParallel = async (prompts, options = {}) => {
824
896
  const promises = prompts.map((prompt, index) => {
825
897
  console.log(` ⚡ Launching batch ${index + 1}/${prompts.length}...`);
826
898
  logger.debug('claude-client - analyzeCodeParallel', `Starting batch ${index + 1}`);
827
- return analyzeCode(prompt, options);
899
+
900
+ // Augment telemetry context with batch info
901
+ const batchTelemetryContext = {
902
+ ...(options.telemetryContext || {}),
903
+ batchIndex: index,
904
+ totalBatches: prompts.length
905
+ };
906
+
907
+ return analyzeCode(prompt, {
908
+ ...options,
909
+ telemetryContext: batchTelemetryContext
910
+ });
828
911
  });
829
912
 
830
913
  console.log(' ⏳ Waiting for all batches to complete...\n');
@@ -38,11 +38,11 @@ class PromptBuilderError extends Error {
38
38
  * Why absolute path: Ensures templates are found regardless of cwd (cross-platform)
39
39
  *
40
40
  * @param {string} templateName - Name of template file
41
- * @param {string} baseDir - Base directory (default: try .claude, fallback to templates)
41
+ * @param {string} baseDir - Base directory (default: .claude/prompts, fallback to templates)
42
42
  * @returns {Promise<string>} Template content
43
43
  * @throws {PromptBuilderError} If template not found
44
44
  */
45
- const loadTemplate = async (templateName, baseDir = '.claude') => {
45
+ const loadTemplate = async (templateName, baseDir = '.claude/prompts') => {
46
46
  // Why: Use repo root for absolute path (works on Windows/PowerShell/Git Bash)
47
47
  const repoRoot = getRepoRoot();
48
48
  let templatePath = path.join(repoRoot, baseDir, templateName);
@@ -118,7 +118,7 @@ const replaceTemplate = (template, variables) => {
118
118
  *
119
119
  * @param {string} templateName - Name of template file
120
120
  * @param {Object} variables - Variables to replace in template
121
- * @param {string} baseDir - Base directory (default: .claude)
121
+ * @param {string} baseDir - Base directory (default: .claude/prompts)
122
122
  * @returns {Promise<string>} Prompt with replaced variables
123
123
  * @throws {PromptBuilderError} If template not found
124
124
  *
@@ -130,7 +130,7 @@ const replaceTemplate = (template, variables) => {
130
130
  * DELETIONS: 5
131
131
  * });
132
132
  */
133
- const loadPrompt = async (templateName, variables = {}, baseDir = '.claude') => {
133
+ const loadPrompt = async (templateName, variables = {}, baseDir = '.claude/prompts') => {
134
134
  logger.debug(
135
135
  'prompt-builder - loadPrompt',
136
136
  'Loading prompt with variables',
@@ -219,19 +219,20 @@ const buildAnalysisPrompt = async ({
219
219
  guidelinesName = 'CLAUDE_PRE_COMMIT.md',
220
220
  files = [],
221
221
  metadata = {},
222
- subagentConfig = null
222
+ subagentConfig = null,
223
+ baseDir = '.claude/prompts'
223
224
  } = {}) => {
224
225
  logger.debug(
225
226
  'prompt-builder - buildAnalysisPrompt',
226
227
  'Building analysis prompt',
227
- { templateName, guidelinesName, fileCount: files.length, subagentsEnabled: subagentConfig?.enabled }
228
+ { templateName, guidelinesName, fileCount: files.length, subagentsEnabled: subagentConfig?.enabled, baseDir }
228
229
  );
229
230
 
230
231
  try {
231
232
  // Load template and guidelines
232
233
  const [template, guidelines] = await Promise.all([
233
- loadTemplate(templateName),
234
- loadTemplate(guidelinesName)
234
+ loadTemplate(templateName, baseDir),
235
+ loadTemplate(guidelinesName, baseDir)
235
236
  ]);
236
237
 
237
238
  // Start with template
@@ -240,7 +241,7 @@ const buildAnalysisPrompt = async ({
240
241
  // Add subagent instruction if enabled and 3+ files
241
242
  if (subagentConfig?.enabled && files.length >= 3) {
242
243
  try {
243
- const subagentInstruction = await loadTemplate('SUBAGENT_INSTRUCTION.md');
244
+ const subagentInstruction = await loadTemplate('SUBAGENT_INSTRUCTION.md', baseDir);
244
245
  const subagentVariables = {
245
246
  BATCH_SIZE: subagentConfig.batchSize || 3,
246
247
  MODEL: subagentConfig.model || 'haiku'
@@ -171,7 +171,7 @@ ${content}
171
171
  * @param {Array<Object>} analysisResult.blockingIssues - Array of blocking issues
172
172
  * @param {Object} options - Generation options
173
173
  * @param {string} options.outputPath - Output file path (default: 'claude_resolution_prompt.md')
174
- * @param {string} options.templatePath - Template path (default: '.claude/CLAUDE_RESOLUTION_PROMPT.md')
174
+ * @param {string} options.templatePath - Template path (default: '.claude/prompts/CLAUDE_RESOLUTION_PROMPT.md')
175
175
  * @param {number} options.fileCount - Number of files analyzed
176
176
  * @returns {Promise<string>} Path to generated resolution prompt
177
177
  * @throws {ResolutionPromptError} If generation fails
@@ -180,7 +180,7 @@ const generateResolutionPrompt = async (
180
180
  analysisResult,
181
181
  {
182
182
  outputPath = null,
183
- templatePath = '.claude/CLAUDE_RESOLUTION_PROMPT.md',
183
+ templatePath = '.claude/prompts/CLAUDE_RESOLUTION_PROMPT.md',
184
184
  fileCount = 0
185
185
  } = {}
186
186
  ) => {
@@ -0,0 +1,507 @@
1
+ /**
2
+ * File: telemetry.js
3
+ * Purpose: Local telemetry collection for debugging and optimization
4
+ *
5
+ * Design principles:
6
+ * - OPT-IN ONLY: Requires explicit config.system.telemetry = true
7
+ * - LOCAL ONLY: Data never leaves user's machine
8
+ * - PRIVACY-FIRST: No PII, no code content, just metrics
9
+ * - STRUCTURED LOGS: JSON lines format for easy analysis
10
+ * - AUTO-ROTATION: Limits file size to prevent unbounded growth
11
+ *
12
+ * Key responsibilities:
13
+ * - Track JSON parsing failures with context
14
+ * - Record batch analysis metrics
15
+ * - Provide local statistics via CLI
16
+ * - Auto-rotate log files
17
+ *
18
+ * Dependencies:
19
+ * - fs/promises: File operations
20
+ * - logger: Debug logging
21
+ */
22
+
23
+ import fs from 'fs/promises';
24
+ import fsSync from 'fs';
25
+ import path from 'path';
26
+ import logger from './logger.js';
27
+ import config from '../config.js';
28
+
29
+ /**
30
+ * Telemetry event structure
31
+ * @typedef {Object} TelemetryEvent
32
+ * @property {string} id - Unique event ID (timestamp-counter-random)
33
+ * @property {string} timestamp - ISO timestamp
34
+ * @property {string} type - Event type
35
+ * @property {boolean} success - Whether the operation succeeded
36
+ * @property {number} retryAttempt - Current retry attempt (0-based)
37
+ * @property {number} totalRetries - Total retry attempts configured
38
+ * @property {Object} data - Event data
39
+ */
40
+
41
+ /**
42
+ * Counter for generating unique IDs within the same millisecond
43
+ */
44
+ let eventCounter = 0;
45
+
46
+ /**
47
+ * Generate unique event ID
48
+ * Why: Ensure each telemetry event has a unique identifier
49
+ * Format: timestamp-counter-random (e.g., 1703612345678-001-a3f)
50
+ *
51
+ * @returns {string} Unique event ID
52
+ */
53
+ const generateEventId = () => {
54
+ const timestamp = Date.now();
55
+ const counter = String(eventCounter++).padStart(3, '0');
56
+ const random = Math.random().toString(36).substring(2, 5);
57
+
58
+ // Reset counter if it exceeds 999
59
+ if (eventCounter > 999) {
60
+ eventCounter = 0;
61
+ }
62
+
63
+ return `${timestamp}-${counter}-${random}`;
64
+ };
65
+
66
+ /**
67
+ * Get telemetry directory path
68
+ * Why: Store in .claude/telemetry (gitignored) per-repo
69
+ *
70
+ * @returns {string} Telemetry directory path
71
+ */
72
+ const getTelemetryDir = () => {
73
+ return path.join(process.cwd(), '.claude', 'telemetry');
74
+ };
75
+
76
+ /**
77
+ * Get current telemetry log file path
78
+ * Why: Use date-based naming for natural rotation
79
+ *
80
+ * @returns {string} Current log file path
81
+ */
82
+ const getCurrentLogFile = () => {
83
+ const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
84
+ return path.join(getTelemetryDir(), `telemetry-${date}.jsonl`);
85
+ };
86
+
87
+ /**
88
+ * Check if telemetry is enabled
89
+ * Why: Enabled by default, users must explicitly disable
90
+ *
91
+ * @returns {boolean} True if telemetry is enabled (default: true)
92
+ */
93
+ const isTelemetryEnabled = () => {
94
+ // Enabled by default - only disabled if explicitly set to false
95
+ return config.system?.telemetry !== false;
96
+ };
97
+
98
+ /**
99
+ * Ensure telemetry directory exists
100
+ * Why: Create on first use
101
+ */
102
+ const ensureTelemetryDir = async () => {
103
+ try {
104
+ const dir = getTelemetryDir();
105
+ await fs.mkdir(dir, { recursive: true });
106
+ } catch (error) {
107
+ logger.debug('telemetry - ensureTelemetryDir', 'Failed to create directory', error);
108
+ }
109
+ };
110
+
111
+ /**
112
+ * Append event to telemetry log
113
+ * Why: JSON lines format for efficient append and parsing
114
+ *
115
+ * @param {TelemetryEvent} event - Event to log
116
+ */
117
+ const appendEvent = async (event) => {
118
+ try {
119
+ const logFile = getCurrentLogFile();
120
+ const line = JSON.stringify(event) + '\n';
121
+
122
+ // Append to file (create if doesn't exist)
123
+ await fs.appendFile(logFile, line, 'utf8');
124
+
125
+ logger.debug('telemetry - appendEvent', `Event logged: ${event.type}`);
126
+ } catch (error) {
127
+ // Don't fail on telemetry errors
128
+ logger.debug('telemetry - appendEvent', 'Failed to append event', error);
129
+ }
130
+ };
131
+
132
+ /**
133
+ * Record a telemetry event
134
+ * Why: Centralized event recording with opt-in check
135
+ *
136
+ * @param {string} eventType - Type of event
137
+ * @param {Object} eventData - Event-specific data (no PII, no code content)
138
+ * @param {Object} metadata - Event metadata (success, retryAttempt, totalRetries)
139
+ */
140
+ export const recordEvent = async (eventType, eventData = {}, metadata = {}) => {
141
+ // Check opt-in
142
+ if (!isTelemetryEnabled()) {
143
+ return;
144
+ }
145
+
146
+ try {
147
+ // Ensure directory exists
148
+ await ensureTelemetryDir();
149
+
150
+ // Create event with all metadata
151
+ const event = {
152
+ id: generateEventId(),
153
+ timestamp: new Date().toISOString(),
154
+ type: eventType,
155
+ success: metadata.success ?? false,
156
+ retryAttempt: metadata.retryAttempt ?? 0,
157
+ totalRetries: metadata.totalRetries ?? 0,
158
+ data: eventData
159
+ };
160
+
161
+ // Append to log
162
+ await appendEvent(event);
163
+ } catch (error) {
164
+ // Don't fail on telemetry errors
165
+ logger.debug('telemetry - recordEvent', 'Failed to record event', error);
166
+ }
167
+ };
168
+
169
+ /**
170
+ * Record JSON parsing failure or API execution error
171
+ * Why: Track failures with batch context and retry information for debugging
172
+ *
173
+ * @param {Object} options - Failure context
174
+ * @param {number} options.retryAttempt - Current retry attempt (0-based)
175
+ * @param {number} options.totalRetries - Total retry attempts configured
176
+ */
177
+ export const recordJsonParseFailure = async (options) => {
178
+ await recordEvent(
179
+ 'json_parse_failure',
180
+ {
181
+ fileCount: options.fileCount || 0,
182
+ batchSize: options.batchSize || 0,
183
+ batchIndex: options.batchIndex ?? -1,
184
+ totalBatches: options.totalBatches || 0,
185
+ model: options.model || 'unknown',
186
+ responseLength: options.responseLength || 0,
187
+ // Only include first 100 chars to avoid storing large responses
188
+ responsePreview: (options.responsePreview || '').substring(0, 100),
189
+ errorMessage: options.errorMessage || '',
190
+ errorType: options.errorType || 'unknown',
191
+ parallelMode: (options.totalBatches || 0) > 1,
192
+ hook: options.hook || 'unknown' // pre-commit, prepare-commit-msg, analyze-diff, create-pr
193
+ },
194
+ {
195
+ success: false,
196
+ retryAttempt: options.retryAttempt ?? 0,
197
+ totalRetries: options.totalRetries ?? 0
198
+ }
199
+ );
200
+ };
201
+
202
+ /**
203
+ * Record successful batch analysis
204
+ * Why: Track successes with retry information to compare against failures
205
+ *
206
+ * @param {Object} options - Success context
207
+ * @param {number} options.retryAttempt - Current retry attempt (0-based)
208
+ * @param {number} options.totalRetries - Total retry attempts configured
209
+ */
210
+ export const recordBatchSuccess = async (options) => {
211
+ await recordEvent(
212
+ 'batch_success',
213
+ {
214
+ fileCount: options.fileCount || 0,
215
+ batchSize: options.batchSize || 0,
216
+ batchIndex: options.batchIndex ?? -1,
217
+ totalBatches: options.totalBatches || 0,
218
+ model: options.model || 'unknown',
219
+ duration: options.duration || 0,
220
+ responseLength: options.responseLength || 0,
221
+ parallelMode: (options.totalBatches || 0) > 1,
222
+ hook: options.hook || 'unknown'
223
+ },
224
+ {
225
+ success: true,
226
+ retryAttempt: options.retryAttempt ?? 0,
227
+ totalRetries: options.totalRetries ?? 0
228
+ }
229
+ );
230
+ };
231
+
232
+ /**
233
+ * Read all telemetry events from log files
234
+ * Why: Aggregate data for statistics
235
+ *
236
+ * @param {number} maxDays - Maximum days to read (default: 7)
237
+ * @returns {Promise<Array<TelemetryEvent>>} Array of events
238
+ */
239
+ const readTelemetryEvents = async (maxDays = 7) => {
240
+ try {
241
+ const dir = getTelemetryDir();
242
+ const files = await fs.readdir(dir);
243
+
244
+ // Filter to .jsonl files only
245
+ const logFiles = files.filter(f => f.endsWith('.jsonl'));
246
+
247
+ // Sort by date (newest first)
248
+ logFiles.sort().reverse();
249
+
250
+ // Limit to maxDays
251
+ const limitedFiles = logFiles.slice(0, maxDays);
252
+
253
+ // Read all events
254
+ const events = [];
255
+ for (const file of limitedFiles) {
256
+ const filePath = path.join(dir, file);
257
+ const content = await fs.readFile(filePath, 'utf8');
258
+
259
+ // Parse JSON lines
260
+ const lines = content.trim().split('\n');
261
+ for (const line of lines) {
262
+ if (line.trim()) {
263
+ try {
264
+ events.push(JSON.parse(line));
265
+ } catch (parseError) {
266
+ // Skip invalid lines
267
+ logger.debug('telemetry - readTelemetryEvents', 'Invalid JSON line', parseError);
268
+ }
269
+ }
270
+ }
271
+ }
272
+
273
+ return events;
274
+ } catch (error) {
275
+ logger.debug('telemetry - readTelemetryEvents', 'Failed to read events', error);
276
+ return [];
277
+ }
278
+ };
279
+
280
+ /**
281
+ * Get telemetry statistics
282
+ * Why: Provide insights for debugging and optimization
283
+ *
284
+ * @param {number} maxDays - Maximum days to analyze (default: 7)
285
+ * @returns {Promise<Object>} Statistics object
286
+ */
287
+ export const getStatistics = async (maxDays = 7) => {
288
+ if (!isTelemetryEnabled()) {
289
+ return {
290
+ enabled: false,
291
+ message: 'Telemetry is disabled. To enable (default), remove or set "system.telemetry: true" in .claude/config.json'
292
+ };
293
+ }
294
+
295
+ try {
296
+ const events = await readTelemetryEvents(maxDays);
297
+
298
+ const stats = {
299
+ enabled: true,
300
+ period: `Last ${maxDays} days`,
301
+ totalEvents: events.length,
302
+ jsonParseFailures: 0,
303
+ batchSuccesses: 0,
304
+ failuresByBatchSize: {},
305
+ failuresByModel: {},
306
+ failuresByHook: {},
307
+ successesByHook: {},
308
+ avgFilesPerFailure: 0,
309
+ avgFilesPerSuccess: 0,
310
+ failureRate: 0
311
+ };
312
+
313
+ let totalFilesInFailures = 0;
314
+ let totalFilesInSuccesses = 0;
315
+
316
+ events.forEach(event => {
317
+ if (event.type === 'json_parse_failure') {
318
+ stats.jsonParseFailures++;
319
+ totalFilesInFailures += event.data.fileCount || 0;
320
+
321
+ // Group by batch size
322
+ const batchSize = event.data.batchSize || 0;
323
+ stats.failuresByBatchSize[batchSize] = (stats.failuresByBatchSize[batchSize] || 0) + 1;
324
+
325
+ // Group by model
326
+ const model = event.data.model || 'unknown';
327
+ stats.failuresByModel[model] = (stats.failuresByModel[model] || 0) + 1;
328
+
329
+ // Group by hook
330
+ const hook = event.data.hook || 'unknown';
331
+ stats.failuresByHook[hook] = (stats.failuresByHook[hook] || 0) + 1;
332
+
333
+ } else if (event.type === 'batch_success') {
334
+ stats.batchSuccesses++;
335
+ totalFilesInSuccesses += event.data.fileCount || 0;
336
+
337
+ // Group by hook
338
+ const hook = event.data.hook || 'unknown';
339
+ stats.successesByHook[hook] = (stats.successesByHook[hook] || 0) + 1;
340
+ }
341
+ });
342
+
343
+ // Calculate averages
344
+ if (stats.jsonParseFailures > 0) {
345
+ stats.avgFilesPerFailure = parseFloat((totalFilesInFailures / stats.jsonParseFailures).toFixed(2));
346
+ }
347
+ if (stats.batchSuccesses > 0) {
348
+ stats.avgFilesPerSuccess = parseFloat((totalFilesInSuccesses / stats.batchSuccesses).toFixed(2));
349
+ }
350
+
351
+ // Calculate failure rate
352
+ const totalAnalyses = stats.jsonParseFailures + stats.batchSuccesses;
353
+ if (totalAnalyses > 0) {
354
+ stats.failureRate = parseFloat((stats.jsonParseFailures / totalAnalyses * 100).toFixed(2));
355
+ }
356
+
357
+ return stats;
358
+ } catch (error) {
359
+ logger.debug('telemetry - getStatistics', 'Failed to calculate statistics', error);
360
+ return {
361
+ enabled: true,
362
+ error: 'Failed to calculate statistics',
363
+ details: error.message
364
+ };
365
+ }
366
+ };
367
+
368
+ /**
369
+ * Rotate old telemetry files
370
+ * Why: Prevent unbounded growth
371
+ *
372
+ * @param {number} maxDays - Keep files newer than this many days (default: 30)
373
+ */
374
+ export const rotateTelemetry = async (maxDays = 30) => {
375
+ if (!isTelemetryEnabled()) {
376
+ return;
377
+ }
378
+
379
+ try {
380
+ const dir = getTelemetryDir();
381
+ const files = await fs.readdir(dir);
382
+
383
+ // Filter to .jsonl files
384
+ const logFiles = files.filter(f => f.endsWith('.jsonl'));
385
+
386
+ // Calculate cutoff date
387
+ const cutoffDate = new Date();
388
+ cutoffDate.setDate(cutoffDate.getDate() - maxDays);
389
+ const cutoffStr = cutoffDate.toISOString().split('T')[0]; // YYYY-MM-DD
390
+
391
+ // Delete old files
392
+ for (const file of logFiles) {
393
+ // Extract date from filename (telemetry-YYYY-MM-DD.jsonl)
394
+ const match = file.match(/telemetry-(\d{4}-\d{2}-\d{2})\.jsonl/);
395
+ if (match) {
396
+ const fileDate = match[1];
397
+ if (fileDate < cutoffStr) {
398
+ const filePath = path.join(dir, file);
399
+ await fs.unlink(filePath);
400
+ logger.debug('telemetry - rotateTelemetry', `Deleted old telemetry file: ${file}`);
401
+ }
402
+ }
403
+ }
404
+ } catch (error) {
405
+ logger.debug('telemetry - rotateTelemetry', 'Failed to rotate telemetry', error);
406
+ }
407
+ };
408
+
409
+ /**
410
+ * Clear all telemetry data
411
+ * Why: Allow users to reset
412
+ */
413
+ export const clearTelemetry = async () => {
414
+ try {
415
+ const dir = getTelemetryDir();
416
+
417
+ // Check if directory exists
418
+ if (!fsSync.existsSync(dir)) {
419
+ logger.info('No telemetry data found');
420
+ return;
421
+ }
422
+
423
+ // Delete all .jsonl files
424
+ const files = await fs.readdir(dir);
425
+ const logFiles = files.filter(f => f.endsWith('.jsonl'));
426
+
427
+ for (const file of logFiles) {
428
+ const filePath = path.join(dir, file);
429
+ await fs.unlink(filePath);
430
+ }
431
+
432
+ logger.info(`Cleared ${logFiles.length} telemetry files`);
433
+ } catch (error) {
434
+ logger.error('telemetry - clearTelemetry', 'Failed to clear telemetry', error);
435
+ }
436
+ };
437
+
438
+ /**
439
+ * Display telemetry statistics to console
440
+ * Why: User-friendly stats display for CLI
441
+ */
442
+ export const displayStatistics = async () => {
443
+ const stats = await getStatistics();
444
+
445
+ console.log('\n╔════════════════════════════════════════════════════════════════════╗');
446
+ console.log('║ TELEMETRY STATISTICS ║');
447
+ console.log('╚════════════════════════════════════════════════════════════════════╝\n');
448
+
449
+ if (!stats.enabled) {
450
+ console.log(stats.message);
451
+ console.log('\nTelemetry is disabled in your configuration.');
452
+ console.log('To re-enable (default), remove or set to true in .claude/config.json:');
453
+ console.log('{');
454
+ console.log(' "system": {');
455
+ console.log(' "telemetry": true');
456
+ console.log(' }');
457
+ console.log('}\n');
458
+ return;
459
+ }
460
+
461
+ if (stats.error) {
462
+ console.log(`Error: ${stats.error}`);
463
+ console.log(`Details: ${stats.details}\n`);
464
+ return;
465
+ }
466
+
467
+ console.log(`Period: ${stats.period}`);
468
+ console.log(`Total events: ${stats.totalEvents}\n`);
469
+
470
+ console.log('━━━ ANALYSIS RESULTS ━━━');
471
+ console.log(`✅ Successful analyses: ${stats.batchSuccesses}`);
472
+ console.log(`❌ JSON parse failures: ${stats.jsonParseFailures}`);
473
+ console.log(`📊 Failure rate: ${stats.failureRate}%\n`);
474
+
475
+ if (stats.jsonParseFailures > 0) {
476
+ console.log('━━━ FAILURES BY BATCH SIZE ━━━');
477
+ Object.entries(stats.failuresByBatchSize)
478
+ .sort((a, b) => b[1] - a[1])
479
+ .forEach(([size, count]) => {
480
+ console.log(` Batch size ${size}: ${count} failures`);
481
+ });
482
+ console.log();
483
+
484
+ console.log('━━━ FAILURES BY MODEL ━━━');
485
+ Object.entries(stats.failuresByModel)
486
+ .sort((a, b) => b[1] - a[1])
487
+ .forEach(([model, count]) => {
488
+ console.log(` ${model}: ${count} failures`);
489
+ });
490
+ console.log();
491
+
492
+ console.log('━━━ FAILURES BY HOOK ━━━');
493
+ Object.entries(stats.failuresByHook)
494
+ .sort((a, b) => b[1] - a[1])
495
+ .forEach(([hook, count]) => {
496
+ console.log(` ${hook}: ${count} failures`);
497
+ });
498
+ console.log();
499
+
500
+ console.log('━━━ AVERAGES ━━━');
501
+ console.log(` Avg files per failure: ${stats.avgFilesPerFailure}`);
502
+ console.log(` Avg files per success: ${stats.avgFilesPerSuccess}\n`);
503
+ }
504
+
505
+ console.log('📂 Telemetry location: .claude/telemetry/');
506
+ console.log('💡 Tip: Share telemetry logs when reporting issues for faster debugging\n');
507
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-git-hooks",
3
- "version": "2.8.0",
3
+ "version": "2.9.1",
4
4
  "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
5
  "type": "module",
6
6
  "bin": {