@entro314labs/ai-changelog-generator 3.2.1 → 3.3.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 (32) hide show
  1. package/ai-changelog-mcp.sh +0 -0
  2. package/ai-changelog.sh +0 -0
  3. package/bin/ai-changelog-dxt.js +0 -0
  4. package/package.json +72 -80
  5. package/src/ai-changelog-generator.js +5 -4
  6. package/src/application/orchestrators/changelog.orchestrator.js +12 -202
  7. package/src/cli.js +4 -5
  8. package/src/domains/ai/ai-analysis.service.js +2 -0
  9. package/src/domains/analysis/analysis.engine.js +714 -37
  10. package/src/domains/changelog/changelog.service.js +615 -30
  11. package/src/domains/changelog/workspace-changelog.service.js +418 -627
  12. package/src/domains/git/commit-tagger.js +552 -0
  13. package/src/domains/git/git-manager.js +357 -0
  14. package/src/domains/git/git.service.js +865 -16
  15. package/src/infrastructure/cli/cli.controller.js +14 -9
  16. package/src/infrastructure/config/configuration.manager.js +24 -2
  17. package/src/infrastructure/interactive/interactive-workflow.service.js +8 -1
  18. package/src/infrastructure/mcp/mcp-server.service.js +35 -11
  19. package/src/infrastructure/providers/core/base-provider.js +1 -1
  20. package/src/infrastructure/providers/implementations/anthropic.js +16 -173
  21. package/src/infrastructure/providers/implementations/azure.js +16 -63
  22. package/src/infrastructure/providers/implementations/dummy.js +13 -16
  23. package/src/infrastructure/providers/implementations/mock.js +13 -26
  24. package/src/infrastructure/providers/implementations/ollama.js +12 -4
  25. package/src/infrastructure/providers/implementations/openai.js +13 -165
  26. package/src/infrastructure/providers/provider-management.service.js +126 -412
  27. package/src/infrastructure/providers/utils/base-provider-helpers.js +11 -0
  28. package/src/shared/utils/cli-ui.js +1 -1
  29. package/src/shared/utils/diff-processor.js +21 -19
  30. package/src/shared/utils/error-classes.js +33 -0
  31. package/src/shared/utils/utils.js +65 -60
  32. package/src/domains/git/git-repository.analyzer.js +0 -678
@@ -1,743 +1,534 @@
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 { ChangelogService } from './changelog.service.js'
11
3
 
12
4
  /**
13
- * Workspace Changelog Service
14
- *
15
- * Handles changelog generation from working directory changes
5
+ * WorkspaceChangelogService extends ChangelogService with workspace-specific functionality
6
+ * for analyzing uncommitted changes and workspace state
16
7
  */
