@entro314labs/ai-changelog-generator 3.1.1 → 3.2.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 (51) hide show
  1. package/CHANGELOG.md +383 -877
  2. package/README.md +8 -3
  3. package/ai-changelog-mcp.sh +0 -0
  4. package/ai-changelog.sh +0 -0
  5. package/bin/ai-changelog-dxt.js +9 -9
  6. package/bin/ai-changelog-mcp.js +19 -17
  7. package/bin/ai-changelog.js +6 -6
  8. package/package.json +80 -48
  9. package/src/ai-changelog-generator.js +83 -81
  10. package/src/application/orchestrators/changelog.orchestrator.js +791 -516
  11. package/src/application/services/application.service.js +137 -128
  12. package/src/cli.js +76 -57
  13. package/src/domains/ai/ai-analysis.service.js +289 -209
  14. package/src/domains/analysis/analysis.engine.js +253 -193
  15. package/src/domains/changelog/changelog.service.js +1062 -784
  16. package/src/domains/changelog/workspace-changelog.service.js +420 -249
  17. package/src/domains/git/git-repository.analyzer.js +348 -258
  18. package/src/domains/git/git.service.js +132 -112
  19. package/src/infrastructure/cli/cli.controller.js +390 -274
  20. package/src/infrastructure/config/configuration.manager.js +220 -190
  21. package/src/infrastructure/interactive/interactive-staging.service.js +154 -135
  22. package/src/infrastructure/interactive/interactive-workflow.service.js +200 -159
  23. package/src/infrastructure/mcp/mcp-server.service.js +208 -207
  24. package/src/infrastructure/metrics/metrics.collector.js +140 -123
  25. package/src/infrastructure/providers/core/base-provider.js +87 -40
  26. package/src/infrastructure/providers/implementations/anthropic.js +101 -99
  27. package/src/infrastructure/providers/implementations/azure.js +124 -101
  28. package/src/infrastructure/providers/implementations/bedrock.js +136 -126
  29. package/src/infrastructure/providers/implementations/dummy.js +23 -23
  30. package/src/infrastructure/providers/implementations/google.js +123 -114
  31. package/src/infrastructure/providers/implementations/huggingface.js +94 -87
  32. package/src/infrastructure/providers/implementations/lmstudio.js +75 -60
  33. package/src/infrastructure/providers/implementations/mock.js +69 -73
  34. package/src/infrastructure/providers/implementations/ollama.js +89 -66
  35. package/src/infrastructure/providers/implementations/openai.js +88 -89
  36. package/src/infrastructure/providers/implementations/vertex.js +227 -197
  37. package/src/infrastructure/providers/provider-management.service.js +245 -207
  38. package/src/infrastructure/providers/provider-manager.service.js +145 -125
  39. package/src/infrastructure/providers/utils/base-provider-helpers.js +308 -302
  40. package/src/infrastructure/providers/utils/model-config.js +220 -195
  41. package/src/infrastructure/providers/utils/provider-utils.js +105 -100
  42. package/src/infrastructure/validation/commit-message-validation.service.js +259 -161
  43. package/src/shared/constants/colors.js +453 -180
  44. package/src/shared/utils/cli-demo.js +285 -0
  45. package/src/shared/utils/cli-entry-utils.js +257 -249
  46. package/src/shared/utils/cli-ui.js +447 -0
  47. package/src/shared/utils/diff-processor.js +513 -0
  48. package/src/shared/utils/error-classes.js +125 -156
  49. package/src/shared/utils/json-utils.js +93 -89
  50. package/src/shared/utils/utils.js +1117 -945
  51. package/types/index.d.ts +353 -344
@@ -1,5 +1,13 @@
1
- import { getWorkingDirectoryChanges, summarizeFileChanges, categorizeFile, detectLanguage, assessFileImportance } from '../../shared/utils/utils.js';
2
- import colors from '../../shared/constants/colors.js';
1
+ import colors from '../../shared/constants/colors.js'
2
+ import { EnhancedConsole } from '../../shared/utils/cli-ui.js'
3
+ import { DiffProcessor } from '../../shared/utils/diff-processor.js'
4
+ import {
5
+ assessFileImportance,
6
+ categorizeFile,
7
+ detectLanguage,
8
+ getWorkingDirectoryChanges,
9
+ summarizeFileChanges,
10
+ } from '../../shared/utils/utils.js'
3
11
 
4
12
  /**
5
13
  * Workspace Changelog Service
@@ -8,75 +16,106 @@ import colors from '../../shared/constants/colors.js';
8
16
  */
