@entro314labs/ai-changelog-generator 3.6.0 → 3.7.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.
- package/manifest.json +13 -1
- package/package.json +9 -10
- package/src/application/orchestrators/changelog.orchestrator.js +29 -3
- package/src/application/services/application.service.js +8 -2
- package/src/domains/changelog/changelog.service.js +5 -2
- package/src/domains/git/git-manager.js +144 -2
- package/src/domains/git/git.service.js +22 -2
- package/src/infrastructure/cli/cli.controller.js +442 -6
- package/src/infrastructure/mcp/mcp-server.service.js +8 -5
- package/src/shared/utils/utils.js +6 -20
- package/src/domains/changelog/workspace-changelog.service.js +0 -566
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "ai-changelog-generator",
|
|
4
4
|
"display_name": "AI Changelog Generator",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.7.0",
|
|
6
6
|
"description": "AI-powered changelog generator with MCP server support - works with most providers, online and local models",
|
|
7
7
|
"long_description": "Generate intelligent changelogs from git commits using AI. Supports multiple providers including OpenAI, Claude, Gemini, Ollama, and LM Studio. Perfect for automating release documentation with smart commit analysis and categorization.\n\nFeatures:\n- Automatic changelog generation from git commits\n- Working directory change analysis\n- Multiple AI provider support (OpenAI, Azure, Claude, Gemini, Ollama, LM Studio)\n- Interactive and batch modes\n- Repository health analysis\n- Branch analysis and recommendations\n- Commit categorization and impact assessment",
|
|
8
8
|
"author": {
|
|
@@ -56,6 +56,18 @@
|
|
|
56
56
|
{
|
|
57
57
|
"name": "configure_providers",
|
|
58
58
|
"description": "Manage AI providers - list, switch, test, and configure"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "analyze_stash",
|
|
62
|
+
"description": "List and analyze git stashed changes with AI-powered insights"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "list_provider_models",
|
|
66
|
+
"description": "List available AI models for configured providers"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "check_provider_health",
|
|
70
|
+
"description": "Check health status and connectivity of all AI providers"
|
|
59
71
|
}
|
|
60
72
|
],
|
|
61
73
|
"tools_generated": false,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entro314labs/ai-changelog-generator",
|
|
3
3
|
"displayName": "AI Changelog Generator",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.7.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "AI-powered changelog generator with MCP server support - works with most providers, online and local models",
|
|
7
7
|
"main": "src/ai-changelog-generator.js",
|
|
@@ -22,16 +22,16 @@
|
|
|
22
22
|
"CHANGELOG.md"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@anthropic-ai/sdk": "^0.71.
|
|
26
|
-
"@aws-sdk/client-bedrock-runtime": "^3.
|
|
25
|
+
"@anthropic-ai/sdk": "^0.71.2",
|
|
26
|
+
"@aws-sdk/client-bedrock-runtime": "^3.948.0",
|
|
27
27
|
"@azure/identity": "^4.13.0",
|
|
28
28
|
"@clack/prompts": "^0.11.0",
|
|
29
|
-
"@google/genai": "^1.
|
|
29
|
+
"@google/genai": "^1.33.0",
|
|
30
30
|
"@google/generative-ai": "^0.24.1",
|
|
31
31
|
"@huggingface/hub": "^2.7.1",
|
|
32
|
-
"@huggingface/inference": "^4.13.
|
|
32
|
+
"@huggingface/inference": "^4.13.5",
|
|
33
33
|
"@lmstudio/sdk": "^1.5.0",
|
|
34
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
34
|
+
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
35
35
|
"@modelcontextprotocol/server-filesystem": "2025.11.25",
|
|
36
36
|
"boxen": "^8.0.1",
|
|
37
37
|
"chalk": "^5.6.2",
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"gradient-string": "^3.0.0",
|
|
43
43
|
"js-yaml": "^4.1.1",
|
|
44
44
|
"ollama": "^0.6.3",
|
|
45
|
-
"openai": "^6.
|
|
45
|
+
"openai": "^6.10.0",
|
|
46
46
|
"ora": "^9.0.0",
|
|
47
47
|
"yargs": "^18.0.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@anthropic-ai/mcpb": "^2.
|
|
51
|
-
"@types/node": "
|
|
50
|
+
"@anthropic-ai/mcpb": "^2.1.2",
|
|
51
|
+
"@types/node": "25.0.1",
|
|
52
52
|
"@vitest/browser": "latest",
|
|
53
53
|
"@vitest/coverage-v8": "latest",
|
|
54
54
|
"@vitest/ui": "latest",
|
|
@@ -144,7 +144,6 @@
|
|
|
144
144
|
"test:mcp": "vitest run test/mcp.test.js",
|
|
145
145
|
"test:styling": "vitest run test/cli-ui.test.js test/colors.test.js test/styling-integration.test.js",
|
|
146
146
|
"test:styling-e2e": "vitest run test/cli-styling-e2e.test.js --testTimeout=60000",
|
|
147
|
-
"test:legacy": "node test/test-runner.js comprehensive",
|
|
148
147
|
"test:git": "node src/domains/git/git.service.js info",
|
|
149
148
|
"setup": "node scripts/setup-azure-openai.js",
|
|
150
149
|
"providers:test": "node test/test-providers.js",
|
|
@@ -154,7 +154,7 @@ export class ChangelogOrchestrator {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
async generateChangelog(version, since) {
|
|
157
|
+
async generateChangelog(version, since, extraOptions = {}) {
|
|
158
158
|
try {
|
|
159
159
|
await this.ensureInitialized()
|
|
160
160
|
|
|
@@ -167,8 +167,34 @@ export class ChangelogOrchestrator {
|
|
|
167
167
|
throw new Error('Not a git repository')
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// Handle tag range option (e.g., v1.0.0..v2.0.0)
|
|
171
|
+
let effectiveSince = since
|
|
172
|
+
let effectiveUntil = 'HEAD'
|
|
173
|
+
if (extraOptions.tagRange) {
|
|
174
|
+
const [fromTag, toTag] = extraOptions.tagRange.split('..')
|
|
175
|
+
if (fromTag) {
|
|
176
|
+
effectiveSince = fromTag
|
|
177
|
+
}
|
|
178
|
+
if (toTag) {
|
|
179
|
+
effectiveUntil = toTag
|
|
180
|
+
}
|
|
181
|
+
console.log(colors.infoMessage(`📦 Generating changelog between tags: ${fromTag} → ${toTag || 'HEAD'}`))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Display author filter if specified
|
|
185
|
+
if (extraOptions.author) {
|
|
186
|
+
console.log(colors.infoMessage(`👤 Filtering commits by author: ${extraOptions.author}`))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Merge options
|
|
190
|
+
const mergedOptions = {
|
|
191
|
+
...this.options,
|
|
192
|
+
author: extraOptions.author,
|
|
193
|
+
until: effectiveUntil,
|
|
194
|
+
}
|
|
195
|
+
|
|
170
196
|
// Generate changelog using the service
|
|
171
|
-
const result = await this.changelogService.generateChangelog(version,
|
|
197
|
+
const result = await this.changelogService.generateChangelog(version, effectiveSince, mergedOptions)
|
|
172
198
|
|
|
173
199
|
if (!result) {
|
|
174
200
|
console.log(colors.warningMessage('No changelog generated'))
|
|
@@ -212,7 +238,7 @@ export class ChangelogOrchestrator {
|
|
|
212
238
|
await this.ensureInitialized()
|
|
213
239
|
|
|
214
240
|
// Check for interactive environment
|
|
215
|
-
if (!process.stdin.isTTY
|
|
241
|
+
if (!(process.stdin.isTTY || process.env.CI)) {
|
|
216
242
|
console.log(colors.warningMessage('Interactive mode requires a TTY terminal.'))
|
|
217
243
|
return { interactive: false, status: 'skipped' }
|
|
218
244
|
}
|
|
@@ -41,8 +41,14 @@ export class ApplicationService {
|
|
|
41
41
|
|
|
42
42
|
async generateChangelog(options = {}) {
|
|
43
43
|
try {
|
|
44
|
-
const { version, since } = options
|
|
45
|
-
return await this.orchestrator.generateChangelog(version, since
|
|
44
|
+
const { version, since, author, tagRange, format, output, dryRun } = options
|
|
45
|
+
return await this.orchestrator.generateChangelog(version, since, {
|
|
46
|
+
author,
|
|
47
|
+
tagRange,
|
|
48
|
+
format,
|
|
49
|
+
output,
|
|
50
|
+
dryRun,
|
|
51
|
+
})
|
|
46
52
|
} catch (error) {
|
|
47
53
|
console.error(colors.errorMessage('Application service error:'), error.message)
|
|
48
54
|
throw error
|
|
@@ -37,8 +37,11 @@ export class ChangelogService {
|
|
|
37
37
|
async generateChangelog(version = null, since = null, options = {}) {
|
|
38
38
|
console.log(colors.processingMessage('🤖 Analyzing changes with AI...'))
|
|
39
39
|
|
|
40
|
-
// Get committed changes
|
|
41
|
-
const commits = await this.gitService.getCommitsSince(since
|
|
40
|
+
// Get committed changes with optional filters
|
|
41
|
+
const commits = await this.gitService.getCommitsSince(since, {
|
|
42
|
+
author: options.author,
|
|
43
|
+
until: options.until,
|
|
44
|
+
})
|
|
42
45
|
|
|
43
46
|
// Get working directory changes using analysis engine
|
|
44
47
|
let workingDirAnalysis = null
|
|
@@ -205,11 +205,43 @@ export class GitManager {
|
|
|
205
205
|
* Get commits between two references
|
|
206
206
|
* @param {string} from - Starting reference (commit, tag, branch)
|
|
207
207
|
* @param {string} to - Ending reference (default: 'HEAD')
|
|
208
|
+
* @param {Object} options - Optional filters
|
|
209
|
+
* @param {string} options.author - Filter by author name or email
|
|
208
210
|
* @returns {Array<Object>} Array of commit objects
|
|
209
211
|
*/
|
|
210
|
-
getCommitsBetween(from, to = 'HEAD') {
|
|
212
|
+
getCommitsBetween(from, to = 'HEAD', options = {}) {
|
|
211
213
|
try {
|
|
212
|
-
|
|
214
|
+
let command = `git log ${from}..${to} --oneline`
|
|
215
|
+
if (options.author) {
|
|
216
|
+
command += ` --author="${options.author}"`
|
|
217
|
+
}
|
|
218
|
+
const output = this.execGitSafe(command)
|
|
219
|
+
return output
|
|
220
|
+
.split('\n')
|
|
221
|
+
.filter((line) => line.trim())
|
|
222
|
+
.map((line) => {
|
|
223
|
+
const [hash, ...messageParts] = line.split(' ')
|
|
224
|
+
return {
|
|
225
|
+
hash: hash.trim(),
|
|
226
|
+
message: messageParts.join(' '),
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
} catch {
|
|
230
|
+
return []
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get commits filtered by author
|
|
236
|
+
* @param {string} author - Author name or email to filter by
|
|
237
|
+
* @param {number} limit - Maximum number of commits to return
|
|
238
|
+
* @returns {Array<Object>} Array of commit objects by this author
|
|
239
|
+
*/
|
|
240
|
+
getCommitsByAuthor(author, limit = 50) {
|
|
241
|
+
try {
|
|
242
|
+
const output = this.execGitSafe(
|
|
243
|
+
`git log --author="${author}" --oneline -n ${limit}`
|
|
244
|
+
)
|
|
213
245
|
return output
|
|
214
246
|
.split('\n')
|
|
215
247
|
.filter((line) => line.trim())
|
|
@@ -225,6 +257,116 @@ export class GitManager {
|
|
|
225
257
|
}
|
|
226
258
|
}
|
|
227
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Get commits between two tags
|
|
262
|
+
* @param {string} fromTag - Starting tag
|
|
263
|
+
* @param {string} toTag - Ending tag (default: 'HEAD')
|
|
264
|
+
* @param {Object} options - Optional filters
|
|
265
|
+
* @returns {Array<Object>} Array of commit objects between tags
|
|
266
|
+
*/
|
|
267
|
+
getCommitsBetweenTags(fromTag, toTag = 'HEAD', options = {}) {
|
|
268
|
+
return this.getCommitsBetween(fromTag, toTag, options)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get list of all authors in repository
|
|
273
|
+
* @returns {Array<string>} Array of unique author names
|
|
274
|
+
*/
|
|
275
|
+
getAllAuthors() {
|
|
276
|
+
try {
|
|
277
|
+
const output = this.execGitSafe('git log --format="%an" | sort -u')
|
|
278
|
+
return output
|
|
279
|
+
.split('\n')
|
|
280
|
+
.filter((name) => name.trim())
|
|
281
|
+
.sort()
|
|
282
|
+
} catch {
|
|
283
|
+
return []
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get list of stashed changes
|
|
289
|
+
* @returns {Array<Object>} Array of stash entries with index, message, and date
|
|
290
|
+
*/
|
|
291
|
+
getStashList() {
|
|
292
|
+
try {
|
|
293
|
+
const output = this.execGitSafe('git stash list --format="%gd|%gs|%ci"')
|
|
294
|
+
if (!output.trim()) {
|
|
295
|
+
return []
|
|
296
|
+
}
|
|
297
|
+
return output
|
|
298
|
+
.split('\n')
|
|
299
|
+
.filter((line) => line.trim())
|
|
300
|
+
.map((line) => {
|
|
301
|
+
const [index, message, date] = line.split('|')
|
|
302
|
+
return {
|
|
303
|
+
index: index.trim(),
|
|
304
|
+
message: message.trim(),
|
|
305
|
+
date: date.trim(),
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
} catch {
|
|
309
|
+
return []
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get detailed information about a specific stash entry
|
|
315
|
+
* @param {string} stashRef - Stash reference (e.g., 'stash@{0}')
|
|
316
|
+
* @returns {Object|null} Stash details including files changed
|
|
317
|
+
*/
|
|
318
|
+
getStashDetails(stashRef = 'stash@{0}') {
|
|
319
|
+
try {
|
|
320
|
+
// Get stash message
|
|
321
|
+
const message = this.execGitSafe(`git stash list --format="%gs" -1 ${stashRef}`).trim()
|
|
322
|
+
|
|
323
|
+
// Get files changed in stash
|
|
324
|
+
const statOutput = this.execGitSafe(`git stash show ${stashRef} --stat`)
|
|
325
|
+
const diffOutput = this.execGitSafe(`git stash show ${stashRef} -p`)
|
|
326
|
+
|
|
327
|
+
// Parse stat output for files
|
|
328
|
+
const files = statOutput
|
|
329
|
+
.split('\n')
|
|
330
|
+
.filter((line) => line.includes('|'))
|
|
331
|
+
.map((line) => {
|
|
332
|
+
const match = line.match(/^\s*(.+?)\s*\|\s*(\d+)/)
|
|
333
|
+
if (match) {
|
|
334
|
+
return {
|
|
335
|
+
path: match[1].trim(),
|
|
336
|
+
changes: parseInt(match[2], 10),
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return null
|
|
340
|
+
})
|
|
341
|
+
.filter(Boolean)
|
|
342
|
+
|
|
343
|
+
// Get summary stats
|
|
344
|
+
const summaryMatch = statOutput.match(/(\d+) files? changed(?:, (\d+) insertions?)?(?:, (\d+) deletions?)?/)
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
ref: stashRef,
|
|
348
|
+
message,
|
|
349
|
+
files,
|
|
350
|
+
diff: diffOutput,
|
|
351
|
+
stats: {
|
|
352
|
+
filesChanged: summaryMatch ? parseInt(summaryMatch[1], 10) : files.length,
|
|
353
|
+
insertions: summaryMatch && summaryMatch[2] ? parseInt(summaryMatch[2], 10) : 0,
|
|
354
|
+
deletions: summaryMatch && summaryMatch[3] ? parseInt(summaryMatch[3], 10) : 0,
|
|
355
|
+
},
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
return null
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Check if there are any stashed changes
|
|
364
|
+
* @returns {boolean} True if stash has entries
|
|
365
|
+
*/
|
|
366
|
+
hasStashedChanges() {
|
|
367
|
+
return this.getStashList().length > 0
|
|
368
|
+
}
|
|
369
|
+
|
|
228
370
|
/**
|
|
229
371
|
* Check if a file exists in a specific commit
|
|
230
372
|
* @param {string} commitHash - The commit to check
|
|
@@ -333,9 +333,29 @@ export class GitService {
|
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
async getCommitsSince(since) {
|
|
336
|
+
async getCommitsSince(since, options = {}) {
|
|
337
337
|
try {
|
|
338
|
-
|
|
338
|
+
let command = 'git log --oneline'
|
|
339
|
+
|
|
340
|
+
// Handle different "since" formats
|
|
341
|
+
if (since) {
|
|
342
|
+
// Check if it's a tag/ref (starts with v, contains dots, or is a commit hash)
|
|
343
|
+
const isRef = /^v?\d|^[a-f0-9]{6,40}$/i.test(since)
|
|
344
|
+
if (isRef) {
|
|
345
|
+
const until = options.until || 'HEAD'
|
|
346
|
+
command = `git log ${since}..${until} --oneline`
|
|
347
|
+
} else {
|
|
348
|
+
// Treat as date
|
|
349
|
+
command += ` --since="${since}"`
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
command += ' -10'
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Add author filter if specified
|
|
356
|
+
if (options.author) {
|
|
357
|
+
command += ` --author="${options.author}"`
|
|
358
|
+
}
|
|
339
359
|
|
|
340
360
|
const output = this.gitManager.execGitSafe(command)
|
|
341
361
|
return output
|