17
- export class WorkspaceChangelogService {
18
- constructor(aiAnalysisService, gitService = null) {
19
- this.aiAnalysisService = aiAnalysisService
20
- this.gitService = gitService
8
+ export class WorkspaceChangelogService extends ChangelogService {
9
+ constructor(gitService, aiAnalysisService, analysisEngine = null, configManager = null) {
10
+ super(gitService, aiAnalysisService, analysisEngine, configManager)
11
+ this.workspaceMetrics = {
12
+ unstagedFiles: 0,
13
+ stagedFiles: 0,
14
+ untrackedFiles: 0,
15
+ modifiedLines: 0,
16
+ }
21
17
  }
22
18
 
23
- async generateComprehensiveWorkspaceChangelog(options = {}) {
19
+ /**
20
+ * Analyze workspace changes without committing
21
+ * @returns {Promise<Object>} Workspace analysis results
22
+ */
23
+ async analyzeWorkspaceChanges() {
24
+ console.log(colors.processingMessage('🔍 Analyzing workspace changes...'))
25
+
24
26
  try {
25
- // Get working directory changes as raw array
26
- const rawChanges = getWorkingDirectoryChanges()
27
+ // Get git status information
28
+ const status = await this.gitService.getStatus()
27
29
 
28
- if (!(rawChanges && Array.isArray(rawChanges)) || rawChanges.length === 0) {
29
- EnhancedConsole.info('No changes detected in working directory.')
30
- return null
31
- }
30
+ // Update workspace metrics
31
+ this.workspaceMetrics.unstagedFiles = status.unstaged?.length || 0
32
+ this.workspaceMetrics.stagedFiles = status.staged?.length || 0
33
+ this.workspaceMetrics.untrackedFiles = status.untracked?.length || 0
32
34
 
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,
43
- })
44
-
45
- const processedResult = diffProcessor.processFiles(enhancedChanges)
46
-
47
- // Generate changelog content with processed files
48
- const changelog = await this.generateChangelogContent(
49
- processedResult.processedFiles,
50
- changesSummary,
51
- processedResult,
52
- analysisMode
53
- )
35
+ // Get detailed diff for staged/unstaged changes
36
+ const diff = await this.gitService.getDiff()
37
+
38
+ // Use analysis engine if available
39
+ let analysis = null
40
+ if (this.analysisEngine) {
41
+ analysis = await this.analysisEngine.analyzeCurrentChanges()
42
+ }
54
43
 
55
44
  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,
45
+ status,
46
+ diff,
47
+ analysis,
48
+ metrics: this.workspaceMetrics,
63
49
  }
64
50
  } catch (error) {
65
- console.error(colors.errorMessage('Workspace changelog generation failed:'), error.message)
51
+ console.error(colors.errorMessage(`Failed to analyze workspace: ${error.message}`))
66
52
  throw error
67
53
  }
68
54
  }
69
55
 
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
- }
75
-
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
- }
56
+ /**
57
+ * Generate changelog preview for workspace changes
58
+ * @returns {Promise<string>} Preview changelog content
59
+ */
60
+ async generateWorkspacePreview() {
61
+ console.log(colors.processingMessage('📝 Generating workspace preview...'))
193
62
 
194
- // Add metadata
195
- const timestamp = new Date().toISOString().split('T')[0]
63
+ const workspaceData = await this.analyzeWorkspaceChanges()
196
64
 
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
- }
65
+ if (!workspaceData.analysis || workspaceData.analysis.changes.length === 0) {
66
+ return colors.infoMessage('No significant workspace changes detected.')
67
+ }
201
68
 
202
- // Add generation metadata
203
- changelog += `\n\n---\n\n*Generated from ${changesSummary.totalFiles} working directory changes*\n`
69
+ // Generate preview using parent class methods
70
+ const previewContent = await this.generateChangelogFromAnalysis(workspaceData.analysis)
204
71
 
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
- }
72
+ return previewContent
73
+ }
220
74
 
221
- console.warn(colors.infoMessage('🔄 Falling back to pattern-based analysis'))
222
- return this.generateBasicChangelogContentFromChanges(changes, changesSummary)
75
+ /**
76
+ * Get workspace statistics
77
+ * @returns {Object} Workspace statistics
78
+ */
79
+ getWorkspaceStats() {
80
+ return {
81
+ ...this.workspaceMetrics,
82
+ hasChanges: this.hasWorkspaceChanges(),
83
+ summary: this.getWorkspaceSummary(),
223
84
  }
224
85
  }