9
17
  export class WorkspaceChangelogService {
10
18
  constructor(aiAnalysisService, gitService = null) {
11
- this.aiAnalysisService = aiAnalysisService;
12
- this.gitService = gitService;
19
+ this.aiAnalysisService = aiAnalysisService
20
+ this.gitService = gitService
13
21
  }
14
22
 
15
23
  async generateComprehensiveWorkspaceChangelog(options = {}) {
16
24
  try {
17
-
18
25
  // Get working directory changes as raw array
19
- const rawChanges = getWorkingDirectoryChanges();
20
-
26
+ const rawChanges = getWorkingDirectoryChanges()
21
27
 
22
- if (!rawChanges || !Array.isArray(rawChanges) || rawChanges.length === 0) {
23
- console.log(colors.infoMessage('No changes detected in working directory.'));
24
- return null;
28
+ if (!(rawChanges && Array.isArray(rawChanges)) || rawChanges.length === 0) {
29
+ EnhancedConsole.info('No changes detected in working directory.')
30
+ return null
25
31
  }
26
32
 
27
33
  // Enhanced analysis of changes with diff content for AI analysis
28
- const enhancedChanges = await this.enhanceChangesWithDiff(rawChanges);
29
- const changesSummary = summarizeFileChanges(enhancedChanges);
34
+ const enhancedChanges = await this.enhanceChangesWithDiff(rawChanges)
35
+ const changesSummary = summarizeFileChanges(enhancedChanges)
30
36
 
31
- // Generate workspace context
32
- const workspaceContext = this.generateWorkspaceContext(enhancedChanges, changesSummary);
37
+ // Use DiffProcessor for intelligent processing
38
+ const analysisMode = options.analysisMode || 'standard'
39
+ const diffProcessor = new DiffProcessor({
40
+ analysisMode,
41
+ enableFiltering: true,
42
+ enablePatternDetection: true,
43
+ })
33
44
 
34
- // Generate changelog content
45
+ const processedResult = diffProcessor.processFiles(enhancedChanges)
46
+
47
+ // Generate changelog content with processed files
35
48
  const changelog = await this.generateChangelogContent(
36
- enhancedChanges,
49
+ processedResult.processedFiles,
37
50
  changesSummary,
38
- workspaceContext,
39
- options.analysisMode || 'standard'
40
- );
51
+ processedResult,
52
+ analysisMode
53
+ )
41
54
 
42
55
  return {
43
56
  changelog,
44
57
  changes: enhancedChanges,
58
+ processedFiles: processedResult.processedFiles,
59
+ patterns: processedResult.patterns,
45
60
  summary: changesSummary,
46
- context: workspaceContext
47
- };
48
-
61
+ filesProcessed: processedResult.filesProcessed,
62
+ filesSkipped: processedResult.filesSkipped,
63
+ }
49
64
  } catch (error) {
50
- console.error(colors.errorMessage('Workspace changelog generation failed:'), error.message);
51
- throw error;
65
+ console.error(colors.errorMessage('Workspace changelog generation failed:'), error.message)
66
+ throw error
52
67
  }
53
68
  }
54
69
 
