@entro314labs/ai-changelog-generator 3.2.0 → 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 (33) hide show
  1. package/CHANGELOG.md +41 -10
  2. package/ai-changelog-mcp.sh +0 -0
  3. package/ai-changelog.sh +0 -0
  4. package/bin/ai-changelog-dxt.js +0 -0
  5. package/package.json +72 -80
  6. package/src/ai-changelog-generator.js +11 -2
  7. package/src/application/orchestrators/changelog.orchestrator.js +12 -202
  8. package/src/cli.js +4 -5
  9. package/src/domains/ai/ai-analysis.service.js +2 -0
  10. package/src/domains/analysis/analysis.engine.js +758 -5
  11. package/src/domains/changelog/changelog.service.js +711 -13
  12. package/src/domains/changelog/workspace-changelog.service.js +429 -571
  13. package/src/domains/git/commit-tagger.js +552 -0
  14. package/src/domains/git/git-manager.js +357 -0
  15. package/src/domains/git/git.service.js +865 -16
  16. package/src/infrastructure/cli/cli.controller.js +14 -9
  17. package/src/infrastructure/config/configuration.manager.js +24 -2
  18. package/src/infrastructure/interactive/interactive-workflow.service.js +8 -1
  19. package/src/infrastructure/mcp/mcp-server.service.js +35 -11
  20. package/src/infrastructure/providers/core/base-provider.js +1 -1
  21. package/src/infrastructure/providers/implementations/anthropic.js +16 -173
  22. package/src/infrastructure/providers/implementations/azure.js +16 -63
  23. package/src/infrastructure/providers/implementations/dummy.js +13 -16
  24. package/src/infrastructure/providers/implementations/mock.js +13 -26
  25. package/src/infrastructure/providers/implementations/ollama.js +12 -4
  26. package/src/infrastructure/providers/implementations/openai.js +13 -165
  27. package/src/infrastructure/providers/provider-management.service.js +126 -412
  28. package/src/infrastructure/providers/utils/base-provider-helpers.js +11 -0
  29. package/src/shared/utils/cli-ui.js +1 -1
  30. package/src/shared/utils/diff-processor.js +21 -19
  31. package/src/shared/utils/error-classes.js +33 -0
  32. package/src/shared/utils/utils.js +65 -60
  33. package/src/domains/git/git-repository.analyzer.js +0 -678
@@ -1,676 +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)
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...'))
62
+
63
+ const workspaceData = await this.analyzeWorkspaceChanges()
64
+
65
+ if (!workspaceData.analysis || workspaceData.analysis.changes.length === 0) {
66
+ return colors.infoMessage('No significant workspace changes detected.')
74
67
  }
75
68
 
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
- }
69
+ // Generate preview using parent class methods
70
+ const previewContent = await this.generateChangelogFromAnalysis(workspaceData.analysis)
101
71
 
102
- const compressionInfo = file.compressionApplied
103
- ? ` [compressed from ${file.originalSize || 'unknown'} chars]`
104
- : ''
105
- const patternInfo = file.bulkPattern ? ` [${file.bulkPattern}]` : ''
72
+ return previewContent
73
+ }
106
74
 
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
- }
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(),
84
+ }
85
+ }
164
86
 
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
- })
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
+ }
190
98
 
191
- changelog = `# Working Directory Changelog - ${timestamp}\n\n## Changes\n\n${basicEntries.join('\n')}`
192
- }
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 = []
193
106
 
194
- // Add metadata
195
- const timestamp = new Date().toISOString().split('T')[0]
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
+ }
196
116
 
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
- }
117
+ return parts.length > 0 ? parts.join(', ') : 'No changes'
118
+ }
201
119
 
202
- // Add generation metadata
203
- changelog += `\n\n---\n\n*Generated from ${changesSummary.totalFiles} working directory changes*\n`
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
131
+ }
204
132
 
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}`))
133
+ // Check if workspace has changes
134
+ if (!this.hasWorkspaceChanges()) {
135
+ console.log(colors.infoMessage('No workspace changes detected'))
136
+ return false
219
137
  }
220
138
 
221
- console.warn(colors.infoMessage('🔄 Falling back to pattern-based analysis'))
222
- return this.generateBasicChangelogContentFromChanges(changes, changesSummary)
139
+ return true
140
+ } catch (error) {
141
+ console.error(colors.errorMessage(`Workspace validation failed: ${error.message}`))
142
+ return false
223
143
  }
224
144
  }
225
145
 
226
- generateBasicChangelogContentFromChanges(changes, changesSummary) {
227
- const timestamp = new Date().toISOString().split('T')[0]
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...'))
228
152
 
229
- let changelog = `# Working Directory Changes - ${timestamp}\n\n`
153
+ try {
154
+ const workspaceData = await this.analyzeWorkspaceChanges()
230
155
 
231
- // Basic summary
232
- changelog += '## Summary\n'
233
- changelog += `${changes.length} files modified across ${Object.keys(changesSummary.categories).length} categories.\n\n`
156
+ if (!workspaceData.analysis) {
157
+ return this.generateBasicWorkspaceChangelog(workspaceData)
158
+ }
234
159
 
235
- // Changes by category
236
- changelog += this.buildChangesByCategory(changes, changesSummary)
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
166
+ }
167
+ }
168
+
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.')
177
+ }
237
178
 
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'
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
+ }
243
185
 