225
86
 
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`
87
+ /**
88
+ * Check if workspace has any changes
89
+ * @returns {boolean} True if workspace has changes
90
+ */
91
+ hasWorkspaceChanges() {
92
+ return (
93
+ this.workspaceMetrics.unstagedFiles > 0 ||
94
+ this.workspaceMetrics.stagedFiles > 0 ||
95
+ this.workspaceMetrics.untrackedFiles > 0
96
+ )
97
+ }
234
98
 
235
- // Changes by category
236
- changelog += this.buildChangesByCategory(changes, changesSummary)
99
+ /**
100
+ * Get workspace summary string
101
+ * @returns {string} Human-readable workspace summary
102
+ */
103
+ getWorkspaceSummary() {
104
+ const { unstagedFiles, stagedFiles, untrackedFiles } = this.workspaceMetrics
105
+ const parts = []
237
106
 
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'
107
+ if (stagedFiles > 0) {
108
+ parts.push(`${stagedFiles} staged`)
109
+ }
110
+ if (unstagedFiles > 0) {
111
+ parts.push(`${unstagedFiles} unstaged`)
112
+ }
113
+ if (untrackedFiles > 0) {
114
+ parts.push(`${untrackedFiles} untracked`)
115
+ }
243
116
 
244
- return changelog
117
+ return parts.length > 0 ? parts.join(', ') : 'No changes'
245
118
  }
246
119
 
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,
120
+ /**
121
+ * Validate workspace state before operations
122
+ * @returns {Promise<boolean>} True if workspace is valid
123
+ */
124
+ async validateWorkspace() {
125
+ try {
126
+ // Check if we're in a git repository
127
+ const isGitRepo = await this.gitService.isGitRepository()
128
+ if (!isGitRepo) {
129
+ console.error(colors.errorMessage('Not in a git repository'))
130
+ return false
257
131
  }
258
132
 
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
- }
133
+ // Check if workspace has changes
134
+ if (!this.hasWorkspaceChanges()) {
135
+ console.log(colors.infoMessage('No workspace changes detected'))
136
+ return false
278
137
  }
279
138
 
280
- enhancedChanges.push(enhancedChange)
139
+ return true
140
+ } catch (error) {
141
+ console.error(colors.errorMessage(`Workspace validation failed: ${error.message}`))
142
+ return false
281
143
  }
282
-
283
- return enhancedChanges
284
144
  }
285
145
 
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),
294
- }
295
-
296
- return context
297
- }
146
+ /**
147
+ * Generate comprehensive workspace changelog
148
+ * @returns {Promise<string>} Comprehensive changelog content
149
+ */
150
+ async generateComprehensiveWorkspaceChangelog() {
151
+ console.log(colors.processingMessage('📋 Generating comprehensive workspace changelog...'))
298
152
 
299
- getPrimaryCategory(categories) {
300
- return Object.entries(categories).sort(([, a], [, b]) => b.length - a.length)[0]?.[0] || 'other'
301
- }
153
+ try {
154
+ const workspaceData = await this.analyzeWorkspaceChanges()
302
155
 
303
- assessWorkspaceRisk(changes) {
304
- const highRiskFiles = changes.filter(
305
- (change) =>
306
- change.importance === 'critical' ||
307
- change.category === 'configuration' ||
308
- change.status === 'D'
309
- )
156
+ if (!workspaceData.analysis) {
157
+ return this.generateBasicWorkspaceChangelog(workspaceData)
158
+ }
310
159
 
311
- if (highRiskFiles.length > changes.length * 0.3) {
312
- return 'high'
313
- }
314
- if (highRiskFiles.length > 0) {
315
- return 'medium'
160
+ return await this.generateAIChangelogContentFromChanges(workspaceData.analysis.changes)
161
+ } catch (error) {
162
+ console.error(
163
+ colors.errorMessage(`Failed to generate comprehensive changelog: ${error.message}`)
164
+ )
165
+ throw error
316
166
  }
317
- return 'low'
318
167
  }
319
168
 
320
- assessWorkspaceComplexity(changes) {
321
- if (changes.length > 20) {
322
- return 'high'
323
- }
324
- if (changes.length > 5) {
325
- return 'medium'
169
+ /**
170
+ * Generate AI-powered changelog content from changes
171
+ * @param {Array} changes - Array of change objects
172
+ * @returns {Promise<string>} Generated changelog content
173
+ */
174
+ async generateAIChangelogContentFromChanges(changes) {
175
+ if (!changes || changes.length === 0) {
176
+ return colors.infoMessage('No changes to process for changelog.')
326
177
  }
327
- return 'low'
328
- }
329
178
 
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')
179
+ try {
180
+ // Use AI analysis service if available
181
+ if (this.aiAnalysisService && this.aiAnalysisService.hasAI) {
182
+ const enhancedChanges = await this.enhanceChangesWithDiff(changes)
183
+ return await this.generateChangelogContent(enhancedChanges)
184
+ }
337
185
 
338
- if (hasSource && !hasTests) {
339
- recommendations.push('Consider adding tests for source code changes')
186
+ // Fallback to rule-based generation
187
+ return this.generateRuleBasedChangelog(changes)
188
+ } catch (error) {
189
+ console.error(colors.errorMessage(`AI changelog generation failed: ${error.message}`))
190
+ return this.generateRuleBasedChangelog(changes)
340
191
  }
192
+ }
341
193
 
342
- if (hasConfig) {
343
- recommendations.push('Review configuration changes carefully')
344
- }
194
+ /**
195
+ * Enhance changes with diff information
196
+ * @param {Array} changes - Array of change objects
197
+ * @returns {Promise<Array>} Enhanced changes with diff data
198
+ */
199
+ async enhanceChangesWithDiff(changes) {
200
+ const enhancedChanges = []
345
201
 
346
- if (hasSource && !hasDocs) {
347
- recommendations.push('Update documentation for new features')
202
+ for (const change of changes) {
203
+ try {
204
+ // Get detailed diff for the file
205
+ const diff = await this.gitService.getFileDiff(change.file)
206
+
207
+ enhancedChanges.push({
208
+ ...change,
209
+ diff,
210
+ complexity: this.assessChangeComplexity(diff),
211
+ impact: this.assessChangeImpact(change.file, diff),
212
+ })
213
+ } catch (error) {
214
+ console.warn(
215
+ colors.warningMessage(`Failed to enhance change for ${change.file}: ${error.message}`)
216
+ )
217
+ enhancedChanges.push(change)
218
+ }
348
219
  }
349
220
 
350
- if (changes.length > 15) {
351
- recommendations.push('Consider breaking this into smaller commits')
352
- }
221
+ return enhancedChanges
222
+ }
353
223
 
354
- const deletedFiles = changes.filter((change) => change.status === 'D')
355
- if (deletedFiles.length > 0) {
356
- recommendations.push(`Review ${deletedFiles.length} deleted files before committing`)
224
+ /**
225
+ * Generate changelog content from enhanced changes
226
+ * @param {Array} enhancedChanges - Enhanced change objects
227
+ * @returns {Promise<string>} Generated changelog content
228
+ */
229
+ async generateChangelogContent(enhancedChanges) {
230
+ const sections = {
231
+ features: [],
232
+ fixes: [],
233
+ improvements: [],
234
+ docs: [],
235
+ tests: [],
236
+ chores: [],
357
237
  }
358
238
 
359
- return recommendations
360
- }
239
+ // Categorize changes
240
+ for (const change of enhancedChanges) {
241
+ const category = this.categorizeChange(change)
242
+ if (sections[category]) {
243
+ sections[category].push(change)
244
+ }
245
+ }
361
246
 
362
- buildChangesByCategory(_changes, changesSummary) {
363
- let content = '## Changes by Category\n\n'
247
+ // Generate markdown content
248
+ let content = '# Workspace Changes\n\n'
364
249
 
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`
250
+ for (const [section, changes] of Object.entries(sections)) {
251
+ if (changes.length > 0) {
252
+ content += `## ${this.formatSectionTitle(section)}\n\n`
368
253
 
369
- files.forEach((file) => {
370
- const statusIcon = this.getStatusIcon(file.status)
371
- content += `- ${statusIcon} ${file.path}\n`
372
- })
254
+ for (const change of changes) {
255
+ content += `- ${this.formatChangeEntry(change)}\n`
256
+ }
373
257
 
374
- content += '\n'
375
- })
258
+ content += '\n'
259
+ }
260
+ }
376
261
 