55
70
  async generateAIChangelogContentFromChanges(changes, changesSummary, analysisMode = 'standard') {
56
71
  if (!this.aiAnalysisService.hasAI) {
57
- console.log(colors.infoMessage('AI not available, using rule-based analysis...'));
58
- return this.generateBasicChangelogContentFromChanges(changes, changesSummary);
72
+ console.log(colors.infoMessage('AI not available, using rule-based analysis...'))
73
+ return this.generateBasicChangelogContentFromChanges(changes, changesSummary)
59
74
  }
60
75
 
61
76
  try {
62
- // Build comprehensive prompt with ALL change details
77
+ // Use DiffProcessor for intelligent diff processing
78
+ const diffProcessor = new DiffProcessor({
79
+ analysisMode,
80
+ enableFiltering: true,
81
+ enablePatternDetection: true,
82
+ })
83
+
84
+ const processedResult = diffProcessor.processFiles(changes)
85
+ const { processedFiles, patterns } = processedResult
86
+
87
+ // Build pattern summary if patterns were detected
88
+ const patternSummary =
89
+ Object.keys(patterns).length > 0
90
+ ? `\n\n**BULK PATTERNS DETECTED:**\n${Object.values(patterns)
91
+ .map((p) => `- ${p.description}`)
92
+ .join('\n')}`
93
+ : ''
94
+
95
+ // Build files section with processed diffs
96
+ const filesSection = processedFiles
97
+ .map((file) => {
98
+ if (file.isSummary) {
99
+ return `\n**[REMAINING FILES]:** ${file.diff}`
100
+ }
101
+
102
+ const compressionInfo = file.compressionApplied
103
+ ? ` [compressed from ${file.originalSize || 'unknown'} chars]`
104
+ : ''
105
+ const patternInfo = file.bulkPattern ? ` [${file.bulkPattern}]` : ''
106
+
107
+ return `\n**${file.filePath || file.path}** (${file.status})${compressionInfo}${patternInfo}:\n${file.diff}`
108
+ })
109
+ .join('\n')
110
+
111
+ // Build comprehensive prompt with processed changes
63
112
  const prompt = `Generate a comprehensive AI changelog for the following working directory changes:
64
113
 
65
114
  **Analysis Mode**: ${analysisMode}
66
- **Total Files**: ${changesSummary.totalFiles}
67
- **Categories**: ${Object.keys(changesSummary.categories).join(', ')}
68
-
69
- **Files by category with details**:
70
- ${Object.entries(changesSummary.categories).map(([cat, files]) =>
71
- `**${cat}**: ${files.map(f => {
72
- // Find the full change object with diff content
73
- const fullChange = changes.find(change => (change.path || change.filePath) === (f.path || f.filePath));
74
- const diffPreview = fullChange?.diff ?
75
- (fullChange.diff.length > 200 ? fullChange.diff.substring(0, 200) + '...' : fullChange.diff) :
76
- 'No diff available';
77
- return `${f.status} ${f.path} (${diffPreview.replace(/\n/g, ' ')})`;
78
- }).join('\n ')}`
79
- ).join('\n')}
115
+ **Total Files**: ${changesSummary.totalFiles} (${processedResult.filesProcessed} analyzed, ${processedResult.filesSkipped} summarized)
116
+ **Categories**: ${Object.keys(changesSummary.categories).join(', ')}${patternSummary}
117
+
118
+ **PROCESSED FILES:**${filesSection}
80
119
 
81
120
  CRITICAL INSTRUCTIONS FOR ANALYSIS:
82
121
  1. **ONLY DESCRIBE CHANGES VISIBLE IN THE DIFF CONTENT** - Do not invent or assume changes
@@ -87,12 +126,14 @@ CRITICAL INSTRUCTIONS FOR ANALYSIS:
87
126
 
88
127
  STRICT FORMATTING REQUIREMENTS:
89
128
  Generate working directory change entries based ONLY on visible diff content:
90
- - (type) brief factual description - List only the specific changes you can see in the diffs
129
+ - (type) Detailed but focused description - Include key functional changes, method/function names, and important technical details without overwhelming verbosity
130
+
131
+ EXAMPLES of CORRECT DETAILED FORMAT:
132
+ ✅ (feature) Created new bedrock.js file - Added BedrockProvider class with generateCompletion(), initializeClient(), and getAvailableModels() methods. Imported AWS SDK BedrockRuntimeClient and added support for Claude-3-5-sonnet and Llama-3.1 models with streaming capabilities.
91
133
 
92
- EXAMPLES of CORRECT FACTUAL FORMAT:
93
- ✅ (feature) Created new bedrock.js file - Added BedrockProvider class with generateCompletion(), initializeClient(), and getAvailableModels() methods. Imported AWS SDK BedrockRuntimeClient and added support for Claude and Llama models.
134
+ (refactor) Updated model list in anthropic.js - Changed getDefaultModel() return value from 'claude-3-5-sonnet-20241022' to 'claude-sonnet-4-20250514'. Added claude-sonnet-4 model entry with 200k context and updated pricing tier.
94
135
 
95
- ✅ (refactor) Updated model list in anthropic.js - Changed getDefaultModel() return value from 'claude-3-5-sonnet-20241022' to 'claude-sonnet-4-20250514'. Added new model entries with updated capabilities.
136
+ ✅ (fix) Updated configuration.manager.js - Added null check in getProviderConfig() method to prevent crashes when .env.local file is missing. Modified loadConfig() to gracefully handle missing environment files.
96
137
 
97
138
  EXAMPLES of FORBIDDEN ASSUMPTIONS:
98
139
  ❌ "Updated other providers to recognize bedrock" (not visible in diff)
@@ -100,95 +141,120 @@ EXAMPLES of FORBIDDEN ASSUMPTIONS:
100
141
  ❌ "Improved integration across the system" (speculation)
101
142
  ❌ "Enhanced error handling throughout" (assumption)
102
143
 
103
- ONLY describe what you can literally see in the diff content. Do not invent connections or integrations.`;
144
+ ONLY describe what you can literally see in the diff content. Do not invent connections or integrations.`
104
145
 
105
146
  // Make AI call with all the context
106
147
  const messages = [
107
148
  {
108
- role: "system",
109
- content: "You are an expert at analyzing code changes and generating factual changelog entries. You MUST only describe changes that are visible in the provided diff content. Never make assumptions, never invent integrations, never speculate about how files work together. Be precise and factual - only describe what you can literally see in the diffs."
149
+ role: 'system',
150
+ content:
151
+ 'You are an expert at analyzing code changes and generating detailed but focused changelog entries. You MUST only describe changes that are visible in the provided diff content. Include specific function/method names, key technical details, and the functional purpose of changes. Be precise and factual - only describe what you can literally see in the diffs. Provide enough detail to understand what changed technically, but avoid overwhelming verbosity.',
110
152
  },
111
153
  {
112
- role: "user",
113
- content: prompt
114
- }
115
- ];
154
+ role: 'user',
155
+ content: prompt,
156
+ },
157
+ ]
116
158
 
117
159
  const options = {
118
- max_tokens: analysisMode === 'enterprise' ? 2000 : analysisMode === 'detailed' ? 1500 : 1000,
119
- temperature: 0.3
120
- };
121
-
122
-
123
- const response = await this.aiAnalysisService.aiProvider.generateCompletion(messages, options);
160
+ max_tokens:
161
+ analysisMode === 'enterprise' ? 2500 : analysisMode === 'detailed' ? 2000 : 1200,
162
+ temperature: 0.2,
163
+ }
124
164
 
125
- let changelog = response.content || response.text;
165
+ const response = await this.aiAnalysisService.aiProvider.generateCompletion(messages, options)
166
+
167
+ let changelog = response.content || response.text
168
+
169
+ // Check if changelog content is valid
170
+ if (!changelog || typeof changelog !== 'string') {
171
+ console.warn(colors.warningMessage('⚠️ AI response was empty or invalid'))
172
+ console.warn(colors.infoMessage('💡 Using basic file change detection instead'))
173
+
174
+ // Generate basic changelog from file changes
175
+ const timestamp = new Date().toISOString().split('T')[0]
176
+ const fallbackChanges = rawChanges || getWorkingDirectoryChanges()
177
+ const basicEntries = fallbackChanges.map((change) => {
178
+ const filePath = change.filePath || change.path || 'unknown file'
179
+ const status = change.status || 'M'
180
+ const changeType =
181
+ status === 'M'
182
+ ? 'Modified'
183
+ : status === 'A'
184
+ ? 'Added'
185
+ : status === 'D'
186
+ ? 'Deleted'
187
+ : 'Changed'
188
+ return `- ${changeType} ${filePath}`
189
+ })
190
+
191
+ changelog = `# Working Directory Changelog - ${timestamp}\n\n## Changes\n\n${basicEntries.join('\n')}`
192
+ }
126
193
 
127
194
  // Add metadata
128
- const timestamp = new Date().toISOString().split('T')[0];
195
+ const timestamp = new Date().toISOString().split('T')[0]
129
196
 
130
- // Ensure proper changelog format
197
+ // Ensure proper changelog format with Keep a Changelog header
131
198
  if (!changelog.includes('# ')) {
132
- changelog = `# Working Directory Changelog - ${timestamp}\n\n${changelog}`;
199
+ changelog = `# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased] - ${timestamp}\n\n${changelog}`
133
200
  }
134
201
 
135
202
  // Add generation metadata
136
- changelog += `\n\n---\n\n*Generated from ${changesSummary.totalFiles} working directory changes*\n`;
137
-
138
- return changelog;
203
+ changelog += `\n\n---\n\n*Generated from ${changesSummary.totalFiles} working directory changes*\n`
139
204
 
205
+ return changelog
140
206
  } catch (error) {
141
207
  // Specific error guidance for AI failures
142
208
  if (error.message.includes('fetch failed') || error.message.includes('ECONNREFUSED')) {
143
- console.warn(colors.warningMessage('⚠️ Cannot connect to AI provider'));
144
- console.warn(colors.infoMessage('💡 Check internet connection and provider service status'));
209
+ console.warn(colors.warningMessage('⚠️ Cannot connect to AI provider'))
210
+ console.warn(colors.infoMessage('💡 Check internet connection and provider service status'))
145
211
  } else if (error.message.includes('API key') || error.message.includes('401')) {
146
- console.warn(colors.warningMessage('⚠️ API authentication failed'));
147
- console.warn(colors.infoMessage('💡 Run: ai-changelog init'));
212
+ console.warn(colors.warningMessage('⚠️ API authentication failed'))
213
+ console.warn(colors.infoMessage('💡 Run: ai-changelog init'))
148
214
  } else if (error.message.includes('rate limit')) {
149
- console.warn(colors.warningMessage('⚠️ Rate limit exceeded'));
150
- console.warn(colors.infoMessage('💡 Wait a moment before retrying'));
215
+ console.warn(colors.warningMessage('⚠️ Rate limit exceeded'))
216
+ console.warn(colors.infoMessage('💡 Wait a moment before retrying'))
151
217
  } else {
152
- console.warn(colors.warningMessage(`⚠️ AI analysis failed: ${error.message}`));
218
+ console.warn(colors.warningMessage(`⚠️ AI analysis failed: ${error.message}`))
153
219
  }
154
220
 
155
- console.warn(colors.infoMessage('🔄 Falling back to pattern-based analysis'));
156
- return this.generateBasicChangelogContentFromChanges(changes, changesSummary);
221
+ console.warn(colors.infoMessage('🔄 Falling back to pattern-based analysis'))
222
+ return this.generateBasicChangelogContentFromChanges(changes, changesSummary)
157
223
  }
