@releasekit/notes 0.1.0 → 0.1.1-next.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/README.md +87 -143
- package/dist/{chunk-GN5RQW3G.js → chunk-W7DVGQ7D.js} +98 -27
- package/dist/cli.cjs +117 -32
- package/dist/cli.js +20 -6
- package/dist/index.cjs +98 -27
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -1
- package/package.json +5 -5
- package/dist/chunk-7Y5Y5LY6.js +0 -1496
- package/dist/chunk-DZVNOAOX.js +0 -1447
- package/dist/chunk-NYHVW46L.js +0 -1516
- package/dist/chunk-R4KN5PKO.js +0 -1509
- package/dist/chunk-RKRG4VKL.js +0 -1281
- package/dist/chunk-RX6K7GTL.js +0 -1363
- package/dist/chunk-TQNHZXY7.js +0 -1279
- package/dist/chunk-WMYZTQDI.js +0 -1496
- package/dist/chunk-XRLAWPB3.js +0 -1410
package/dist/cli.cjs
CHANGED
|
@@ -233,15 +233,17 @@ var OllamaProvider = class extends BaseLLMProvider {
|
|
|
233
233
|
name = "ollama";
|
|
234
234
|
baseURL;
|
|
235
235
|
model;
|
|
236
|
+
apiKey;
|
|
236
237
|
constructor(config = {}) {
|
|
237
238
|
super();
|
|
238
239
|
this.baseURL = config.baseURL ?? process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
|
|
239
240
|
this.model = config.model ?? LLM_DEFAULTS.models.ollama;
|
|
241
|
+
this.apiKey = config.apiKey ?? process.env.OLLAMA_API_KEY;
|
|
240
242
|
}
|
|
241
243
|
async complete(prompt, options) {
|
|
242
244
|
const requestBody = {
|
|
243
245
|
model: this.model,
|
|
244
|
-
prompt,
|
|
246
|
+
messages: [{ role: "user", content: prompt }],
|
|
245
247
|
stream: false,
|
|
246
248
|
options: {
|
|
247
249
|
num_predict: this.getMaxTokens(options),
|
|
@@ -249,11 +251,16 @@ var OllamaProvider = class extends BaseLLMProvider {
|
|
|
249
251
|
}
|
|
250
252
|
};
|
|
251
253
|
try {
|
|
252
|
-
const
|
|
254
|
+
const headers = {
|
|
255
|
+
"Content-Type": "application/json"
|
|
256
|
+
};
|
|
257
|
+
if (this.apiKey) {
|
|
258
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
259
|
+
}
|
|
260
|
+
const baseUrl = this.baseURL.endsWith("/api") ? this.baseURL.slice(0, -4) : this.baseURL;
|
|
261
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
253
262
|
method: "POST",
|
|
254
|
-
headers
|
|
255
|
-
"Content-Type": "application/json"
|
|
256
|
-
},
|
|
263
|
+
headers,
|
|
257
264
|
body: JSON.stringify(requestBody)
|
|
258
265
|
});
|
|
259
266
|
if (!response.ok) {
|
|
@@ -261,10 +268,10 @@ var OllamaProvider = class extends BaseLLMProvider {
|
|
|
261
268
|
throw new LLMError(`Ollama request failed: ${response.status} ${text}`);
|
|
262
269
|
}
|
|
263
270
|
const data = await response.json();
|
|
264
|
-
if (!data.
|
|
271
|
+
if (!data.message?.content) {
|
|
265
272
|
throw new LLMError("Empty response from Ollama");
|
|
266
273
|
}
|
|
267
|
-
return data.
|
|
274
|
+
return data.message.content;
|
|
268
275
|
} catch (error2) {
|
|
269
276
|
if (error2 instanceof LLMError) throw error2;
|
|
270
277
|
throw new LLMError(`Ollama error: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
@@ -347,7 +354,7 @@ var OpenAICompatibleProvider = class extends BaseLLMProvider {
|
|
|
347
354
|
|
|
348
355
|
// src/llm/tasks/categorize.ts
|
|
349
356
|
var import_core3 = require("@releasekit/core");
|
|
350
|
-
var
|
|
357
|
+
var DEFAULT_CATEGORIZE_PROMPT = `You are categorizing changelog entries for a software release.
|
|
351
358
|
|
|
352
359
|
Given the following entries, group them into meaningful categories (e.g., "Core", "UI", "API", "Performance", "Bug Fixes", "Documentation").
|
|
353
360
|
|
|
@@ -357,24 +364,63 @@ Entries:
|
|
|
357
364
|
{{entries}}
|
|
358
365
|
|
|
359
366
|
Output only valid JSON, nothing else:`;
|
|
360
|
-
|
|
367
|
+
function buildCustomCategorizePrompt(categories) {
|
|
368
|
+
const categoryList = categories.map((c) => `- "${c.name}": ${c.description}`).join("\n");
|
|
369
|
+
return `You are categorizing changelog entries for a software release.
|
|
370
|
+
|
|
371
|
+
Given the following entries, group them into the specified categories. Only use the categories listed below.
|
|
372
|
+
|
|
373
|
+
Categories:
|
|
374
|
+
${categoryList}
|
|
375
|
+
|
|
376
|
+
For entries in categories that involve internal/developer changes, set a "scope" field on those entries with a short subcategory label (e.g., "CI", "Dependencies", "Testing", "Code Quality", "Build System").
|
|
377
|
+
|
|
378
|
+
Output a JSON object with two fields:
|
|
379
|
+
- "categories": an object where keys are category names and values are arrays of entry indices (0-based)
|
|
380
|
+
- "scopes": an object where keys are entry indices (as strings) and values are scope labels
|
|
381
|
+
|
|
382
|
+
Entries:
|
|
383
|
+
{{entries}}
|
|
384
|
+
|
|
385
|
+
Output only valid JSON, nothing else:`;
|
|
386
|
+
}
|
|
387
|
+
async function categorizeEntries(provider, entries, context) {
|
|
361
388
|
if (entries.length === 0) {
|
|
362
389
|
return [];
|
|
363
390
|
}
|
|
364
391
|
const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
|
|
365
|
-
const
|
|
392
|
+
const hasCustomCategories = context.categories && context.categories.length > 0;
|
|
393
|
+
const promptTemplate = hasCustomCategories ? buildCustomCategorizePrompt(context.categories) : DEFAULT_CATEGORIZE_PROMPT;
|
|
394
|
+
const prompt = promptTemplate.replace("{{entries}}", entriesText);
|
|
366
395
|
try {
|
|
367
396
|
const response = await provider.complete(prompt);
|
|
368
397
|
const cleaned = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
369
398
|
const parsed = JSON.parse(cleaned);
|
|
370
399
|
const result = [];
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
400
|
+
if (hasCustomCategories && parsed.categories) {
|
|
401
|
+
const categoryMap = parsed.categories;
|
|
402
|
+
const scopeMap = parsed.scopes || {};
|
|
403
|
+
for (const [indexStr, scope] of Object.entries(scopeMap)) {
|
|
404
|
+
const idx = Number.parseInt(indexStr, 10);
|
|
405
|
+
if (entries[idx] && scope) {
|
|
406
|
+
entries[idx] = { ...entries[idx], scope };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
for (const [category, rawIndices] of Object.entries(categoryMap)) {
|
|
410
|
+
const indices = Array.isArray(rawIndices) ? rawIndices : [];
|
|
411
|
+
const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
|
|
412
|
+
if (categoryEntries.length > 0) {
|
|
413
|
+
result.push({ category, entries: categoryEntries });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
const categoryMap = parsed;
|
|
418
|
+
for (const [category, rawIndices] of Object.entries(categoryMap)) {
|
|
419
|
+
const indices = Array.isArray(rawIndices) ? rawIndices : [];
|
|
420
|
+
const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
|
|
421
|
+
if (categoryEntries.length > 0) {
|
|
422
|
+
result.push({ category, entries: categoryEntries });
|
|
423
|
+
}
|
|
378
424
|
}
|
|
379
425
|
}
|
|
380
426
|
return result;
|
|
@@ -392,10 +438,10 @@ Given a technical commit message, rewrite it as a clear, user-friendly changelog
|
|
|
392
438
|
|
|
393
439
|
Rules:
|
|
394
440
|
- Be concise (1-2 sentences max)
|
|
395
|
-
- Use present tense ("Add feature" not "Added feature")
|
|
396
441
|
- Focus on user impact, not implementation details
|
|
397
442
|
- Don't use technical jargon unless necessary
|
|
398
443
|
- Preserve the scope if mentioned (e.g., "core:", "api:")
|
|
444
|
+
{{style}}
|
|
399
445
|
|
|
400
446
|
Original entry:
|
|
401
447
|
Type: {{type}}
|
|
@@ -404,7 +450,8 @@ Description: {{description}}
|
|
|
404
450
|
|
|
405
451
|
Rewritten description (only output the new description, nothing else):`;
|
|
406
452
|
async function enhanceEntry(provider, entry, _context) {
|
|
407
|
-
const
|
|
453
|
+
const styleText = _context.style ? `- ${_context.style}` : '- Use present tense ("Add feature" not "Added feature")';
|
|
454
|
+
const prompt = ENHANCE_PROMPT.replace("{{style}}", styleText).replace("{{type}}", entry.type).replace("{{#if scope}}Scope: {{scope}}{{/if}}", entry.scope ? `Scope: ${entry.scope}` : "").replace("{{description}}", entry.description);
|
|
408
455
|
const response = await provider.complete(prompt);
|
|
409
456
|
return response.trim();
|
|
410
457
|
}
|
|
@@ -506,6 +553,7 @@ function createProvider(config) {
|
|
|
506
553
|
});
|
|
507
554
|
case "ollama":
|
|
508
555
|
return new OllamaProvider({
|
|
556
|
+
apiKey,
|
|
509
557
|
baseURL: config.baseURL,
|
|
510
558
|
model: config.model
|
|
511
559
|
});
|
|
@@ -578,6 +626,10 @@ function formatVersion(context) {
|
|
|
578
626
|
lines.push(`[Full Changelog](${context.compareUrl})`);
|
|
579
627
|
lines.push("");
|
|
580
628
|
}
|
|
629
|
+
if (context.enhanced?.summary) {
|
|
630
|
+
lines.push(context.enhanced.summary);
|
|
631
|
+
lines.push("");
|
|
632
|
+
}
|
|
581
633
|
const grouped = groupEntriesByType(context.entries);
|
|
582
634
|
for (const [type, entries] of grouped) {
|
|
583
635
|
if (entries.length === 0) continue;
|
|
@@ -1123,37 +1175,58 @@ async function processWithLLM(context, config) {
|
|
|
1123
1175
|
packageName: context.packageName,
|
|
1124
1176
|
version: context.version,
|
|
1125
1177
|
previousVersion: context.previousVersion ?? void 0,
|
|
1126
|
-
date: context.date
|
|
1178
|
+
date: context.date,
|
|
1179
|
+
categories: config.llm.categories,
|
|
1180
|
+
style: config.llm.style
|
|
1127
1181
|
};
|
|
1128
1182
|
const enhanced = {
|
|
1129
1183
|
entries: context.entries
|
|
1130
1184
|
};
|
|
1131
1185
|
try {
|
|
1186
|
+
(0, import_core7.info)(`Using LLM provider: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
|
|
1187
|
+
if (config.llm.baseURL) {
|
|
1188
|
+
(0, import_core7.info)(`LLM base URL: ${config.llm.baseURL}`);
|
|
1189
|
+
}
|
|
1132
1190
|
const rawProvider = createProvider(config.llm);
|
|
1133
1191
|
const retryOpts = config.llm.retry ?? LLM_DEFAULTS.retry;
|
|
1134
1192
|
const provider = {
|
|
1135
1193
|
name: rawProvider.name,
|
|
1136
1194
|
complete: (prompt, opts) => withRetry(() => rawProvider.complete(prompt, opts), retryOpts)
|
|
1137
1195
|
};
|
|
1196
|
+
const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
|
|
1197
|
+
(0, import_core7.info)(`Running LLM tasks: ${activeTasks.join(", ")}`);
|
|
1138
1198
|
if (tasks.enhance) {
|
|
1139
|
-
(0, import_core7.
|
|
1199
|
+
(0, import_core7.info)("Enhancing entries with LLM...");
|
|
1140
1200
|
enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
|
|
1201
|
+
(0, import_core7.info)(`Enhanced ${enhanced.entries.length} entries`);
|
|
1141
1202
|
}
|
|
1142
1203
|
if (tasks.summarize) {
|
|
1143
|
-
(0, import_core7.
|
|
1204
|
+
(0, import_core7.info)("Summarizing entries with LLM...");
|
|
1144
1205
|
enhanced.summary = await summarizeEntries(provider, enhanced.entries, llmContext);
|
|
1206
|
+
if (enhanced.summary) {
|
|
1207
|
+
(0, import_core7.info)("Summary generated successfully");
|
|
1208
|
+
(0, import_core7.debug)(`Summary: ${enhanced.summary.substring(0, 100)}...`);
|
|
1209
|
+
} else {
|
|
1210
|
+
(0, import_core7.warn)("Summary generation returned empty result");
|
|
1211
|
+
}
|
|
1145
1212
|
}
|
|
1146
1213
|
if (tasks.categorize) {
|
|
1147
|
-
(0, import_core7.
|
|
1214
|
+
(0, import_core7.info)("Categorizing entries with LLM...");
|
|
1148
1215
|
const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
|
|
1149
1216
|
enhanced.categories = {};
|
|
1150
1217
|
for (const cat of categorized) {
|
|
1151
1218
|
enhanced.categories[cat.category] = cat.entries;
|
|
1152
1219
|
}
|
|
1220
|
+
(0, import_core7.info)(`Created ${categorized.length} categories`);
|
|
1153
1221
|
}
|
|
1154
1222
|
if (tasks.releaseNotes) {
|
|
1155
|
-
(0, import_core7.
|
|
1223
|
+
(0, import_core7.info)("Generating release notes with LLM...");
|
|
1156
1224
|
enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
|
|
1225
|
+
if (enhanced.releaseNotes) {
|
|
1226
|
+
(0, import_core7.info)("Release notes generated successfully");
|
|
1227
|
+
} else {
|
|
1228
|
+
(0, import_core7.warn)("Release notes generation returned empty result");
|
|
1229
|
+
}
|
|
1157
1230
|
}
|
|
1158
1231
|
return {
|
|
1159
1232
|
...context,
|
|
@@ -1205,11 +1278,9 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
|
|
|
1205
1278
|
async function runPipeline(input, config, dryRun) {
|
|
1206
1279
|
(0, import_core7.debug)(`Processing ${input.packages.length} package(s)`);
|
|
1207
1280
|
let contexts = input.packages.map(createTemplateContext);
|
|
1208
|
-
if (config.llm && !process.env.CHANGELOG_NO_LLM
|
|
1281
|
+
if (config.llm && !process.env.CHANGELOG_NO_LLM) {
|
|
1209
1282
|
(0, import_core7.info)("Processing with LLM enhancement");
|
|
1210
1283
|
contexts = await Promise.all(contexts.map((ctx) => processWithLLM(ctx, config)));
|
|
1211
|
-
} else if (config.llm && dryRun) {
|
|
1212
|
-
(0, import_core7.info)("Skipping LLM processing in dry-run mode");
|
|
1213
1284
|
}
|
|
1214
1285
|
for (const output of config.output) {
|
|
1215
1286
|
(0, import_core7.info)(`Generating ${output.format} output`);
|
|
@@ -1392,7 +1463,7 @@ function detectMonorepo(cwd) {
|
|
|
1392
1463
|
// src/cli.ts
|
|
1393
1464
|
var program = new import_commander.Command();
|
|
1394
1465
|
program.name("releasekit-notes").description("Generate changelogs with LLM-powered enhancement and flexible templating").version("0.1.0");
|
|
1395
|
-
program.command("generate", { isDefault: true }).description("Generate changelog from input data").option("-i, --input <file>", "Input file (default: stdin)").option("-o, --output <spec>", "Output spec (format:file)", collectOutputs, []).option("-t, --template <path>", "Template file or directory").option("-e, --engine <engine>", "Template engine (handlebars|liquid|ejs)").option("--monorepo <mode>", "Monorepo mode (root|packages|both)").option("--llm-provider <provider>", "LLM provider").option("--llm-model <model>", "LLM model").option("--llm-tasks <tasks>", "Comma-separated LLM tasks").option("--no-llm", "Disable LLM processing").option("--config <path>", "Config file path").option("--dry-run", "Preview without writing").option("--regenerate", "Regenerate entire changelog").option("-v, --verbose", "Increase verbosity", increaseVerbosity, 0).option("-q, --quiet", "Suppress non-error output").action(async (options) => {
|
|
1466
|
+
program.command("generate", { isDefault: true }).description("Generate changelog from input data").option("-i, --input <file>", "Input file (default: stdin)").option("-o, --output <spec>", "Output spec (format:file)", collectOutputs, []).option("-t, --template <path>", "Template file or directory").option("-e, --engine <engine>", "Template engine (handlebars|liquid|ejs)").option("--monorepo <mode>", "Monorepo mode (root|packages|both)").option("--llm-provider <provider>", "LLM provider").option("--llm-model <model>", "LLM model").option("--llm-base-url <url>", "LLM base URL (for openai-compatible provider)").option("--llm-tasks <tasks>", "Comma-separated LLM tasks").option("--no-llm", "Disable LLM processing").option("--config <path>", "Config file path").option("--dry-run", "Preview without writing").option("--regenerate", "Regenerate entire changelog").option("-v, --verbose", "Increase verbosity", increaseVerbosity, 0).option("-q, --quiet", "Suppress non-error output").action(async (options) => {
|
|
1396
1467
|
setVerbosity(options.verbose);
|
|
1397
1468
|
if (options.quiet) (0, import_core9.setQuietMode)(true);
|
|
1398
1469
|
try {
|
|
@@ -1413,11 +1484,13 @@ program.command("generate", { isDefault: true }).description("Generate changelog
|
|
|
1413
1484
|
config.templates = { ...config.templates, engine: options.engine };
|
|
1414
1485
|
}
|
|
1415
1486
|
if (options.llm === false) {
|
|
1487
|
+
(0, import_core9.info)("LLM processing disabled via --no-llm flag");
|
|
1416
1488
|
delete config.llm;
|
|
1417
|
-
} else if (options.llmProvider || options.llmModel || options.llmTasks) {
|
|
1489
|
+
} else if (options.llmProvider || options.llmModel || options.llmBaseUrl || options.llmTasks) {
|
|
1418
1490
|
config.llm = config.llm ?? { provider: "openai-compatible", model: "" };
|
|
1419
1491
|
if (options.llmProvider) config.llm.provider = options.llmProvider;
|
|
1420
1492
|
if (options.llmModel) config.llm.model = options.llmModel;
|
|
1493
|
+
if (options.llmBaseUrl) config.llm.baseURL = options.llmBaseUrl;
|
|
1421
1494
|
if (options.llmTasks) {
|
|
1422
1495
|
const taskNames = options.llmTasks.split(",").map((t) => t.trim());
|
|
1423
1496
|
config.llm.tasks = {
|
|
@@ -1427,6 +1500,14 @@ program.command("generate", { isDefault: true }).description("Generate changelog
|
|
|
1427
1500
|
releaseNotes: taskNames.includes("release-notes") || taskNames.includes("releaseNotes")
|
|
1428
1501
|
};
|
|
1429
1502
|
}
|
|
1503
|
+
(0, import_core9.info)(`LLM configured: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
|
|
1504
|
+
if (config.llm.baseURL) {
|
|
1505
|
+
(0, import_core9.info)(`LLM base URL: ${config.llm.baseURL}`);
|
|
1506
|
+
}
|
|
1507
|
+
const taskList = Object.entries(config.llm.tasks || {}).filter(([, enabled]) => enabled).map(([name]) => name).join(", ");
|
|
1508
|
+
if (taskList) {
|
|
1509
|
+
(0, import_core9.info)(`LLM tasks: ${taskList}`);
|
|
1510
|
+
}
|
|
1430
1511
|
}
|
|
1431
1512
|
let inputJson;
|
|
1432
1513
|
if (options.input) {
|
|
@@ -1458,7 +1539,11 @@ program.command("generate", { isDefault: true }).description("Generate changelog
|
|
|
1458
1539
|
} else {
|
|
1459
1540
|
await runPipeline(input, config, options.dryRun ?? false);
|
|
1460
1541
|
}
|
|
1461
|
-
(
|
|
1542
|
+
if (options.dryRun) {
|
|
1543
|
+
(0, import_core9.info)("Dry run complete - no files were written");
|
|
1544
|
+
} else {
|
|
1545
|
+
(0, import_core9.success)("Changelog generation complete");
|
|
1546
|
+
}
|
|
1462
1547
|
} catch (err) {
|
|
1463
1548
|
handleError(err);
|
|
1464
1549
|
}
|
|
@@ -1514,8 +1599,8 @@ function increaseVerbosity(_, previous) {
|
|
|
1514
1599
|
return previous + 1;
|
|
1515
1600
|
}
|
|
1516
1601
|
function setVerbosity(level) {
|
|
1517
|
-
const levels = ["
|
|
1518
|
-
(0, import_core9.setLogLevel)(levels[Math.min(level, levels.length - 1)] ?? "
|
|
1602
|
+
const levels = ["info", "debug", "trace"];
|
|
1603
|
+
(0, import_core9.setLogLevel)(levels[Math.min(level, levels.length - 1)] ?? "info");
|
|
1519
1604
|
}
|
|
1520
1605
|
async function readStdin() {
|
|
1521
1606
|
const chunks = [];
|
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
runPipeline,
|
|
12
12
|
saveAuth,
|
|
13
13
|
writeMonorepoChangelogs
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-W7DVGQ7D.js";
|
|
15
15
|
|
|
16
16
|
// src/cli.ts
|
|
17
17
|
import * as fs from "fs";
|
|
@@ -20,7 +20,7 @@ import { error, info, setLogLevel, setQuietMode, success } from "@releasekit/cor
|
|
|
20
20
|
import { Command } from "commander";
|
|
21
21
|
var program = new Command();
|
|
22
22
|
program.name("releasekit-notes").description("Generate changelogs with LLM-powered enhancement and flexible templating").version("0.1.0");
|
|
23
|
-
program.command("generate", { isDefault: true }).description("Generate changelog from input data").option("-i, --input <file>", "Input file (default: stdin)").option("-o, --output <spec>", "Output spec (format:file)", collectOutputs, []).option("-t, --template <path>", "Template file or directory").option("-e, --engine <engine>", "Template engine (handlebars|liquid|ejs)").option("--monorepo <mode>", "Monorepo mode (root|packages|both)").option("--llm-provider <provider>", "LLM provider").option("--llm-model <model>", "LLM model").option("--llm-tasks <tasks>", "Comma-separated LLM tasks").option("--no-llm", "Disable LLM processing").option("--config <path>", "Config file path").option("--dry-run", "Preview without writing").option("--regenerate", "Regenerate entire changelog").option("-v, --verbose", "Increase verbosity", increaseVerbosity, 0).option("-q, --quiet", "Suppress non-error output").action(async (options) => {
|
|
23
|
+
program.command("generate", { isDefault: true }).description("Generate changelog from input data").option("-i, --input <file>", "Input file (default: stdin)").option("-o, --output <spec>", "Output spec (format:file)", collectOutputs, []).option("-t, --template <path>", "Template file or directory").option("-e, --engine <engine>", "Template engine (handlebars|liquid|ejs)").option("--monorepo <mode>", "Monorepo mode (root|packages|both)").option("--llm-provider <provider>", "LLM provider").option("--llm-model <model>", "LLM model").option("--llm-base-url <url>", "LLM base URL (for openai-compatible provider)").option("--llm-tasks <tasks>", "Comma-separated LLM tasks").option("--no-llm", "Disable LLM processing").option("--config <path>", "Config file path").option("--dry-run", "Preview without writing").option("--regenerate", "Regenerate entire changelog").option("-v, --verbose", "Increase verbosity", increaseVerbosity, 0).option("-q, --quiet", "Suppress non-error output").action(async (options) => {
|
|
24
24
|
setVerbosity(options.verbose);
|
|
25
25
|
if (options.quiet) setQuietMode(true);
|
|
26
26
|
try {
|
|
@@ -41,11 +41,13 @@ program.command("generate", { isDefault: true }).description("Generate changelog
|
|
|
41
41
|
config.templates = { ...config.templates, engine: options.engine };
|
|
42
42
|
}
|
|
43
43
|
if (options.llm === false) {
|
|
44
|
+
info("LLM processing disabled via --no-llm flag");
|
|
44
45
|
delete config.llm;
|
|
45
|
-
} else if (options.llmProvider || options.llmModel || options.llmTasks) {
|
|
46
|
+
} else if (options.llmProvider || options.llmModel || options.llmBaseUrl || options.llmTasks) {
|
|
46
47
|
config.llm = config.llm ?? { provider: "openai-compatible", model: "" };
|
|
47
48
|
if (options.llmProvider) config.llm.provider = options.llmProvider;
|
|
48
49
|
if (options.llmModel) config.llm.model = options.llmModel;
|
|
50
|
+
if (options.llmBaseUrl) config.llm.baseURL = options.llmBaseUrl;
|
|
49
51
|
if (options.llmTasks) {
|
|
50
52
|
const taskNames = options.llmTasks.split(",").map((t) => t.trim());
|
|
51
53
|
config.llm.tasks = {
|
|
@@ -55,6 +57,14 @@ program.command("generate", { isDefault: true }).description("Generate changelog
|
|
|
55
57
|
releaseNotes: taskNames.includes("release-notes") || taskNames.includes("releaseNotes")
|
|
56
58
|
};
|
|
57
59
|
}
|
|
60
|
+
info(`LLM configured: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
|
|
61
|
+
if (config.llm.baseURL) {
|
|
62
|
+
info(`LLM base URL: ${config.llm.baseURL}`);
|
|
63
|
+
}
|
|
64
|
+
const taskList = Object.entries(config.llm.tasks || {}).filter(([, enabled]) => enabled).map(([name]) => name).join(", ");
|
|
65
|
+
if (taskList) {
|
|
66
|
+
info(`LLM tasks: ${taskList}`);
|
|
67
|
+
}
|
|
58
68
|
}
|
|
59
69
|
let inputJson;
|
|
60
70
|
if (options.input) {
|
|
@@ -86,7 +96,11 @@ program.command("generate", { isDefault: true }).description("Generate changelog
|
|
|
86
96
|
} else {
|
|
87
97
|
await runPipeline(input, config, options.dryRun ?? false);
|
|
88
98
|
}
|
|
89
|
-
|
|
99
|
+
if (options.dryRun) {
|
|
100
|
+
info("Dry run complete - no files were written");
|
|
101
|
+
} else {
|
|
102
|
+
success("Changelog generation complete");
|
|
103
|
+
}
|
|
90
104
|
} catch (err) {
|
|
91
105
|
handleError(err);
|
|
92
106
|
}
|
|
@@ -142,8 +156,8 @@ function increaseVerbosity(_, previous) {
|
|
|
142
156
|
return previous + 1;
|
|
143
157
|
}
|
|
144
158
|
function setVerbosity(level) {
|
|
145
|
-
const levels = ["
|
|
146
|
-
setLogLevel(levels[Math.min(level, levels.length - 1)] ?? "
|
|
159
|
+
const levels = ["info", "debug", "trace"];
|
|
160
|
+
setLogLevel(levels[Math.min(level, levels.length - 1)] ?? "info");
|
|
147
161
|
}
|
|
148
162
|
async function readStdin() {
|
|
149
163
|
const chunks = [];
|
package/dist/index.cjs
CHANGED
|
@@ -283,15 +283,17 @@ var OllamaProvider = class extends BaseLLMProvider {
|
|
|
283
283
|
name = "ollama";
|
|
284
284
|
baseURL;
|
|
285
285
|
model;
|
|
286
|
+
apiKey;
|
|
286
287
|
constructor(config = {}) {
|
|
287
288
|
super();
|
|
288
289
|
this.baseURL = config.baseURL ?? process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
|
|
289
290
|
this.model = config.model ?? LLM_DEFAULTS.models.ollama;
|
|
291
|
+
this.apiKey = config.apiKey ?? process.env.OLLAMA_API_KEY;
|
|
290
292
|
}
|
|
291
293
|
async complete(prompt, options) {
|
|
292
294
|
const requestBody = {
|
|
293
295
|
model: this.model,
|
|
294
|
-
prompt,
|
|
296
|
+
messages: [{ role: "user", content: prompt }],
|
|
295
297
|
stream: false,
|
|
296
298
|
options: {
|
|
297
299
|
num_predict: this.getMaxTokens(options),
|
|
@@ -299,11 +301,16 @@ var OllamaProvider = class extends BaseLLMProvider {
|
|
|
299
301
|
}
|
|
300
302
|
};
|
|
301
303
|
try {
|
|
302
|
-
const
|
|
304
|
+
const headers = {
|
|
305
|
+
"Content-Type": "application/json"
|
|
306
|
+
};
|
|
307
|
+
if (this.apiKey) {
|
|
308
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
309
|
+
}
|
|
310
|
+
const baseUrl = this.baseURL.endsWith("/api") ? this.baseURL.slice(0, -4) : this.baseURL;
|
|
311
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
303
312
|
method: "POST",
|
|
304
|
-
headers
|
|
305
|
-
"Content-Type": "application/json"
|
|
306
|
-
},
|
|
313
|
+
headers,
|
|
307
314
|
body: JSON.stringify(requestBody)
|
|
308
315
|
});
|
|
309
316
|
if (!response.ok) {
|
|
@@ -311,10 +318,10 @@ var OllamaProvider = class extends BaseLLMProvider {
|
|
|
311
318
|
throw new LLMError(`Ollama request failed: ${response.status} ${text}`);
|
|
312
319
|
}
|
|
313
320
|
const data = await response.json();
|
|
314
|
-
if (!data.
|
|
321
|
+
if (!data.message?.content) {
|
|
315
322
|
throw new LLMError("Empty response from Ollama");
|
|
316
323
|
}
|
|
317
|
-
return data.
|
|
324
|
+
return data.message.content;
|
|
318
325
|
} catch (error) {
|
|
319
326
|
if (error instanceof LLMError) throw error;
|
|
320
327
|
throw new LLMError(`Ollama error: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -397,7 +404,7 @@ var OpenAICompatibleProvider = class extends BaseLLMProvider {
|
|
|
397
404
|
|
|
398
405
|
// src/llm/tasks/categorize.ts
|
|
399
406
|
var import_core3 = require("@releasekit/core");
|
|
400
|
-
var
|
|
407
|
+
var DEFAULT_CATEGORIZE_PROMPT = `You are categorizing changelog entries for a software release.
|
|
401
408
|
|
|
402
409
|
Given the following entries, group them into meaningful categories (e.g., "Core", "UI", "API", "Performance", "Bug Fixes", "Documentation").
|
|
403
410
|
|
|
@@ -407,24 +414,63 @@ Entries:
|
|
|
407
414
|
{{entries}}
|
|
408
415
|
|
|
409
416
|
Output only valid JSON, nothing else:`;
|
|
410
|
-
|
|
417
|
+
function buildCustomCategorizePrompt(categories) {
|
|
418
|
+
const categoryList = categories.map((c) => `- "${c.name}": ${c.description}`).join("\n");
|
|
419
|
+
return `You are categorizing changelog entries for a software release.
|
|
420
|
+
|
|
421
|
+
Given the following entries, group them into the specified categories. Only use the categories listed below.
|
|
422
|
+
|
|
423
|
+
Categories:
|
|
424
|
+
${categoryList}
|
|
425
|
+
|
|
426
|
+
For entries in categories that involve internal/developer changes, set a "scope" field on those entries with a short subcategory label (e.g., "CI", "Dependencies", "Testing", "Code Quality", "Build System").
|
|
427
|
+
|
|
428
|
+
Output a JSON object with two fields:
|
|
429
|
+
- "categories": an object where keys are category names and values are arrays of entry indices (0-based)
|
|
430
|
+
- "scopes": an object where keys are entry indices (as strings) and values are scope labels
|
|
431
|
+
|
|
432
|
+
Entries:
|
|
433
|
+
{{entries}}
|
|
434
|
+
|
|
435
|
+
Output only valid JSON, nothing else:`;
|
|
436
|
+
}
|
|
437
|
+
async function categorizeEntries(provider, entries, context) {
|
|
411
438
|
if (entries.length === 0) {
|
|
412
439
|
return [];
|
|
413
440
|
}
|
|
414
441
|
const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
|
|
415
|
-
const
|
|
442
|
+
const hasCustomCategories = context.categories && context.categories.length > 0;
|
|
443
|
+
const promptTemplate = hasCustomCategories ? buildCustomCategorizePrompt(context.categories) : DEFAULT_CATEGORIZE_PROMPT;
|
|
444
|
+
const prompt = promptTemplate.replace("{{entries}}", entriesText);
|
|
416
445
|
try {
|
|
417
446
|
const response = await provider.complete(prompt);
|
|
418
447
|
const cleaned = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
419
448
|
const parsed = JSON.parse(cleaned);
|
|
420
449
|
const result = [];
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
450
|
+
if (hasCustomCategories && parsed.categories) {
|
|
451
|
+
const categoryMap = parsed.categories;
|
|
452
|
+
const scopeMap = parsed.scopes || {};
|
|
453
|
+
for (const [indexStr, scope] of Object.entries(scopeMap)) {
|
|
454
|
+
const idx = Number.parseInt(indexStr, 10);
|
|
455
|
+
if (entries[idx] && scope) {
|
|
456
|
+
entries[idx] = { ...entries[idx], scope };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
for (const [category, rawIndices] of Object.entries(categoryMap)) {
|
|
460
|
+
const indices = Array.isArray(rawIndices) ? rawIndices : [];
|
|
461
|
+
const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
|
|
462
|
+
if (categoryEntries.length > 0) {
|
|
463
|
+
result.push({ category, entries: categoryEntries });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
const categoryMap = parsed;
|
|
468
|
+
for (const [category, rawIndices] of Object.entries(categoryMap)) {
|
|
469
|
+
const indices = Array.isArray(rawIndices) ? rawIndices : [];
|
|
470
|
+
const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
|
|
471
|
+
if (categoryEntries.length > 0) {
|
|
472
|
+
result.push({ category, entries: categoryEntries });
|
|
473
|
+
}
|
|
428
474
|
}
|
|
429
475
|
}
|
|
430
476
|
return result;
|
|
@@ -442,10 +488,10 @@ Given a technical commit message, rewrite it as a clear, user-friendly changelog
|
|
|
442
488
|
|
|
443
489
|
Rules:
|
|
444
490
|
- Be concise (1-2 sentences max)
|
|
445
|
-
- Use present tense ("Add feature" not "Added feature")
|
|
446
491
|
- Focus on user impact, not implementation details
|
|
447
492
|
- Don't use technical jargon unless necessary
|
|
448
493
|
- Preserve the scope if mentioned (e.g., "core:", "api:")
|
|
494
|
+
{{style}}
|
|
449
495
|
|
|
450
496
|
Original entry:
|
|
451
497
|
Type: {{type}}
|
|
@@ -454,7 +500,8 @@ Description: {{description}}
|
|
|
454
500
|
|
|
455
501
|
Rewritten description (only output the new description, nothing else):`;
|
|
456
502
|
async function enhanceEntry(provider, entry, _context) {
|
|
457
|
-
const
|
|
503
|
+
const styleText = _context.style ? `- ${_context.style}` : '- Use present tense ("Add feature" not "Added feature")';
|
|
504
|
+
const prompt = ENHANCE_PROMPT.replace("{{style}}", styleText).replace("{{type}}", entry.type).replace("{{#if scope}}Scope: {{scope}}{{/if}}", entry.scope ? `Scope: ${entry.scope}` : "").replace("{{description}}", entry.description);
|
|
458
505
|
const response = await provider.complete(prompt);
|
|
459
506
|
return response.trim();
|
|
460
507
|
}
|
|
@@ -556,6 +603,7 @@ function createProvider(config) {
|
|
|
556
603
|
});
|
|
557
604
|
case "ollama":
|
|
558
605
|
return new OllamaProvider({
|
|
606
|
+
apiKey,
|
|
559
607
|
baseURL: config.baseURL,
|
|
560
608
|
model: config.model
|
|
561
609
|
});
|
|
@@ -628,6 +676,10 @@ function formatVersion(context) {
|
|
|
628
676
|
lines.push(`[Full Changelog](${context.compareUrl})`);
|
|
629
677
|
lines.push("");
|
|
630
678
|
}
|
|
679
|
+
if (context.enhanced?.summary) {
|
|
680
|
+
lines.push(context.enhanced.summary);
|
|
681
|
+
lines.push("");
|
|
682
|
+
}
|
|
631
683
|
const grouped = groupEntriesByType(context.entries);
|
|
632
684
|
for (const [type, entries] of grouped) {
|
|
633
685
|
if (entries.length === 0) continue;
|
|
@@ -1173,37 +1225,58 @@ async function processWithLLM(context, config) {
|
|
|
1173
1225
|
packageName: context.packageName,
|
|
1174
1226
|
version: context.version,
|
|
1175
1227
|
previousVersion: context.previousVersion ?? void 0,
|
|
1176
|
-
date: context.date
|
|
1228
|
+
date: context.date,
|
|
1229
|
+
categories: config.llm.categories,
|
|
1230
|
+
style: config.llm.style
|
|
1177
1231
|
};
|
|
1178
1232
|
const enhanced = {
|
|
1179
1233
|
entries: context.entries
|
|
1180
1234
|
};
|
|
1181
1235
|
try {
|
|
1236
|
+
(0, import_core7.info)(`Using LLM provider: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
|
|
1237
|
+
if (config.llm.baseURL) {
|
|
1238
|
+
(0, import_core7.info)(`LLM base URL: ${config.llm.baseURL}`);
|
|
1239
|
+
}
|
|
1182
1240
|
const rawProvider = createProvider(config.llm);
|
|
1183
1241
|
const retryOpts = config.llm.retry ?? LLM_DEFAULTS.retry;
|
|
1184
1242
|
const provider = {
|
|
1185
1243
|
name: rawProvider.name,
|
|
1186
1244
|
complete: (prompt, opts) => withRetry(() => rawProvider.complete(prompt, opts), retryOpts)
|
|
1187
1245
|
};
|
|
1246
|
+
const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
|
|
1247
|
+
(0, import_core7.info)(`Running LLM tasks: ${activeTasks.join(", ")}`);
|
|
1188
1248
|
if (tasks.enhance) {
|
|
1189
|
-
(0, import_core7.
|
|
1249
|
+
(0, import_core7.info)("Enhancing entries with LLM...");
|
|
1190
1250
|
enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
|
|
1251
|
+
(0, import_core7.info)(`Enhanced ${enhanced.entries.length} entries`);
|
|
1191
1252
|
}
|
|
1192
1253
|
if (tasks.summarize) {
|
|
1193
|
-
(0, import_core7.
|
|
1254
|
+
(0, import_core7.info)("Summarizing entries with LLM...");
|
|
1194
1255
|
enhanced.summary = await summarizeEntries(provider, enhanced.entries, llmContext);
|
|
1256
|
+
if (enhanced.summary) {
|
|
1257
|
+
(0, import_core7.info)("Summary generated successfully");
|
|
1258
|
+
(0, import_core7.debug)(`Summary: ${enhanced.summary.substring(0, 100)}...`);
|
|
1259
|
+
} else {
|
|
1260
|
+
(0, import_core7.warn)("Summary generation returned empty result");
|
|
1261
|
+
}
|
|
1195
1262
|
}
|
|
1196
1263
|
if (tasks.categorize) {
|
|
1197
|
-
(0, import_core7.
|
|
1264
|
+
(0, import_core7.info)("Categorizing entries with LLM...");
|
|
1198
1265
|
const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
|
|
1199
1266
|
enhanced.categories = {};
|
|
1200
1267
|
for (const cat of categorized) {
|
|
1201
1268
|
enhanced.categories[cat.category] = cat.entries;
|
|
1202
1269
|
}
|
|
1270
|
+
(0, import_core7.info)(`Created ${categorized.length} categories`);
|
|
1203
1271
|
}
|
|
1204
1272
|
if (tasks.releaseNotes) {
|
|
1205
|
-
(0, import_core7.
|
|
1273
|
+
(0, import_core7.info)("Generating release notes with LLM...");
|
|
1206
1274
|
enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
|
|
1275
|
+
if (enhanced.releaseNotes) {
|
|
1276
|
+
(0, import_core7.info)("Release notes generated successfully");
|
|
1277
|
+
} else {
|
|
1278
|
+
(0, import_core7.warn)("Release notes generation returned empty result");
|
|
1279
|
+
}
|
|
1207
1280
|
}
|
|
1208
1281
|
return {
|
|
1209
1282
|
...context,
|
|
@@ -1255,11 +1328,9 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
|
|
|
1255
1328
|
async function runPipeline(input, config, dryRun) {
|
|
1256
1329
|
(0, import_core7.debug)(`Processing ${input.packages.length} package(s)`);
|
|
1257
1330
|
let contexts = input.packages.map(createTemplateContext);
|
|
1258
|
-
if (config.llm && !process.env.CHANGELOG_NO_LLM
|
|
1331
|
+
if (config.llm && !process.env.CHANGELOG_NO_LLM) {
|
|
1259
1332
|
(0, import_core7.info)("Processing with LLM enhancement");
|
|
1260
1333
|
contexts = await Promise.all(contexts.map((ctx) => processWithLLM(ctx, config)));
|
|
1261
|
-
} else if (config.llm && dryRun) {
|
|
1262
|
-
(0, import_core7.info)("Skipping LLM processing in dry-run mode");
|
|
1263
1334
|
}
|
|
1264
1335
|
for (const output of config.output) {
|
|
1265
1336
|
(0, import_core7.info)(`Generating ${output.format} output`);
|
package/dist/index.d.cts
CHANGED
|
@@ -84,6 +84,11 @@ interface LLMConfig {
|
|
|
84
84
|
categorize?: boolean;
|
|
85
85
|
releaseNotes?: boolean;
|
|
86
86
|
};
|
|
87
|
+
categories?: Array<{
|
|
88
|
+
name: string;
|
|
89
|
+
description: string;
|
|
90
|
+
}>;
|
|
91
|
+
style?: string;
|
|
87
92
|
}
|
|
88
93
|
type OutputFormat = 'markdown' | 'github-release' | 'json';
|
|
89
94
|
interface OutputConfig {
|
package/dist/index.d.ts
CHANGED
|
@@ -84,6 +84,11 @@ interface LLMConfig {
|
|
|
84
84
|
categorize?: boolean;
|
|
85
85
|
releaseNotes?: boolean;
|
|
86
86
|
};
|
|
87
|
+
categories?: Array<{
|
|
88
|
+
name: string;
|
|
89
|
+
description: string;
|
|
90
|
+
}>;
|
|
91
|
+
style?: string;
|
|
87
92
|
}
|
|
88
93
|
type OutputFormat = 'markdown' | 'github-release' | 'json';
|
|
89
94
|
interface OutputConfig {
|