377
262
  return content
378
263
  }
379
264
 
380
- getCategoryIcon(category) {
381
- const icons = {
382
- source: '💻',
383
- tests: '🧪',
384
- documentation: '📚',
385
- configuration: '⚙️',
386
- frontend: '🎨',
387
- assets: '🖼️',
388
- build: '🔧',
389
- other: '📄',
390
- }
391
- return icons[category] || '📄'
392
- }
265
+ /**
266
+ * Generate commit-style working directory entries
267
+ * @returns {Promise<Array>} Array of commit-style entries
268
+ */
269
+ async generateCommitStyleWorkingDirectoryEntries() {
270
+ const workspaceData = await this.analyzeWorkspaceChanges()
271
+ const entries = []
272
+
273
+ if (workspaceData.status) {
274
+ // Process staged files
275
+ if (workspaceData.status.staged) {
276
+ for (const file of workspaceData.status.staged) {
277
+ entries.push({
278
+ type: 'staged',
279
+ file,
280
+ message: `Add: ${file}`,
281
+ timestamp: new Date().toISOString(),
282
+ })
283
+ }
284
+ }
393
285
 
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] || '📄'
403
- }
286
+ // Process unstaged files
287
+ if (workspaceData.status.unstaged) {
288
+ for (const file of workspaceData.status.unstaged) {
289
+ entries.push({
290
+ type: 'unstaged',
291
+ file,
292
+ message: `Modify: ${file}`,
293
+ timestamp: new Date().toISOString(),
294
+ })
295
+ }
296
+ }
404
297
 