158
224
  }
159
225
 
160
226
  generateBasicChangelogContentFromChanges(changes, changesSummary) {
161
- const timestamp = new Date().toISOString().split('T')[0];
227
+ const timestamp = new Date().toISOString().split('T')[0]
162
228
 
163
- let changelog = `# Working Directory Changes - ${timestamp}\n\n`;
229
+ let changelog = `# Working Directory Changes - ${timestamp}\n\n`
164
230
 
165
231
  // Basic summary
166
- changelog += `## Summary\n`;
167
- changelog += `${changes.length} files modified across ${Object.keys(changesSummary.categories).length} categories.\n\n`;
232
+ changelog += '## Summary\n'
233
+ changelog += `${changes.length} files modified across ${Object.keys(changesSummary.categories).length} categories.\n\n`
168
234
 
169
235
  // Changes by category
170
- changelog += this.buildChangesByCategory(changes, changesSummary);
236
+ changelog += this.buildChangesByCategory(changes, changesSummary)
171
237
 
172
238
  // Basic recommendations
173
- changelog += `## Recommendations\n`;
174
- changelog += `- Review changes before committing\n`;
175
- changelog += `- Consider adding tests for new functionality\n`;
176
- changelog += `- Update documentation if needed\n\n`;
239
+ changelog += '## Recommendations\n'
240
+ changelog += '- Review changes before committing\n'
241
+ changelog += '- Consider adding tests for new functionality\n'
242
+ changelog += '- Update documentation if needed\n\n'
177
243
 
178
- return changelog;
244
+ return changelog
179
245
  }
180
246
 
181
247
  async enhanceChangesWithDiff(changes) {
182
- const enhancedChanges = [];
248
+ const enhancedChanges = []
183
249
 
184
250
  for (const change of changes) {
185
- let enhancedChange = {
251
+ const enhancedChange = {
186
252
  ...change,
187
253
  category: categorizeFile(change.path || change.filePath),
188
254
  language: detectLanguage(change.path || change.filePath),
189
255
  importance: assessFileImportance(change.path || change.filePath, change.status),
190
- enhanced: true
191
- };
256
+ enhanced: true,
257
+ }
192
258
 
193
259
  // Get diff content if git service is available
194
260
  if (this.gitService) {
@@ -196,28 +262,27 @@ ONLY describe what you can literally see in the diff content. Do not invent conn
196
262
  const diffAnalysis = await this.gitService.analyzeWorkingDirectoryFileChange(
197
263
  change.status,
198
264
  change.path || change.filePath
199
- );
265
+ )
200
266
 
201
267
  if (diffAnalysis) {
202
- enhancedChange.diff = diffAnalysis.diff;
203
- enhancedChange.beforeContent = diffAnalysis.beforeContent;
204
- enhancedChange.afterContent = diffAnalysis.afterContent;
205
- enhancedChange.semanticChanges = diffAnalysis.semanticChanges;
206
- enhancedChange.functionalImpact = diffAnalysis.functionalImpact;
207
- enhancedChange.complexity = diffAnalysis.complexity;
268
+ enhancedChange.diff = diffAnalysis.diff
269
+ enhancedChange.beforeContent = diffAnalysis.beforeContent
270
+ enhancedChange.afterContent = diffAnalysis.afterContent
271
+ enhancedChange.semanticChanges = diffAnalysis.semanticChanges
272
+ enhancedChange.functionalImpact = diffAnalysis.functionalImpact
273
+ enhancedChange.complexity = diffAnalysis.complexity
208
274
  }
209
275
  } catch (error) {
210
- console.warn(`Failed to get diff for ${change.path || change.filePath}:`, error.message);
276
+ console.warn(`Failed to get diff for ${change.path || change.filePath}:`, error.message)
211
277
  }
212
278
  }
213
279
 
214
- enhancedChanges.push(enhancedChange);
280
+ enhancedChanges.push(enhancedChange)
215
281
  }
216
282
 
217
- return enhancedChanges;
283
+ return enhancedChanges
218
284
  }
219
285
 
220
-
221
286
  generateWorkspaceContext(changes, summary) {
222
287
  const context = {
223
288
  totalFiles: changes.length,
@@ -225,281 +290,387 @@ ONLY describe what you can literally see in the diff content. Do not invent conn
225
290
  primaryCategory: this.getPrimaryCategory(summary.categories),
226
291
  riskLevel: this.assessWorkspaceRisk(changes),
227
292
  complexity: this.assessWorkspaceComplexity(changes),
228
- recommendations: this.generateRecommendations(changes)
229
- };
293
+ recommendations: this.generateRecommendations(changes),
294
+ }
230
295
 