244
- return changelog
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)
191
+ }
245
192
  }
246
193
 
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
+ */
247
199
  async enhanceChangesWithDiff(changes) {
248
200
  const enhancedChanges = []
249
201
 
250
202
  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,
257
- }
258
-
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
- }
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)
278
218
  }
279
-
280
- enhancedChanges.push(enhancedChange)
281
219
  }
282
220
 
283
221
  return enhancedChanges
284
222
  }
285
223
 
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),
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: [],
294
237
  }
295
238
 
296
- return context
297
- }
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
+ }
298
246
 
299
- getPrimaryCategory(categories) {
300
- return Object.entries(categories).sort(([, a], [, b]) => b.length - a.length)[0]?.[0] || 'other'
301
- }
247
+ // Generate markdown content
248
+ let content = '# Workspace Changes\n\n'
302
249
 
303
- assessWorkspaceRisk(changes) {
304
- const highRiskFiles = changes.filter(
305
- (change) =>
306
- change.importance === 'critical' ||
307
- change.category === 'configuration' ||
308
- change.status === 'D'
309
- )
250
+ for (const [section, changes] of Object.entries(sections)) {
251
+ if (changes.length > 0) {
252
+ content += `## ${this.formatSectionTitle(section)}\n\n`
310
253
 
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
- }
254
+ for (const change of changes) {
255
+ content += `- ${this.formatChangeEntry(change)}\n`
256
+ }
319
257
 
320
- assessWorkspaceComplexity(changes) {
321
- if (changes.length > 20) {
322
- return 'high'
323
- }
324
- if (changes.length > 5) {
325
- return 'medium'
258
+ content += '\n'
259
+ }
326
260
  }
327
- return 'low'
328
- }
329
261
 
330
- generateRecommendations(changes) {
331
- const recommendations = []
262
+ return content
263
+ }
332
264
 
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')
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
+ }
337
285
 
338
- if (hasSource && !hasTests) {
339
- recommendations.push('Consider adding tests for source code changes')
340
- }
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
+ }
341
297
 
342
- if (hasConfig) {
343
- recommendations.push('Review configuration changes carefully')
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
+ }
344
309
  }
345
310
 
346
- if (hasSource && !hasDocs) {
347
- recommendations.push('Update documentation for new features')
348
- }
311
+ return entries
312
+ }
349
313
 
350
- if (changes.length > 15) {
351
- recommendations.push('Consider breaking this into smaller commits')
352
- }
314
+ /**
315
+ * Generate workspace changelog
316
+ * @returns {Promise<string>} Workspace changelog content
317
+ */
318
+ async generateWorkspaceChangelog() {
319
+ console.log(colors.processingMessage('📝 Generating workspace changelog...'))
353
320
 
354
- const deletedFiles = changes.filter((change) => change.status === 'D')
355
- if (deletedFiles.length > 0) {
356
- recommendations.push(`Review ${deletedFiles.length} deleted files before committing`)
357
- }
321
+ try {
322
+ const entries = await this.generateCommitStyleWorkingDirectoryEntries()
358
323
 
359
- return recommendations
360
- }
324
+ if (entries.length === 0) {
325
+ return colors.infoMessage('No workspace changes to include in changelog.')
326
+ }
361
327
 
362
- buildChangesByCategory(_changes, changesSummary) {
363
- let content = '## Changes by Category\n\n'
328
+ let content = '# Workspace Changes\n\n'
329
+ content += `Generated on: ${new Date().toLocaleString()}\n\n`
364
330
 
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`
331
+ const groupedEntries = this.groupEntriesByType(entries)
368
332
 
369
- files.forEach((file) => {
370
- const statusIcon = this.getStatusIcon(file.status)
371
- content += `- ${statusIcon} ${file.path}\n`
372
- })
333
+ for (const [type, typeEntries] of Object.entries(groupedEntries)) {
334
+ if (typeEntries.length > 0) {
335
+ content += `## ${this.formatEntryType(type)} (${typeEntries.length})\n\n`
373
336
 
374
- content += '\n'
375
- })
337
+ for (const entry of typeEntries) {
338
+ content += `- ${entry.message}\n`
339
+ }
376
340
 