405
- async generateChangelogContent(changes, summary, _context, analysisMode) {
406
- if (analysisMode === 'detailed' || analysisMode === 'enterprise') {
407
- return await this.generateAIChangelogContentFromChanges(changes, summary, analysisMode)
298
+ // Process untracked files
299
+ if (workspaceData.status.untracked) {
300
+ for (const file of workspaceData.status.untracked) {
301
+ entries.push({
302
+ type: 'untracked',
303
+ file,
304
+ message: `Create: ${file}`,
305
+ timestamp: new Date().toISOString(),
306
+ })
307
+ }
308
+ }
408
309
  }
409
- return this.generateBasicChangelogContentFromChanges(changes, summary)
310
+
311
+ return entries
410
312
  }
411
313
 
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
- }
314
+ /**
315
+ * Generate workspace changelog
316
+ * @returns {Promise<string>} Workspace changelog content
317
+ */
318
+ async generateWorkspaceChangelog() {
319
+ console.log(colors.processingMessage('📝 Generating workspace changelog...'))
421
320
 
422
321
  try {
423
- if (!(rawChanges && Array.isArray(rawChanges)) || rawChanges.length === 0) {
424
- return { entries: [] }
425
- }
322
+ const entries = await this.generateCommitStyleWorkingDirectoryEntries()
426
323
 
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
- })
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')
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
324
+ if (entries.length === 0) {
325
+ return colors.infoMessage('No workspace changes to include in changelog.')
518
326
  }
519
327
 
520
- // Increase token limit for large numbers of working directory changes
521
- if (enhancedChanges.length > 50) {
522
- maxTokens = Math.min(maxTokens + 1500, 6000)
523
- }
328
+ let content = '# Workspace Changes\n\n'
329
+ content += `Generated on: ${new Date().toLocaleString()}\n\n`
524
330
 
525
- const options_ai = {
526
- max_tokens: maxTokens,
527
- temperature: 0.3,
528
- }
331
+ const groupedEntries = this.groupEntriesByType(entries)
529
332
 
530
- const response = await this.aiAnalysisService.aiProvider.generateCompletion(
531
- messages,
532
- options_ai
533
- )
333
+ for (const [type, typeEntries] of Object.entries(groupedEntries)) {
334
+ if (typeEntries.length > 0) {
335
+ content += `## ${this.formatEntryType(type)} (${typeEntries.length})\n\n`
534
336
 
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
- })
337
+ for (const entry of typeEntries) {
338
+ content += `- ${entry.message}\n`
339
+ }
565
340
 
566
- return { entries: basicEntries }
341
+ content += '\n'
342
+ }
567
343
  }
568
344
 
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
- }
345
+ return content
587
346
  } 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 }