231
- return context;
296
+ return context
232
297
  }
233
298
 
234
299
  getPrimaryCategory(categories) {
235
- return Object.entries(categories)
236
- .sort(([,a], [,b]) => b.length - a.length)[0]?.[0] || 'other';
300
+ return Object.entries(categories).sort(([, a], [, b]) => b.length - a.length)[0]?.[0] || 'other'
237
301
  }
238
302
 
239
303
  assessWorkspaceRisk(changes) {
240
- const highRiskFiles = changes.filter(change =>
241
- change.importance === 'critical' ||
242
- change.category === 'configuration' ||
243
- change.status === 'D'
244
- );
245
-
246
- if (highRiskFiles.length > changes.length * 0.3) return 'high';
247
- if (highRiskFiles.length > 0) return 'medium';
248
- return 'low';
304
+ const highRiskFiles = changes.filter(
305
+ (change) =>
306
+ change.importance === 'critical' ||
307
+ change.category === 'configuration' ||
308
+ change.status === 'D'
309
+ )
310
+
311
+ if (highRiskFiles.length > changes.length * 0.3) {
312
+ return 'high'
313
+ }
314
+ if (highRiskFiles.length > 0) {
315
+ return 'medium'
316
+ }
317
+ return 'low'
249
318
  }
250
319
 
251
320
  assessWorkspaceComplexity(changes) {
252
- if (changes.length > 20) return 'high';
253
- if (changes.length > 5) return 'medium';
254
- return 'low';
321
+ if (changes.length > 20) {
322
+ return 'high'
323
+ }
324
+ if (changes.length > 5) {
325
+ return 'medium'
326
+ }
327
+ return 'low'
255
328
  }
256
329
 
257
330
  generateRecommendations(changes) {
258
- const recommendations = [];
331
+ const recommendations = []
259
332
 
260
- const hasTests = changes.some(change => change.category === 'tests');
261
- const hasSource = changes.some(change => change.category === 'source');
262
- const hasConfig = changes.some(change => change.category === 'configuration');
263
- const hasDocs = changes.some(change => change.category === 'documentation');
333
+ const hasTests = changes.some((change) => change.category === 'tests')
334
+ const hasSource = changes.some((change) => change.category === 'source')
335
+ const hasConfig = changes.some((change) => change.category === 'configuration')
336
+ const hasDocs = changes.some((change) => change.category === 'documentation')
264
337
 
265
338
  if (hasSource && !hasTests) {
266
- recommendations.push('Consider adding tests for source code changes');
339
+ recommendations.push('Consider adding tests for source code changes')
267
340
  }
268
341
 
269
342
  if (hasConfig) {
270
- recommendations.push('Review configuration changes carefully');
343
+ recommendations.push('Review configuration changes carefully')
271
344
  }
272
345
 
273
346
  if (hasSource && !hasDocs) {
274
- recommendations.push('Update documentation for new features');
347
+ recommendations.push('Update documentation for new features')
275
348
  }
276
349
 
277
350
  if (changes.length > 15) {
278
- recommendations.push('Consider breaking this into smaller commits');
351
+ recommendations.push('Consider breaking this into smaller commits')
279
352
  }
280
353
 
281
- const deletedFiles = changes.filter(change => change.status === 'D');
354
+ const deletedFiles = changes.filter((change) => change.status === 'D')
282
355
  if (deletedFiles.length > 0) {
283
- recommendations.push(`Review ${deletedFiles.length} deleted files before committing`);
356
+ recommendations.push(`Review ${deletedFiles.length} deleted files before committing`)
284
357
  }
285
358
 
286
- return recommendations;
359
+ return recommendations
287
360
  }
288
361
 
289
- buildChangesByCategory(changes, changesSummary) {
290
- let content = `## Changes by Category\n\n`;
362
+ buildChangesByCategory(_changes, changesSummary) {
363
+ let content = '## Changes by Category\n\n'
291
364
 
292
365
  Object.entries(changesSummary.categories).forEach(([category, files]) => {
293
- const categoryIcon = this.getCategoryIcon(category);
294
- content += `### ${categoryIcon} ${category.charAt(0).toUpperCase() + category.slice(1)} (${files.length} files)\n\n`;
366
+ const categoryIcon = this.getCategoryIcon(category)
367
+ content += `### ${categoryIcon} ${category.charAt(0).toUpperCase() + category.slice(1)} (${files.length} files)\n\n`
295
368
 
296
- files.forEach(file => {
297
- const statusIcon = this.getStatusIcon(file.status);
298
- content += `- ${statusIcon} ${file.path}\n`;
299
- });
369
+ files.forEach((file) => {
370
+ const statusIcon = this.getStatusIcon(file.status)
371
+ content += `- ${statusIcon} ${file.path}\n`
372
+ })
300
373
 
301
- content += '\n';
302
- });
374
+ content += '\n'
375
+ })
303
376
 
304
- return content;
377
+ return content
305
378
  }
306
379
 
307
380
  getCategoryIcon(category) {
308
381
  const icons = {
309
- 'source': '💻',
310
- 'tests': '🧪',
311
- 'documentation': '📚',
312
- 'configuration': '⚙️',
313
- 'frontend': '🎨',
314
- 'assets': '🖼️',
315
- 'build': '🔧',
316
- 'other': '📄'
317
- };
318
- return icons[category] || '📄';
382
+ source: '💻',
383
+ tests: '🧪',
384
+ documentation: '📚',
385
+ configuration: '⚙️',
386
+ frontend: '🎨',
387
+ assets: '🖼️',
388
+ build: '🔧',
389
+ other: '📄',
390
+ }
391
+ return icons[category] || '📄'
319
392
  }
320
393
 
