@entro314labs/ai-changelog-generator 3.2.1 → 3.6.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 (36) hide show
  1. package/CHANGELOG.md +42 -2
  2. package/README.md +21 -1
  3. package/ai-changelog-mcp.sh +0 -0
  4. package/ai-changelog.sh +0 -0
  5. package/bin/ai-changelog-dxt.js +6 -3
  6. package/manifest.json +177 -0
  7. package/package.json +76 -81
  8. package/src/ai-changelog-generator.js +5 -4
  9. package/src/application/orchestrators/changelog.orchestrator.js +19 -203
  10. package/src/cli.js +16 -5
  11. package/src/domains/ai/ai-analysis.service.js +2 -0
  12. package/src/domains/analysis/analysis.engine.js +714 -37
  13. package/src/domains/changelog/changelog.service.js +623 -32
  14. package/src/domains/changelog/workspace-changelog.service.js +445 -622
  15. package/src/domains/git/commit-tagger.js +552 -0
  16. package/src/domains/git/git-manager.js +357 -0
  17. package/src/domains/git/git.service.js +865 -16
  18. package/src/infrastructure/cli/cli.controller.js +14 -9
  19. package/src/infrastructure/config/configuration.manager.js +25 -11
  20. package/src/infrastructure/interactive/interactive-workflow.service.js +8 -1
  21. package/src/infrastructure/mcp/mcp-server.service.js +105 -32
  22. package/src/infrastructure/providers/core/base-provider.js +1 -1
  23. package/src/infrastructure/providers/implementations/anthropic.js +16 -173
  24. package/src/infrastructure/providers/implementations/azure.js +16 -63
  25. package/src/infrastructure/providers/implementations/dummy.js +13 -16
  26. package/src/infrastructure/providers/implementations/mock.js +13 -26
  27. package/src/infrastructure/providers/implementations/ollama.js +12 -4
  28. package/src/infrastructure/providers/implementations/openai.js +13 -165
  29. package/src/infrastructure/providers/provider-management.service.js +126 -412
  30. package/src/infrastructure/providers/utils/base-provider-helpers.js +11 -0
  31. package/src/shared/utils/cli-ui.js +8 -10
  32. package/src/shared/utils/diff-processor.js +21 -19
  33. package/src/shared/utils/error-classes.js +33 -0
  34. package/src/shared/utils/utils.js +83 -63
  35. package/types/index.d.ts +61 -68
  36. package/src/domains/git/git-repository.analyzer.js +0 -678
@@ -1,743 +1,566 @@
1
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'
2
+ import { getWorkingDirectoryChanges } from '../../shared/utils/utils.js'
3
+ import { ChangelogService } from './changelog.service.js'
11
4
 
12
5
  /**
13
- * Workspace Changelog Service
6
+ * WorkspaceChangelogService extends ChangelogService with workspace-specific functionality
7
+ * for analyzing uncommitted changes and workspace state
14
8
  *
15
- * Handles changelog generation from working directory changes
9
+ * @deprecated This class is being phased out. The workspace changelog logic has been
10
+ * consolidated into ChangelogService. This class is maintained for backward compatibility
11
+ * with existing tests but should not be used in new code.
16
12
  */