347
+ console.error(colors.errorMessage(`Failed to generate workspace changelog: ${error.message}`))
348
+ throw error
627
349
  }
628
350
  }
629
351
 
630
- async generateWorkspaceChangelog(version = null, options = {}) {
631
- const result = await this.generateComprehensiveWorkspaceChangelog(options)
352
+ // Helper methods
632
353
 
633
- if (!result) {
634
- return null
354
+ /**
355
+ * Generate basic workspace changelog without AI
356
+ */
357
+ generateBasicWorkspaceChangelog(workspaceData) {
358
+ let content = '# Workspace Changes\n\n'
359
+
360
+ if (workspaceData.metrics) {
361
+ content += '## Summary\n\n'
362
+ content += `- Staged files: ${workspaceData.metrics.stagedFiles}\n`
363
+ content += `- Unstaged files: ${workspaceData.metrics.unstagedFiles}\n`
364
+ content += `- Untracked files: ${workspaceData.metrics.untrackedFiles}\n\n`
635
365
  }
636
366
 
637
- let changelog = result.changelog
367
+ return content
368
+ }
638
369
 
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
- }
370
+ /**
371
+ * Generate rule-based changelog fallback
372
+ */
373
+ generateRuleBasedChangelog(changes) {
374
+ let content = '# Changes Summary\n\n'
646
375
 
647
- // Add context information for detailed modes
648
- if (options.analysisMode === 'detailed' || options.analysisMode === 'enterprise') {
649
- changelog += this.generateContextSection(result.context)
376
+ for (const change of changes) {
377
+ content += `- ${change.type || 'Modified'}: ${change.file}\n`
650
378
  }
651
379
 
652
- return {
653
- ...result,
654
- changelog,
655
- version,
656
- }
380
+ return content
657
381
  }
658
382
 
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
- }
383
+ /**
384
+ * Assess change complexity
385
+ */
386
+ assessChangeComplexity(diff) {
387
+ if (!diff) return 'low'
673
388
 
674
- return section
389
+ const lines = diff.split('\n').length
390
+ if (lines > 100) return 'high'
391
+ if (lines > 20) return 'medium'
392
+ return 'low'
675
393
  }
676
394
 
677
- // Missing methods expected by tests
678
- analyzeWorkspaceStructure() {
679
- return {
680
- structure: 'standard',
681
- directories: [],
682
- files: []
683
- }
395
+ /**
396
+ * Assess change impact
397
+ */
398
+ assessChangeImpact(file, diff) {
399
+ const criticalFiles = ['package.json', 'README.md', 'Dockerfile', '.env']
400
+ const isCritical = criticalFiles.some((critical) => file.includes(critical))
401
+
402
+ if (isCritical) return 'high'
403
+ if (file.includes('test') || file.includes('spec')) return 'low'
404
+ return 'medium'
684
405
  }
685
406
 
686
- identifyWorkspacePatterns() {
687
- return {
688
- patterns: ['monorepo', 'standard'],
689
- confidence: 'medium'
690
- }
407
+ /**
408
+ * Categorize a change
409
+ */
410
+ categorizeChange(change) {
411
+ const file = change.file.toLowerCase()
412
+
413
+ if (file.includes('test') || file.includes('spec')) return 'tests'
414
+ if (file.includes('readme') || file.includes('doc')) return 'docs'
415
+ if (file.includes('fix') || change.type === 'fix') return 'fixes'
416
+ if (file.includes('feat') || change.type === 'feature') return 'features'
417
+ if (file.includes('config') || file.includes('package')) return 'chores'
418
+
419
+ return 'improvements'
691
420
  }
692
421
 
693
- assessWorkspaceHealth() {
694
- return {
695
- health: 'good',
696
- issues: [],
697
- score: 85
698
- }
422
+ /**
423
+ * Format section title
424
+ */
425
+ formatSectionTitle(section) {
426
+ const titles = {
427
+ features: 'Features',
428
+ fixes: 'Bug Fixes',
429
+ improvements: 'Improvements',
430
+ docs: 'Documentation',
431
+ tests: 'Tests',
432
+ chores: 'Maintenance',
433
+ }
434
+ return titles[section] || section
699
435
  }
700
436
 
701
- generateWorkspaceInsights() {
702
- return {
703
- insights: [],
704
- recommendations: []
705
- }
437
+ /**
438
+ * Format change entry
439
+ */
440
+ formatChangeEntry(change) {
441
+ const impact = change.impact ? `[${change.impact}]` : ''
442
+ const complexity = change.complexity ? `{${change.complexity}}` : ''
443
+ return `${change.file} ${impact} ${complexity}`.trim()
706
444
  }
707
445
 
708
- optimizeWorkspaceConfiguration() {
709
- return {
710
- optimizations: [],
711
- impact: 'low'
712
- }
446
+ /**
447
+ * Group entries by type
448
+ */
449
+ groupEntriesByType(entries) {
450
+ return entries.reduce((groups, entry) => {
451
+ const type = entry.type || 'unknown'
452
+ if (!groups[type]) groups[type] = []
453
+ groups[type].push(entry)
454
+ return groups
455
+ }, {})
713
456
  }
714
457
 
715
- validateWorkspaceStandards() {
716
- return {
717
- compliant: true,
718
- violations: []
458
+ /**
459
+ * Format entry type for display
460
+ */
461
+ formatEntryType(type) {
462
+ const types = {
463
+ staged: 'Staged Changes',
464
+ unstaged: 'Unstaged Changes',
465
+ untracked: 'New Files',
719
466
  }
467
+ return types[type] || type
720
468
  }
721
469
 
722
- compareWorkspaceConfigurations(a, b) {
723
- return {
724
- similarity: 90,
725
- differences: []
470
+ /**
471
+ * Initialize workspace for analysis
472
+ * @returns {Promise<boolean>} True if initialization successful
473
+ */
474
+ async initializeWorkspace() {
475
+ try {
476
+ console.log(colors.processingMessage('🔧 Initializing workspace...'))
477
+
478
+ // Reset metrics
479
+ this.cleanup()
480
+
481
+ // Validate git repository
482
+ const isValid = await this.validateWorkspace()
483
+ if (!isValid) {
484
+ return false
485
+ }
486
+
487
+ // Perform initial analysis
488
+ await this.analyzeWorkspaceChanges()
489
+
490
+ console.log(colors.successMessage('✅ Workspace initialized successfully'))
491
+ return true
492
+ } catch (error) {
493
+ console.error(colors.errorMessage(`Failed to initialize workspace: ${error.message}`))
494
+ return false
726
495
  }
727
496
  }
728
497
 
729
- extractWorkspaceMetadata() {
730
- return {
731
- type: 'standard',
732
- tools: [],
733
- frameworks: []
498
+ /**
499
+ * Validate workspace structure
500
+ * @returns {boolean} True if workspace structure is valid
501
+ */
502
+ validateWorkspaceStructure() {
503
+ try {
504
+ // Check if we have the required services
505
+ if (!this.gitService) {
506
+ console.error(colors.errorMessage('Git service not available'))
507
+ return false
508
+ }
509
+
510
+ // Check if workspace has any changes to analyze
511
+ if (!this.hasWorkspaceChanges()) {
512
+ console.log(colors.infoMessage('No workspace changes detected'))
513
+ return true // Still valid, just nothing to do
514
+ }
515
+
516
+ return true
517
+ } catch (error) {
518
+ console.error(colors.errorMessage(`Workspace structure validation failed: ${error.message}`))
519
+ return false
734
520
  }
735
521
  }
736
522
 
737
- suggestWorkspaceImprovements() {
738
- return {
739
- improvements: [],
740
- priority: 'low'
523
+ /**
524
+ * Clean up workspace analysis resources
525
+ */
526
+ cleanup() {
527
+ this.workspaceMetrics = {
528
+ unstagedFiles: 0,
529
+ stagedFiles: 0,
530
+ untrackedFiles: 0,
531
+ modifiedLines: 0,
741
532
  }
742
533
  }
743
534
  }