321
394
  getStatusIcon(status) {
322
395
  const icons = {
323
- 'A': '➕', // Added
324
- 'M': '✏️', // Modified
325
- 'D': '❌', // Deleted
326
- 'R': '📝', // Renamed
327
- 'C': '📋' // Copied
328
- };
329
- return icons[status] || '📄';
396
+ A: '➕', // Added
397
+ M: '✏️', // Modified
398
+ D: '❌', // Deleted
399
+ R: '📝', // Renamed
400
+ C: '📋', // Copied
401
+ }
402
+ return icons[status] || '📄'
330
403
  }
331
404
 
332
- async generateChangelogContent(changes, summary, context, analysisMode) {
405
+ async generateChangelogContent(changes, summary, _context, analysisMode) {
333
406
  if (analysisMode === 'detailed' || analysisMode === 'enterprise') {
334
- return await this.generateAIChangelogContentFromChanges(changes, summary, analysisMode);
335
- } else {
336
- return this.generateBasicChangelogContentFromChanges(changes, summary);
407
+ return await this.generateAIChangelogContentFromChanges(changes, summary, analysisMode)
337
408
  }
409
+ return this.generateBasicChangelogContentFromChanges(changes, summary)
338
410
  }
339
411
 
340
412
  // Integration with main changelog service
341
413
  async generateCommitStyleWorkingDirectoryEntries(options = {}) {
342
- try {
343
- // Use provided working directory analysis or get current changes
344
- let rawChanges;
345
- if (options.workingDirAnalysis && options.workingDirAnalysis.changes) {
346
- rawChanges = options.workingDirAnalysis.changes;
347
- } else {
348
- rawChanges = getWorkingDirectoryChanges();
349
- }
414
+ // Use provided working directory analysis or get current changes
415
+ let rawChanges
416
+ if (options.workingDirAnalysis?.changes) {
417
+ rawChanges = options.workingDirAnalysis.changes
418
+ } else {
419
+ rawChanges = getWorkingDirectoryChanges()
420
+ }
350
421
 
351
- if (!rawChanges || !Array.isArray(rawChanges) || rawChanges.length === 0) {
352
- return { entries: [] };
422
+ try {
423
+ if (!(rawChanges && Array.isArray(rawChanges)) || rawChanges.length === 0) {
424
+ return { entries: [] }
353
425
  }
354
426
 
355
427
  // Enhanced analysis of changes with diff content for AI analysis
356
- const enhancedChanges = await this.enhanceChangesWithDiff(rawChanges);
357
- const changesSummary = summarizeFileChanges(enhancedChanges);
428
+ const enhancedChanges = await this.enhanceChangesWithDiff(rawChanges)
429
+ const changesSummary = summarizeFileChanges(enhancedChanges)
430
+
431
+ // Use DiffProcessor for intelligent diff processing
432
+ const analysisMode =
433
+ this.aiAnalysisService?.analysisMode || options.analysisMode || 'standard'
434
+ const diffProcessor = new DiffProcessor({
435
+ analysisMode,
436
+ enableFiltering: true,
437
+ enablePatternDetection: true,
438
+ })
439
+
440
+ const processedResult = diffProcessor.processFiles(enhancedChanges)
441
+ const { processedFiles, patterns } = processedResult
442
+
443
+ // Build pattern summary if patterns were detected
444
+ const patternSummary =
445
+ Object.keys(patterns).length > 0
446
+ ? `\n\n**BULK PATTERNS DETECTED:**\n${Object.values(patterns)
447
+ .map((p) => `- ${p.description}`)
448
+ .join('\n')}`
449
+ : ''
450
+
451
+ // Build files section with processed diffs
452
+ const filesSection = processedFiles
453
+ .map((file) => {
454
+ if (file.isSummary) {
455
+ return `\n**[REMAINING FILES]:** ${file.diff}`
456
+ }
457
+
458
+ const compressionInfo = file.compressionApplied
459
+ ? ` [compressed from ${file.originalSize || 'unknown'} chars]`
460
+ : ''
461
+ const patternInfo = file.bulkPattern ? ` [${file.bulkPattern}]` : ''
462
+
463
+ return `\n**${file.filePath || file.path}** (${file.status})${compressionInfo}${patternInfo}:\n${file.diff}`
464
+ })
465
+ .join('\n')
358
466
 
359
467
  // Build prompt for commit-style entries
360
468
  const prompt = `Generate working directory change entries in the SAME FORMAT as git commits:
361
469
 
362
- **Analysis Mode**: ${options.analysisMode || 'standard'}
363
- **Total Files**: ${changesSummary.totalFiles}
364
- **Categories**: ${Object.keys(changesSummary.categories).join(', ')}
365
-
366
- **Files by category with details**:
367
- ${Object.entries(changesSummary.categories).map(([cat, files]) =>
368
- `**${cat}**: ${files.map(f => {
369
- // Find the full change object with diff content
370
- const fullChange = enhancedChanges.find(change => (change.path || change.filePath) === (f.path || f.filePath));
371
- const diffPreview = fullChange?.diff ?
372
- (fullChange.diff.length > 200 ? fullChange.diff.substring(0, 200) + '...' : fullChange.diff) :
373
- 'No diff available';
374
- return `${f.status} ${f.path} (${diffPreview.replace(/\n/g, ' ')})`;
375
- }).join('\n ')}`
376
- ).join('\n')}
470
+ **Analysis Mode**: ${analysisMode}
471
+ **Total Files**: ${changesSummary.totalFiles} (${processedResult.filesProcessed} analyzed, ${processedResult.filesSkipped} summarized)
472
+ **Categories**: ${Object.keys(changesSummary.categories).join(', ')}${patternSummary}
473
+
474
+ **PROCESSED FILES:**${filesSection}
377
475
 
378
476
  STRICT FORMATTING REQUIREMENTS:
379
477
  Generate working directory change entries based ONLY on visible diff content:
380
- - (type) brief factual description - List only the specific changes you can see in the diffs
478
+ - (type) Detailed but focused description - Include key functional changes, method/function names, and important technical details without overwhelming verbosity
381
479
 
382
480
  Where:
383
481
  - type = feature, fix, refactor, docs, chore, etc. based on the actual changes
384
- - brief description = concise summary of what was actually changed
385
- - Detailed explanation = specific lines, functions, imports that were added/removed/modified
482
+ - Detailed description = specific functions/methods affected, key technical changes, and functional purpose
483
+ - Include exact method names, variable names, and technical specifics from the diffs
484
+
485
+ EXAMPLES of CORRECT DETAILED FORMAT:
486
+ - (feature) Created new bedrock.js file - Added BedrockProvider class with generateCompletion(), initializeClient(), and getAvailableModels() methods. Imported AWS SDK BedrockRuntimeClient and added support for Claude-3-5-sonnet and Llama-3.1 models with streaming capabilities.
386
487
 
387
- EXAMPLES of CORRECT FACTUAL FORMAT:
388
- - (feature) Created new bedrock.js file - Added BedrockProvider class with generateCompletion(), initializeClient(), and getAvailableModels() methods. Imported AWS SDK BedrockRuntimeClient and added support for Claude and Llama models.
488
+ - (refactor) Updated model list in anthropic.js - Changed getDefaultModel() return value from 'claude-3-5-sonnet-20241022' to 'claude-sonnet-4-20250514'. Added claude-sonnet-4 model entry with 200k context window and updated pricing tier.
389
489
 
390
- - (refactor) Updated model list in anthropic.js - Changed getDefaultModel() return value from 'claude-3-5-sonnet-20241022' to 'claude-sonnet-4-20250514'. Added new model entries in getAvailableModels() method.
490
+ - (fix) Updated configuration.manager.js - Added null check in getProviderConfig() method to prevent crashes when .env.local file is missing. Modified loadConfig() to gracefully handle missing environment files.
391
491
 
392
492
  FORBIDDEN - DO NOT MAKE ASSUMPTIONS:
393
493
  ❌ Do not mention "integration" unless you see actual integration code
394
494
  ❌ Do not mention "provider selection logic" unless you see that specific code
395
495
  ❌ Do not assume files work together unless explicitly shown in diffs
396
496
 
397
- Generate one entry per file or logical change group. Only describe what you can literally see.`;
497
+ Generate one entry per file or logical change group. Only describe what you can literally see.`
398
498
 
399
499
  // Make AI call
400
500
  const messages = [
401
501
  {
402
- role: "system",
403
- content: "You are an expert at analyzing code changes and generating factual commit-style changelog entries. You MUST only describe changes that are visible in the provided diff content. Never make assumptions, never invent integrations, never speculate. Be precise and factual - only describe what you can literally see in the diffs."
502
+ role: 'system',
503
+ content:
504
+ 'You are an expert at analyzing code changes and generating detailed but focused commit-style changelog entries. You MUST only describe changes that are visible in the provided diff content. Include specific function/method names, key technical details, and the functional purpose of changes. Be precise and factual - only describe what you can literally see in the diffs. Provide enough detail to understand what changed technically, but avoid overwhelming verbosity.',
404
505
  },
405
506
  {
406
- role: "user",
407
- content: prompt
408
- }
409
- ];
410
-
411
- const options_ai = {
412
- max_tokens: 1000,
413
- temperature: 0.3
414
- };
507
+ role: 'user',
508
+ content: prompt,
509
+ },
510
+ ]
511
+
512
+ // Set token limits based on analysis mode and number of changes
513
+ let maxTokens = 1200 // Default
514
+ if (analysisMode === 'enterprise') {
515
+ maxTokens = 3000
516
+ } else if (analysisMode === 'detailed') {
517
+ maxTokens = 2500
518
+ }
415
519
 
520
+ // Increase token limit for large numbers of working directory changes
521
+ if (enhancedChanges.length > 50) {
522
+ maxTokens = Math.min(maxTokens + 1500, 6000)
523
+ }
416
524
 
417
- const response = await this.aiAnalysisService.aiProvider.generateCompletion(messages, options_ai);
525
+ const options_ai = {
526
+ max_tokens: maxTokens,
527
+ temperature: 0.3,
528
+ }
418
529
 
419
- let content = response.content || response.text;
530
+ const response = await this.aiAnalysisService.aiProvider.generateCompletion(
531
+ messages,
532
+ options_ai
533
+ )
534
+
535
+ const content = response.content || response.text
536
+
537
+ // Check if content is valid before processing
538
+ if (!content || typeof content !== 'string') {
539
+ console.warn(colors.warningMessage('⚠️ AI response was empty or invalid'))
540
+ console.warn(colors.infoMessage('💡 Using basic file change detection instead'))
541
+
542
+ // Fallback to basic entries from the changes we were given
543
+ const fallbackChanges = rawChanges || getWorkingDirectoryChanges()
544
+ const basicEntries = fallbackChanges.map((change) => {
545
+ const filePath = change.filePath || change.path || 'unknown file'
546
+ const status = change.status || 'M'
547
+ const changeType =
548
+ status === 'M'
549
+ ? 'update'
550
+ : status === 'A'
551
+ ? 'feature'
552
+ : status === 'D'
553
+ ? 'remove'
554
+ : 'chore'
555
+ const changeDesc =
556
+ status === 'M'
557
+ ? 'updated'
558
+ : status === 'A'
559
+ ? 'added'
560
+ : status === 'D'
561
+ ? 'deleted'
562
+ : 'changed'
563
+ return `- (${changeType}) Modified ${filePath} - File ${changeDesc} (pattern-based analysis)`
564
+ })
565
+
566
+ return { entries: basicEntries }
567
+ }
420
568
 
421
569
  // Parse entries from response
422
- const entries = content.split('\n')
423
- .filter(line => line.trim().startsWith('-'))
424
- .map(line => line.trim());
425
-
570
+ const entries = content
571
+ .split('\n')
572
+ .filter((line) => {
573
+ const trimmed = line.trim()
574
+ // Accept lines starting with '- (' or directly with '(' for changelog entries
575
+ return trimmed.startsWith('- (') || trimmed.startsWith('(')
576
+ })
577
+ .map((line) => {
578
+ const trimmed = line.trim()
579
+ // Ensure all entries start with '- ' for consistent formatting
580
+ return trimmed.startsWith('- ') ? trimmed : `- ${trimmed}`
581
+ })
426
582
  return {
427
583
  entries,
428
584
  changes: enhancedChanges,
429
- summary: changesSummary
430
- };
431
-
585
+ summary: changesSummary,
586
+ }
432
587
  } catch (error) {
433
588
  // Provide specific guidance based on error type
434
589
  if (error.message.includes('fetch failed') || error.message.includes('connection')) {
435
- console.warn(colors.warningMessage('⚠️ AI provider connection failed'));
436
- console.warn(colors.infoMessage('💡 Check your internet connection and provider configuration'));
590
+ console.warn(colors.warningMessage('⚠️ AI provider connection failed'))
591
+ console.warn(
592
+ colors.infoMessage('💡 Check your internet connection and provider configuration')
593
+ )
437
594
  } else if (error.message.includes('API key') || error.message.includes('401')) {
438
- console.warn(colors.warningMessage('⚠️ Authentication failed'));
439
- console.warn(colors.infoMessage('💡 Run `ai-changelog init` to configure your API key'));
595
+ console.warn(colors.warningMessage('⚠️ Authentication failed'))
596
+ console.warn(colors.infoMessage('💡 Run `ai-changelog init` to configure your API key'))
440
597
  } else {
441
- console.warn(colors.warningMessage(`⚠️ AI analysis failed: ${error.message}`));
442
- console.warn(colors.infoMessage('💡 Using basic file change detection instead'));
598
+ console.warn(colors.warningMessage(`⚠️ AI analysis failed: ${error.message}`))
599
+ console.warn(colors.infoMessage('💡 Using basic file change detection instead'))
443
600
  }
444
601
 
445
602
  // Return basic entries from the changes we were given instead of getting fresh ones
446
- const fallbackChanges = rawChanges || getWorkingDirectoryChanges();
447
- const basicEntries = fallbackChanges.map(change => {
448
- const filePath = change.filePath || change.path || 'unknown file';
449
- const status = change.status || 'M';
450
- const changeType = status === 'M' ? 'update' : status === 'A' ? 'feature' : status === 'D' ? 'remove' : 'chore';
451
- const changeDesc = status === 'M' ? 'updated' : status === 'A' ? 'added' : status === 'D' ? 'deleted' : 'changed';
452
- return `- (${changeType}) Modified ${filePath} - File ${changeDesc} (pattern-based analysis)`;
453
- });
454
-
455
- return { entries: basicEntries };
603
+ const fallbackChanges = rawChanges || getWorkingDirectoryChanges()
604
+ const basicEntries = fallbackChanges.map((change) => {
605
+ const filePath = change.filePath || change.path || 'unknown file'
606
+ const status = change.status || 'M'
607
+ const changeType =
608
+ status === 'M'
609
+ ? 'update'
610
+ : status === 'A'
611
+ ? 'feature'
612
+ : status === 'D'
613
+ ? 'remove'
614
+ : 'chore'
615
+ const changeDesc =
616
+ status === 'M'
617
+ ? 'updated'
618
+ : status === 'A'
619
+ ? 'added'
620
+ : status === 'D'
621
+ ? 'deleted'
622
+ : 'changed'
623
+ return `- (${changeType}) Modified ${filePath} - File ${changeDesc} (pattern-based analysis)`
624
+ })
625
+
626
+ return { entries: basicEntries }
456
627
  }
457
628
  }
458
629
 
459
630
  async generateWorkspaceChangelog(version = null, options = {}) {
460
- const result = await this.generateComprehensiveWorkspaceChangelog(options);
631
+ const result = await this.generateComprehensiveWorkspaceChangelog(options)
461
632
 
462
633
  if (!result) {
463
- return null;
634
+ return null
464
635
  }
465
636
 
466
- let changelog = result.changelog;
637
+ let changelog = result.changelog
467
638
 
468
639
  // Add version information if provided
469
640
  if (version) {
470
641
  changelog = changelog.replace(
471
642
  /# Working Directory Changelog/,
472
643
  `# Working Directory Changelog - Version ${version}`
473
- );
644
+ )
474
645
  }
475
646
 
476
647
  // Add context information for detailed modes
477
648
  if (options.analysisMode === 'detailed' || options.analysisMode === 'enterprise') {
478
- changelog += this.generateContextSection(result.context);
649
+ changelog += this.generateContextSection(result.context)
479
650
  }
480
651
 
481
652
  return {
482
653
  ...result,
483
654
  changelog,
484
- version
485
- };
655
+ version,
656
+ }
486
657
  }