377
- return content
378
- }
341
+ content += '\n'
342
+ }
343
+ }
379
344
 
380
- getCategoryIcon(category) {
381
- const icons = {
382
- source: '💻',
383
- tests: '🧪',
384
- documentation: '📚',
385
- configuration: '⚙️',
386
- frontend: '🎨',
387
- assets: '🖼️',
388
- build: '🔧',
389
- other: '📄',
345
+ return content
346
+ } catch (error) {
347
+ console.error(colors.errorMessage(`Failed to generate workspace changelog: ${error.message}`))
348
+ throw error
390
349
  }
391
- return icons[category] || '📄'
392
350
  }
393
351
 
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
- }
352
+ // Helper methods
353
+
354
+ /**
355
+ * Generate basic workspace changelog without AI
356
+ */
357
+ generateBasicWorkspaceChangelog(workspaceData) {
358
+ let content = '# Workspace Changes\n\n'
404
359
 
405
- async generateChangelogContent(changes, summary, _context, analysisMode) {
406
- if (analysisMode === 'detailed' || analysisMode === 'enterprise') {
407
- return await this.generateAIChangelogContentFromChanges(changes, summary, analysisMode)
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`
408
365
  }
409
- return this.generateBasicChangelogContentFromChanges(changes, summary)
366
+
367
+ return content
410
368
  }
411
369
 
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()
370
+ /**
371
+ * Generate rule-based changelog fallback
372
+ */
373
+ generateRuleBasedChangelog(changes) {
374
+ let content = '# Changes Summary\n\n'
375
+
376
+ for (const change of changes) {
377
+ content += `- ${change.type || 'Modified'}: ${change.file}\n`
420
378
  }
421
379
 
422
- try {
423
- if (!(rawChanges && Array.isArray(rawChanges)) || rawChanges.length === 0) {
424
- return { entries: [] }
425
- }
380
+ return content
381
+ }
426
382
 
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
- }
383
+ /**
384
+ * Assess change complexity
385
+ */
386
+ assessChangeComplexity(diff) {
387
+ if (!diff) return 'low'
457
388
 
458
- const compressionInfo = file.compressionApplied
459
- ? ` [compressed from ${file.originalSize || 'unknown'} chars]`
460
- : ''
461
- const patternInfo = file.bulkPattern ? ` [${file.bulkPattern}]` : ''
389
+ const lines = diff.split('\n').length
390
+ if (lines > 100) return 'high'
391
+ if (lines > 20) return 'medium'
392
+ return 'low'
393
+ }
462
394
 
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
518
- }
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))
519
401
 
520
- // Increase token limit for large numbers of working directory changes
521
- if (enhancedChanges.length > 50) {
522
- maxTokens = Math.min(maxTokens + 1500, 6000)
523
- }
402
+ if (isCritical) return 'high'
403
+ if (file.includes('test') || file.includes('spec')) return 'low'
404
+ return 'medium'
405
+ }
524
406
 
525
- const options_ai = {
526
- max_tokens: maxTokens,
527
- temperature: 0.3,
528
- }
407
+ /**
408
+ * Categorize a change
409
+ */
410
+ categorizeChange(change) {
411
+ const file = change.file.toLowerCase()
529
412
 
530
- const response = await this.aiAnalysisService.aiProvider.generateCompletion(
531
- messages,
532
- options_ai
533
- )
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'
534
418
 
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
- })
419
+ return 'improvements'
420
+ }
565
421
 
566
- return { entries: basicEntries }
567
- }
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
435
+ }
568
436
 
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
- }
587
- } 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
- }
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()
444
+ }
601
445
 
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 }
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
+ }, {})
456
+ }
457
+
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',
627
466
  }
467
+ return types[type] || type
628
468
  }
629
469
 
630
- async generateWorkspaceChangelog(version = null, options = {}) {
631
- const result = await this.generateComprehensiveWorkspaceChangelog(options)
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...'))
632
477
 
633
- if (!result) {
634
- return null
635
- }
478
+ // Reset metrics
479
+ this.cleanup()
636
480
 
637
- let changelog = result.changelog
481
+ // Validate git repository
482
+ const isValid = await this.validateWorkspace()
483
+ if (!isValid) {
484
+ return false
485
+ }
638
486
 
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
- }
487
+ // Perform initial analysis
488
+ await this.analyzeWorkspaceChanges()
646
489
 
647
- // Add context information for detailed modes
648
- if (options.analysisMode === 'detailed' || options.analysisMode === 'enterprise') {
649
- changelog += this.generateContextSection(result.context)
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
650
495
  }
496
+ }
651
497
 
652
- return {
653
- ...result,
654
- changelog,
655
- version,
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
656
520
  }
657
521
  }
658
522
 
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'
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,
672
532
  }
673
-
674
- return section
675
533
  }
676
534
  }