17
- export class WorkspaceChangelogService {
18
- constructor(aiAnalysisService, gitService = null) {
19
- this.aiAnalysisService = aiAnalysisService
20
- this.gitService = gitService
13
+ export class WorkspaceChangelogService extends ChangelogService {
14
+ constructor(gitService, aiAnalysisService, analysisEngine = null, configManager = null) {
15
+ super(gitService, aiAnalysisService, analysisEngine, configManager)
16
+ this.workspaceMetrics = {
17
+ unstagedFiles: 0,
18
+ stagedFiles: 0,
19
+ untrackedFiles: 0,
20
+ modifiedLines: 0,
21
+ }
21
22
  }
22
23
 
23
- async generateComprehensiveWorkspaceChangelog(options = {}) {
24
- try {
25
- // Get working directory changes as raw array
26
- const rawChanges = getWorkingDirectoryChanges()
24
+ /**
25
+ * Analyze workspace changes without committing
26
+ * @returns {Promise<Object>} Workspace analysis results
27
+ */
28
+ async analyzeWorkspaceChanges() {
29
+ console.log(colors.processingMessage('🔍 Analyzing workspace changes...'))
27
30
 
28
- if (!(rawChanges && Array.isArray(rawChanges)) || rawChanges.length === 0) {
29
- EnhancedConsole.info('No changes detected in working directory.')
30
- return null
31
+ try {
32
+ // Get git status information using utility function
33
+ const changes = getWorkingDirectoryChanges()
34
+
35
+ // Categorize changes
36
+ const status = {
37
+ staged: [],
38
+ unstaged: [],
39
+ untracked: []
31
40
  }
32
41
 
33
- // Enhanced analysis of changes with diff content for AI analysis
34
- const enhancedChanges = await this.enhanceChangesWithDiff(rawChanges)
35
- const changesSummary = summarizeFileChanges(enhancedChanges)
36
-
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,
42
+ changes.forEach(change => {
43
+ const statusCode = change.status || '??'
44
+ if (statusCode.startsWith('??')) {
45
+ status.untracked.push(change.filePath)
46
+ } else if (statusCode[0] !== ' ' && statusCode[0] !== '?') {
47
+ status.staged.push(change.filePath)
48
+ } else {
49
+ status.unstaged.push(change.filePath)
50
+ }
43
51
  })
44
52
 
45
- const processedResult = diffProcessor.processFiles(enhancedChanges)
53
+ // Update workspace metrics
54
+ this.workspaceMetrics.unstagedFiles = status.unstaged?.length || 0
55
+ this.workspaceMetrics.stagedFiles = status.staged?.length || 0
56
+ this.workspaceMetrics.untrackedFiles = status.untracked?.length || 0
46
57
 
47
- // Generate changelog content with processed files
48
- const changelog = await this.generateChangelogContent(
49
- processedResult.processedFiles,
50
- changesSummary,
51
- processedResult,
52
- analysisMode
53
- )
58
+ // Get detailed diff for staged/unstaged changes (empty for now)
59
+ const diff = ''
60
+
61
+ // Use analysis engine if available
62
+ let analysis = null
63
+ if (this.analysisEngine) {
64
+ analysis = await this.analysisEngine.analyzeCurrentChanges()
65
+ }
54
66
 
55
67
  return {
56
- changelog,
57
- changes: enhancedChanges,
58
- processedFiles: processedResult.processedFiles,
59
- patterns: processedResult.patterns,
60
- summary: changesSummary,
61
- filesProcessed: processedResult.filesProcessed,
62
- filesSkipped: processedResult.filesSkipped,
68
+ status,
69
+ diff,
70
+ analysis,
71
+ metrics: this.workspaceMetrics,
63
72
  }
64
73
  } catch (error) {
65
- console.error(colors.errorMessage('Workspace changelog generation failed:'), error.message)
74
+ console.error(colors.errorMessage(`Failed to analyze workspace: ${error.message}`))
66
75
  throw error
67
76
  }
68
77
  }
69
78
 
70
- async generateAIChangelogContentFromChanges(changes, changesSummary, analysisMode = 'standard') {
71
- if (!this.aiAnalysisService.hasAI) {
72
- console.log(colors.infoMessage('AI not available, using rule-based analysis...'))
73
- return this.generateBasicChangelogContentFromChanges(changes, changesSummary)
74
- }
79
+ /**
80
+ * Generate changelog preview for workspace changes
81
+ * @returns {Promise<string>} Preview changelog content
82
+ */
83
+ async generateWorkspacePreview() {
84
+ console.log(colors.processingMessage('📝 Generating workspace preview...'))
75
85
 
76
- try {
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
112
- const prompt = `Generate a comprehensive AI changelog for the following working directory changes:
113
-
114
- **Analysis Mode**: ${analysisMode}
115
- **Total Files**: ${changesSummary.totalFiles} (${processedResult.filesProcessed} analyzed, ${processedResult.filesSkipped} summarized)
116
- **Categories**: ${Object.keys(changesSummary.categories).join(', ')}${patternSummary}
117
-
118
- **PROCESSED FILES:**${filesSection}
119
-
120
- CRITICAL INSTRUCTIONS FOR ANALYSIS:
121
- 1. **ONLY DESCRIBE CHANGES VISIBLE IN THE DIFF CONTENT** - Do not invent or assume changes
122
- 2. **BE FACTUAL AND PRECISE** - Only mention specific lines, functions, imports that you can see
123
- 3. **NO ASSUMPTIONS OR SPECULATION** - If you can't see it in the diff, don't mention it
124
- 4. **STICK TO OBSERVABLE FACTS** - Describe what was added, removed, or modified line by line
125
- 5. **DO NOT MAKE UP INTEGRATION DETAILS** - Don't assume files work together unless explicitly shown
126
-
127
- STRICT FORMATTING REQUIREMENTS:
128
- Generate working directory change entries based ONLY on visible diff content:
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.
133
-
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.
135
-
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.
137
-
138
- EXAMPLES of FORBIDDEN ASSUMPTIONS:
139
- ❌ "Updated other providers to recognize bedrock" (not visible in diff)
140
- ❌ "Refactored provider selection logic" (not shown in actual changes)
141
- ❌ "Improved integration across the system" (speculation)
142
- ❌ "Enhanced error handling throughout" (assumption)
143
-
144
- ONLY describe what you can literally see in the diff content. Do not invent connections or integrations.`
145
-
146
- // Make AI call with all the context
147
- const messages = [
148
- {
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.',
152
- },
153
- {
154
- role: 'user',
155
- content: prompt,
156
- },
157
- ]
158
-
159
- const options = {
160
- max_tokens:
161
- analysisMode === 'enterprise' ? 2500 : analysisMode === 'detailed' ? 2000 : 1200,
162
- temperature: 0.2,
163
- }
164
-
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
- }
86
+ const workspaceData = await this.analyzeWorkspaceChanges()
193
87
 
194
- // Add metadata
195
- const timestamp = new Date().toISOString().split('T')[0]
196
-
197
- // Ensure proper changelog format with Keep a Changelog header
198
- if (!changelog.includes('# ')) {
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}`
200
- }
88
+ if (!workspaceData.analysis || workspaceData.analysis.changes.length === 0) {
89
+ return colors.infoMessage('No significant workspace changes detected.')
90
+ }
201
91
 
202
- // Add generation metadata
203
- changelog += `\n\n---\n\n*Generated from ${changesSummary.totalFiles} working directory changes*\n`
92
+ // Generate preview using parent class methods
93
+ const previewContent = await this.generateChangelogFromAnalysis(workspaceData.analysis)
204
94
 
205
- return changelog
206
- } catch (error) {
207
- // Specific error guidance for AI failures
208
- if (error.message.includes('fetch failed') || error.message.includes('ECONNREFUSED')) {
209
- console.warn(colors.warningMessage('⚠️ Cannot connect to AI provider'))
210
- console.warn(colors.infoMessage('💡 Check internet connection and provider service status'))
211
- } else if (error.message.includes('API key') || error.message.includes('401')) {
212
- console.warn(colors.warningMessage('⚠️ API authentication failed'))
213
- console.warn(colors.infoMessage('💡 Run: ai-changelog init'))
214
- } else if (error.message.includes('rate limit')) {
215
- console.warn(colors.warningMessage('⚠️ Rate limit exceeded'))
216
- console.warn(colors.infoMessage('💡 Wait a moment before retrying'))
217
- } else {
218
- console.warn(colors.warningMessage(`⚠️ AI analysis failed: ${error.message}`))
219
- }
95
+ return previewContent
96
+ }
220
97
 
221
- console.warn(colors.infoMessage('🔄 Falling back to pattern-based analysis'))
222
- return this.generateBasicChangelogContentFromChanges(changes, changesSummary)
98
+ /**
99
+ * Get workspace statistics
100
+ * @returns {Object} Workspace statistics
101
+ */
102
+ getWorkspaceStats() {
103
+ return {
104
+ ...this.workspaceMetrics,
105
+ hasChanges: this.hasWorkspaceChanges(),
106
+ summary: this.getWorkspaceSummary(),
223
107
  }
224
108
  }
225
109
 
226
- generateBasicChangelogContentFromChanges(changes, changesSummary) {
227
- const timestamp = new Date().toISOString().split('T')[0]
228
-
229
- let changelog = `# Working Directory Changes - ${timestamp}\n\n`
230
-
231
- // Basic summary
232
- changelog += '## Summary\n'
233
- changelog += `${changes.length} files modified across ${Object.keys(changesSummary.categories).length} categories.\n\n`
110
+ /**
111
+ * Check if workspace has any changes
112
+ * @returns {boolean} True if workspace has changes
113
+ */
114
+ hasWorkspaceChanges() {
115
+ return (
116
+ this.workspaceMetrics.unstagedFiles > 0 ||
117
+ this.workspaceMetrics.stagedFiles > 0 ||
118
+ this.workspaceMetrics.untrackedFiles > 0
119
+ )
120
+ }
234
121
 
235
- // Changes by category
236
- changelog += this.buildChangesByCategory(changes, changesSummary)
122
+ /**
123
+ * Get workspace summary string
124
+ * @returns {string} Human-readable workspace summary
125
+ */
126
+ getWorkspaceSummary() {
127
+ const { unstagedFiles, stagedFiles, untrackedFiles } = this.workspaceMetrics
128
+ const parts = []
237
129
 
238
- // Basic recommendations
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'
130
+ if (stagedFiles > 0) {
131
+ parts.push(`${stagedFiles} staged`)
132
+ }
133
+ if (unstagedFiles > 0) {
134
+ parts.push(`${unstagedFiles} unstaged`)
135
+ }
136
+ if (untrackedFiles > 0) {
137
+ parts.push(`${untrackedFiles} untracked`)
138
+ }
243
139
 
244
- return changelog
140
+ return parts.length > 0 ? parts.join(', ') : 'No changes'
245
141
  }
246
142
 
247
- async enhanceChangesWithDiff(changes) {
248
- const enhancedChanges = []
249
-
250
- for (const change of changes) {
251
- const enhancedChange = {
252
- ...change,
253
- category: categorizeFile(change.path || change.filePath),
254
- language: detectLanguage(change.path || change.filePath),
255
- importance: assessFileImportance(change.path || change.filePath, change.status),
256
- enhanced: true,
143
+ /**
144
+ * Validate workspace state before operations
145
+ * @returns {Promise<boolean>} True if workspace is valid
146
+ */
147
+ async validateWorkspace() {
148
+ try {
149
+ // Check if we're in a git repository
150
+ let isGitRepo = false
151
+ try {
152
+ const { execSync } = await import('node:child_process')
153
+ execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' })
154
+ isGitRepo = true
155
+ } catch {
156
+ isGitRepo = false
257
157
  }
258
158
 
259
- // Get diff content if git service is available
260
- if (this.gitService) {
261
- try {
262
- const diffAnalysis = await this.gitService.analyzeWorkingDirectoryFileChange(
263
- change.status,
264
- change.path || change.filePath
265
- )
266
-
267
- if (diffAnalysis) {
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
274
- }
275
- } catch (error) {
276
- console.warn(`Failed to get diff for ${change.path || change.filePath}:`, error.message)
277
- }
159
+ if (!isGitRepo) {
160
+ console.error(colors.errorMessage('Not in a git repository'))
161
+ return false
278
162
  }
279
163
 
280
- enhancedChanges.push(enhancedChange)
281
- }
164
+ const changes = getWorkingDirectoryChanges()
282
165
 
283
- return enhancedChanges
284
- }
166
+ // Check if workspace has changes
167
+ if (!this.hasWorkspaceChanges() && changes.length === 0) {
168
+ console.log(colors.infoMessage('No workspace changes detected'))
169
+ return false
170
+ }
285
171
 
286
- generateWorkspaceContext(changes, summary) {
287
- const context = {
288
- totalFiles: changes.length,
289
- categories: Object.keys(summary.categories),
290
- primaryCategory: this.getPrimaryCategory(summary.categories),
291
- riskLevel: this.assessWorkspaceRisk(changes),
292
- complexity: this.assessWorkspaceComplexity(changes),
293
- recommendations: this.generateRecommendations(changes),
172
+ return true
173
+ } catch (error) {
174
+ console.error(colors.errorMessage(`Workspace validation failed: ${error.message}`))
175
+ return false
294
176
  }
295
-
296
- return context
297
177
  }
298
178
 
299
- getPrimaryCategory(categories) {
300
- return Object.entries(categories).sort(([, a], [, b]) => b.length - a.length)[0]?.[0] || 'other'
301
- }
179
+ /**
180
+ * Generate comprehensive workspace changelog
181
+ * @returns {Promise<string>} Comprehensive changelog content
182
+ */
183
+ async generateComprehensiveWorkspaceChangelog() {
184
+ console.log(colors.processingMessage('📋 Generating comprehensive workspace changelog...'))
302
185
 
303
- assessWorkspaceRisk(changes) {
304
- const highRiskFiles = changes.filter(
305
- (change) =>
306
- change.importance === 'critical' ||
307
- change.category === 'configuration' ||
308
- change.status === 'D'
309
- )
186
+ try {
187
+ const workspaceData = await this.analyzeWorkspaceChanges()
310
188
 
311
- if (highRiskFiles.length > changes.length * 0.3) {
312
- return 'high'
313
- }
314
- if (highRiskFiles.length > 0) {
315
- return 'medium'
316
- }
317
- return 'low'
318
- }
189
+ if (!workspaceData.analysis) {
190
+ return this.generateBasicWorkspaceChangelog(workspaceData)
191
+ }
319
192
 
320
- assessWorkspaceComplexity(changes) {
321
- if (changes.length > 20) {
322
- return 'high'
323
- }
324
- if (changes.length > 5) {
325
- return 'medium'
193
+ return await this.generateAIChangelogContentFromChanges(workspaceData.analysis.changes)
194
+ } catch (error) {
195
+ console.error(
196
+ colors.errorMessage(`Failed to generate comprehensive changelog: ${error.message}`)
197
+ )
198
+ throw error
326
199
  }
327
- return 'low'
328
200
  }
329
201
 
330
- generateRecommendations(changes) {
331
- const recommendations = []
332
-
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')
337
-
338
- if (hasSource && !hasTests) {
339
- recommendations.push('Consider adding tests for source code changes')
202
+ /**
203
+ * Generate AI-powered changelog content from changes
204
+ * @param {Array} changes - Array of change objects
205
+ * @returns {Promise<string>} Generated changelog content
206
+ */
207
+ async generateAIChangelogContentFromChanges(changes) {
208
+ if (!changes || changes.length === 0) {
209
+ return colors.infoMessage('No changes to process for changelog.')
340
210
  }
341
211
 
342
- if (hasConfig) {
343
- recommendations.push('Review configuration changes carefully')
344
- }
212
+ try {
213
+ // Use AI analysis service if available
214
+ if (this.aiAnalysisService && this.aiAnalysisService.hasAI) {
215
+ const enhancedChanges = await this.enhanceChangesWithDiff(changes)
216
+ return await this.generateChangelogContent(enhancedChanges)
217
+ }
345
218
 
346
- if (hasSource && !hasDocs) {
347
- recommendations.push('Update documentation for new features')
219
+ // Fallback to rule-based generation
220
+ return this.generateRuleBasedChangelog(changes)
221
+ } catch (error) {
222
+ console.error(colors.errorMessage(`AI changelog generation failed: ${error.message}`))
223
+ return this.generateRuleBasedChangelog(changes)
348
224
  }
225
+ }
349
226
 
350
- if (changes.length > 15) {
351
- recommendations.push('Consider breaking this into smaller commits')
352
- }
227
+ /**
228
+ * Enhance changes with diff information
229
+ * @param {Array} changes - Array of change objects
230
+ * @returns {Promise<Array>} Enhanced changes with diff data
231
+ */
232
+ async enhanceChangesWithDiff(changes) {
233
+ const enhancedChanges = []
353
234
 
354
- const deletedFiles = changes.filter((change) => change.status === 'D')
355
- if (deletedFiles.length > 0) {
356
- recommendations.push(`Review ${deletedFiles.length} deleted files before committing`)
235
+ for (const change of changes) {
236
+ try {
237
+ // For workspace changelog service, we don't have detailed diffs
238
+ // Just add basic enhancement
239
+ enhancedChanges.push({
240
+ ...change,
241
+ diff: '',
242
+ complexity: this.assessChangeComplexity(''),
243
+ impact: this.assessChangeImpact(change.file, ''),
244
+ })
245
+ } catch (error) {
246
+ console.warn(
247
+ colors.warningMessage(`Failed to enhance change for ${change.file}: ${error.message}`)
248
+ )
249
+ enhancedChanges.push(change)
250
+ }
357
251
  }
358
252
 
359
- return recommendations
253
+ return enhancedChanges
360
254
  }
361
255
 
362
- buildChangesByCategory(_changes, changesSummary) {
363
- let content = '## Changes by Category\n\n'
256
+ /**
257
+ * Generate changelog content from enhanced changes
258
+ * @param {Array} enhancedChanges - Enhanced change objects
259
+ * @returns {Promise<string>} Generated changelog content
260
+ */
261
+ async generateChangelogContent(enhancedChanges) {
262
+ const sections = {
263
+ features: [],
264
+ fixes: [],
265
+ improvements: [],
266
+ docs: [],
267
+ tests: [],
268
+ chores: [],
269
+ }
364
270
 
365
- Object.entries(changesSummary.categories).forEach(([category, files]) => {
366
- const categoryIcon = this.getCategoryIcon(category)
367
- content += `### ${categoryIcon} ${category.charAt(0).toUpperCase() + category.slice(1)} (${files.length} files)\n\n`
271
+ // Categorize changes
272
+ for (const change of enhancedChanges) {
273
+ const category = this.categorizeChange(change)
274
+ if (sections[category]) {
275
+ sections[category].push(change)
276
+ }
277
+ }
368
278
 
369
- files.forEach((file) => {
370
- const statusIcon = this.getStatusIcon(file.status)
371
- content += `- ${statusIcon} ${file.path}\n`
372
- })
279
+ // Generate markdown content
280
+ let content = '# Workspace Changes\n\n'
373
281
 
374
- content += '\n'
375
- })
282
+ for (const [section, changes] of Object.entries(sections)) {
283
+ if (changes.length > 0) {
284
+ content += `## ${this.formatSectionTitle(section)}\n\n`
376
285
 
377
- return content
378
- }
286
+ for (const change of changes) {
287
+ content += `- ${this.formatChangeEntry(change)}\n`
288
+ }
379
289
 
380
- getCategoryIcon(category) {
381
- const icons = {
382
- source: '💻',
383
- tests: '🧪',
384
- documentation: '📚',
385
- configuration: '⚙️',
386
- frontend: '🎨',
387
- assets: '🖼️',
388
- build: '🔧',
389
- other: '📄',
290
+ content += '\n'
291
+ }
390
292
  }
391
- return icons[category] || '📄'
392
- }
393
293
 
394
- getStatusIcon(status) {
395
- const icons = {
396
- A: '➕', // Added
397
- M: '✏️', // Modified
398
- D: '❌', // Deleted
399
- R: '📝', // Renamed
400
- C: '📋', // Copied
401
- }
402
- return icons[status] || '📄'
294
+ return content
403
295
  }
404
296
 
405
- async generateChangelogContent(changes, summary, _context, analysisMode) {
406
- if (analysisMode === 'detailed' || analysisMode === 'enterprise') {
407
- return await this.generateAIChangelogContentFromChanges(changes, summary, analysisMode)
408
- }
409
- return this.generateBasicChangelogContentFromChanges(changes, summary)
410
- }
297
+ /**
298
+ * Generate commit-style working directory entries
299
+ * @returns {Promise<Array>} Array of commit-style entries
300
+ */
301
+ async generateCommitStyleWorkingDirectoryEntries() {
302
+ const workspaceData = await this.analyzeWorkspaceChanges()
303
+ const entries = []
304
+
305
+ if (workspaceData.status) {
306
+ // Process staged files
307
+ if (workspaceData.status.staged) {
308
+ for (const file of workspaceData.status.staged) {
309
+ entries.push({
310
+ type: 'staged',
311
+ file,
312
+ message: `Add: ${file}`,
313
+ timestamp: new Date().toISOString(),
314
+ })
315
+ }
316
+ }
411
317
 
412
- // Integration with main changelog service
413
- async generateCommitStyleWorkingDirectoryEntries(options = {}) {
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
- }
318
+ // Process unstaged files
319
+ if (workspaceData.status.unstaged) {
320
+ for (const file of workspaceData.status.unstaged) {
321
+ entries.push({
322
+ type: 'unstaged',
323
+ file,
324
+ message: `Modify: ${file}`,
325
+ timestamp: new Date().toISOString(),
326
+ })
327
+ }
328
+ }
421
329
 
422
- try {
423
- if (!(rawChanges && Array.isArray(rawChanges)) || rawChanges.length === 0) {
424
- return { entries: [] }
330
+ // Process untracked files
331
+ if (workspaceData.status.untracked) {
332
+ for (const file of workspaceData.status.untracked) {
333
+ entries.push({
334
+ type: 'untracked',
335
+ file,
336
+ message: `Create: ${file}`,
337
+ timestamp: new Date().toISOString(),
338
+ })
339
+ }
425
340
  }
341
+ }
426
342
 
427
- // Enhanced analysis of changes with diff content for AI analysis
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
- })
343
+ return entries
344
+ }
439
345
 
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
- }
346
+ /**
347
+ * Generate workspace changelog
348
+ * @returns {Promise<string>} Workspace changelog content
349
+ */
350
+ async generateWorkspaceChangelog() {
351
+ console.log(colors.processingMessage('📝 Generating workspace changelog...'))
457
352
 
458
- const compressionInfo = file.compressionApplied
459
- ? ` [compressed from ${file.originalSize || 'unknown'} chars]`
460
- : ''
461
- const patternInfo = file.bulkPattern ? ` [${file.bulkPattern}]` : ''
353
+ try {
354
+ const entries = await this.generateCommitStyleWorkingDirectoryEntries()
462
355
 
463
- return `\n**${file.filePath || file.path}** (${file.status})${compressionInfo}${patternInfo}:\n${file.diff}`
464
- })
465
- .join('\n')
466
-
467
- // Build prompt for commit-style entries
468
- const prompt = `Generate working directory change entries in the SAME FORMAT as git commits:
469
-
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}
475
-
476
- STRICT FORMATTING REQUIREMENTS:
477
- Generate working directory change entries based ONLY on visible diff content:
478
- - (type) Detailed but focused description - Include key functional changes, method/function names, and important technical details without overwhelming verbosity
479
-
480
- Where:
481
- - type = feature, fix, refactor, docs, chore, etc. based on the actual changes
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.
487
-
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.
489
-
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.
491
-
492
- FORBIDDEN - DO NOT MAKE ASSUMPTIONS:
493
- ❌ Do not mention "integration" unless you see actual integration code
494
- ❌ Do not mention "provider selection logic" unless you see that specific code
495
- ❌ Do not assume files work together unless explicitly shown in diffs
496
-
497
- Generate one entry per file or logical change group. Only describe what you can literally see.`
498
-
499
- // Make AI call
500
- const messages = [
501
- {
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.',
505
- },
506
- {
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
356
+ if (entries.length === 0) {
357
+ return colors.infoMessage('No workspace changes to include in changelog.')
518
358
  }
519
359
 
520
- // Increase token limit for large numbers of working directory changes
521
- if (enhancedChanges.length > 50) {
522
- maxTokens = Math.min(maxTokens + 1500, 6000)
523
- }
360
+ let content = '# Workspace Changes\n\n'
361
+ content += `Generated on: ${new Date().toLocaleString()}\n\n`
524
362
 
525
- const options_ai = {
526
- max_tokens: maxTokens,
527
- temperature: 0.3,
528
- }
363
+ const groupedEntries = this.groupEntriesByType(entries)
529
364
 
530
- const response = await this.aiAnalysisService.aiProvider.generateCompletion(
531
- messages,
532
- options_ai
533
- )
365
+ for (const [type, typeEntries] of Object.entries(groupedEntries)) {
366
+ if (typeEntries.length > 0) {
367
+ content += `## ${this.formatEntryType(type)} (${typeEntries.length})\n\n`
534
368
 
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
- })
369
+ for (const entry of typeEntries) {
370
+ content += `- ${entry.message}\n`
371
+ }
565
372
 
566
- return { entries: basicEntries }
373
+ content += '\n'
374
+ }
567
375
  }
568
376
 
569
- // Parse entries from response
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
- })
582
- return {
583
- entries,
584
- changes: enhancedChanges,
585
- summary: changesSummary,
586
- }
377
+ return content
587
378
  } catch (error) {
588
- // Provide specific guidance based on error type
589
- if (error.message.includes('fetch failed') || error.message.includes('connection')) {
590
- console.warn(colors.warningMessage('⚠️ AI provider connection failed'))
591
- console.warn(
592
- colors.infoMessage('💡 Check your internet connection and provider configuration')
593
- )
594
- } else if (error.message.includes('API key') || error.message.includes('401')) {
595
- console.warn(colors.warningMessage('⚠️ Authentication failed'))
596
- console.warn(colors.infoMessage('💡 Run `ai-changelog init` to configure your API key'))
597
- } else {
598
- console.warn(colors.warningMessage(`⚠️ AI analysis failed: ${error.message}`))
599
- console.warn(colors.infoMessage('💡 Using basic file change detection instead'))
600
- }
601
-
602
- // Return basic entries from the changes we were given instead of getting fresh ones
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 }
379
+ console.error(colors.errorMessage(`Failed to generate workspace changelog: ${error.message}`))
380
+ throw error
627
381
  }
628
382
  }
629
383
 
630
- async generateWorkspaceChangelog(version = null, options = {}) {
631
- const result = await this.generateComprehensiveWorkspaceChangelog(options)
384
+ // Helper methods
632
385
 
633
- if (!result) {
634
- return null
386
+ /**
387
+ * Generate basic workspace changelog without AI
388
+ */
389
+ generateBasicWorkspaceChangelog(workspaceData) {
390
+ let content = '# Workspace Changes\n\n'
391
+
392
+ if (workspaceData.metrics) {
393
+ content += '## Summary\n\n'
394
+ content += `- Staged files: ${workspaceData.metrics.stagedFiles}\n`
395
+ content += `- Unstaged files: ${workspaceData.metrics.unstagedFiles}\n`
396
+ content += `- Untracked files: ${workspaceData.metrics.untrackedFiles}\n\n`
635
397
  }
636
398
 
637
- let changelog = result.changelog
399
+ return content
400
+ }
638
401
 
639
- // Add version information if provided
640
- if (version) {
641
- changelog = changelog.replace(
642
- /# Working Directory Changelog/,
643
- `# Working Directory Changelog - Version ${version}`
644
- )
645
- }
402
+ /**
403
+ * Generate rule-based changelog fallback
404
+ */
405
+ generateRuleBasedChangelog(changes) {
406
+ let content = '# Changes Summary\n\n'
646
407
 
647
- // Add context information for detailed modes
648
- if (options.analysisMode === 'detailed' || options.analysisMode === 'enterprise') {
649
- changelog += this.generateContextSection(result.context)
408
+ for (const change of changes) {
409
+ content += `- ${change.type || 'Modified'}: ${change.file}\n`
650
410
  }
651
411
 
652
- return {
653
- ...result,
654
- changelog,
655
- version,
656
- }
412
+ return content
657
413
  }
658
414
 
659
- generateContextSection(context) {
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`
665
-
666
- if (context.recommendations.length > 0) {
667
- section += '### Recommendations\n\n'
668
- context.recommendations.forEach((rec) => {
669
- section += `- ${rec}\n`
670
- })
671
- section += '\n'
672
- }
415
+ /**
416
+ * Assess change complexity
417
+ */
418
+ assessChangeComplexity(diff) {
419
+ if (!diff) return 'low'
673
420
 
674
- return section
421
+ const lines = diff.split('\n').length
422
+ if (lines > 100) return 'high'
423
+ if (lines > 20) return 'medium'
424
+ return 'low'
675
425
  }
676
426
 
677
- // Missing methods expected by tests
678
- analyzeWorkspaceStructure() {
679
- return {
680
- structure: 'standard',
681
- directories: [],
682
- files: []
683
- }
427
+ /**
428
+ * Assess change impact
429
+ */
430
+ assessChangeImpact(file, diff) {
431
+ const criticalFiles = ['package.json', 'README.md', 'Dockerfile', '.env']
432
+ const isCritical = criticalFiles.some((critical) => file.includes(critical))
433
+
434
+ if (isCritical) return 'high'
435
+ if (file.includes('test') || file.includes('spec')) return 'low'
436
+ return 'medium'
684
437
  }
685
438
 
686
- identifyWorkspacePatterns() {
687
- return {
688
- patterns: ['monorepo', 'standard'],
689
- confidence: 'medium'
690
- }
439
+ /**
440
+ * Categorize a change
441
+ */
442
+ categorizeChange(change) {
443
+ const file = change.file.toLowerCase()
444
+
445
+ if (file.includes('test') || file.includes('spec')) return 'tests'
446
+ if (file.includes('readme') || file.includes('doc')) return 'docs'
447
+ if (file.includes('fix') || change.type === 'fix') return 'fixes'
448
+ if (file.includes('feat') || change.type === 'feature') return 'features'
449
+ if (file.includes('config') || file.includes('package')) return 'chores'
450
+
451
+ return 'improvements'
691
452
  }
692
453
 
693
- assessWorkspaceHealth() {
694
- return {
695
- health: 'good',
696
- issues: [],
697
- score: 85
698
- }
454
+ /**
455
+ * Format section title
456
+ */
457
+ formatSectionTitle(section) {
458
+ const titles = {
459
+ features: 'Features',
460
+ fixes: 'Bug Fixes',
461
+ improvements: 'Improvements',
462
+ docs: 'Documentation',
463
+ tests: 'Tests',
464
+ chores: 'Maintenance',
465
+ }
466
+ return titles[section] || section
699
467
  }
700
468
 
701
- generateWorkspaceInsights() {
702
- return {
703
- insights: [],
704
- recommendations: []
705
- }
469
+ /**
470
+ * Format change entry
471
+ */
472
+ formatChangeEntry(change) {
473
+ const impact = change.impact ? `[${change.impact}]` : ''
474
+ const complexity = change.complexity ? `{${change.complexity}}` : ''
475
+ return `${change.file} ${impact} ${complexity}`.trim()
706
476
  }
707
477
 
708
- optimizeWorkspaceConfiguration() {
709
- return {
710
- optimizations: [],
711
- impact: 'low'
712
- }
478
+ /**
479
+ * Group entries by type
480
+ */
481
+ groupEntriesByType(entries) {
482
+ return entries.reduce((groups, entry) => {
483
+ const type = entry.type || 'unknown'
484
+ if (!groups[type]) groups[type] = []
485
+ groups[type].push(entry)
486
+ return groups
487
+ }, {})
713
488
  }
714
489
 
715
- validateWorkspaceStandards() {
716
- return {
717
- compliant: true,
718
- violations: []
490
+ /**
491
+ * Format entry type for display
492
+ */
493
+ formatEntryType(type) {
494
+ const types = {
495
+ staged: 'Staged Changes',
496
+ unstaged: 'Unstaged Changes',
497
+ untracked: 'New Files',
719
498
  }
499
+ return types[type] || type
720
500
  }
721
501
 
722
- compareWorkspaceConfigurations(a, b) {
723
- return {
724
- similarity: 90,
725
- differences: []
502
+ /**
503
+ * Initialize workspace for analysis
504
+ * @returns {Promise<boolean>} True if initialization successful
505
+ */
506
+ async initializeWorkspace() {
507
+ try {
508
+ console.log(colors.processingMessage('🔧 Initializing workspace...'))
509
+
510
+ // Reset metrics
511
+ this.cleanup()
512
+
513
+ // Validate git repository
514
+ const isValid = await this.validateWorkspace()
515
+ if (!isValid) {
516
+ return false
517
+ }
518
+
519
+ // Perform initial analysis
520
+ await this.analyzeWorkspaceChanges()
521
+
522
+ console.log(colors.successMessage('✅ Workspace initialized successfully'))
523
+ return true
524
+ } catch (error) {
525
+ console.error(colors.errorMessage(`Failed to initialize workspace: ${error.message}`))
526
+ return false
726
527
  }
727
528
  }
728
529
 
729
- extractWorkspaceMetadata() {
730
- return {
731
- type: 'standard',
732
- tools: [],
733
- frameworks: []
530
+ /**
531
+ * Validate workspace structure
532
+ * @returns {boolean} True if workspace structure is valid
533
+ */
534
+ validateWorkspaceStructure() {
535
+ try {
536
+ // Check if we have the required services
537
+ if (!this.gitService) {
538
+ console.error(colors.errorMessage('Git service not available'))
539
+ return false
540
+ }
541
+
542
+ // Check if workspace has any changes to analyze
543
+ if (!this.hasWorkspaceChanges()) {
544
+ console.log(colors.infoMessage('No workspace changes detected'))
545
+ return true // Still valid, just nothing to do
546
+ }
547
+
548
+ return true
549
+ } catch (error) {
550
+ console.error(colors.errorMessage(`Workspace structure validation failed: ${error.message}`))
551
+ return false
734
552
  }
735
553
  }
736
554
 
737
- suggestWorkspaceImprovements() {
738
- return {
739
- improvements: [],
740
- priority: 'low'
555
+ /**
556
+ * Clean up workspace analysis resources
557
+ */
558
+ cleanup() {
559
+ this.workspaceMetrics = {
560
+ unstagedFiles: 0,
561
+ stagedFiles: 0,
562
+ untrackedFiles: 0,
563
+ modifiedLines: 0,
741
564
  }
742
565
  }
743
566
  }