487
658
 
488
659
  generateContextSection(context) {
489
- let section = `## Context Analysis\n\n`;
490
- section += `- **Total Files:** ${context.totalFiles}\n`;
491
- section += `- **Primary Category:** ${context.primaryCategory}\n`;
492
- section += `- **Risk Level:** ${context.riskLevel}\n`;
493
- section += `- **Complexity:** ${context.complexity}\n\n`;
660
+ let section = '## Context Analysis\n\n'
661
+ section += `- **Total Files:** ${context.totalFiles}\n`
662
+ section += `- **Primary Category:** ${context.primaryCategory}\n`
663
+ section += `- **Risk Level:** ${context.riskLevel}\n`
664
+ section += `- **Complexity:** ${context.complexity}\n\n`
494
665
 
495
666
  if (context.recommendations.length > 0) {
496
- section += `### Recommendations\n\n`;
497
- context.recommendations.forEach(rec => {
498
- section += `- ${rec}\n`;
499
- });
500
- section += '\n';
667
+ section += '### Recommendations\n\n'
668
+ context.recommendations.forEach((rec) => {
669
+ section += `- ${rec}\n`
670
+ })
671
+ section += '\n'
501
672
  }
502
673
 
503
- return section;
674
+ return section
504
675
  }
505
- }
676
+ }