@releasekit/notes 0.1.0 → 0.2.0-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-NHDLQLG2.js} +96 -27
- package/dist/cli.cjs +115 -32
- package/dist/cli.js +20 -6
- package/dist/index.cjs +96 -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,61 @@ 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, indices] of Object.entries(categoryMap)) {
|
|
410
|
+
const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
|
|
411
|
+
if (categoryEntries.length > 0) {
|
|
412
|
+
result.push({ category, entries: categoryEntries });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
const categoryMap = parsed;
|
|
417
|
+
for (const [category, indices] of Object.entries(categoryMap)) {
|
|
418
|
+
const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
|
|
419
|
+
if (categoryEntries.length > 0) {
|
|
420
|
+
result.push({ category, entries: categoryEntries });
|
|
421
|
+
}
|
|
378
422
|
}
|
|
379
423
|
}
|
|
380
424
|
return result;
|
|
@@ -392,10 +436,10 @@ Given a technical commit message, rewrite it as a clear, user-friendly changelog
|
|
|
392
436
|
|
|
393
437
|
Rules:
|
|
394
438
|
- Be concise (1-2 sentences max)
|
|
395
|
-
- Use present tense ("Add feature" not "Added feature")
|
|
396
439
|
- Focus on user impact, not implementation details
|
|
397
440
|
- Don't use technical jargon unless necessary
|
|
398
441
|
- Preserve the scope if mentioned (e.g., "core:", "api:")
|
|
442
|
+
{{style}}
|
|
399
443
|
|
|
400
444
|
Original entry:
|
|
401
445
|
Type: {{type}}
|
|
@@ -404,7 +448,8 @@ Description: {{description}}
|
|
|
404
448
|
|
|
405
449
|
Rewritten description (only output the new description, nothing else):`;
|
|
406
450
|
async function enhanceEntry(provider, entry, _context) {
|
|
407
|
-
const
|
|
451
|
+
const styleText = _context.style ? `- ${_context.style}` : '- Use present tense ("Add feature" not "Added feature")';
|
|
452
|
+
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
453
|
const response = await provider.complete(prompt);
|
|
409
454
|
return response.trim();
|
|
410
455
|
}
|
|
@@ -506,6 +551,7 @@ function createProvider(config) {
|
|
|
506
551
|
});
|
|
507
552
|
case "ollama":
|
|
508
553
|
return new OllamaProvider({
|
|
554
|
+
apiKey,
|
|
509
555
|
baseURL: config.baseURL,
|
|
510
556
|
model: config.model
|
|
511
557
|
});
|
|
@@ -578,6 +624,10 @@ function formatVersion(context) {
|
|
|
578
624
|
lines.push(`[Full Changelog](${context.compareUrl})`);
|
|
579
625
|
lines.push("");
|
|
580
626
|
}
|
|
627
|
+
if (context.enhanced?.summary) {
|
|
628
|
+
lines.push(context.enhanced.summary);
|
|
629
|
+
lines.push("");
|
|
630
|
+
}
|
|
581
631
|
const grouped = groupEntriesByType(context.entries);
|
|
582
632
|
for (const [type, entries] of grouped) {
|
|
583
633
|
if (entries.length === 0) continue;
|
|
@@ -1123,37 +1173,58 @@ async function processWithLLM(context, config) {
|
|
|
1123
1173
|
packageName: context.packageName,
|
|
1124
1174
|
version: context.version,
|
|
1125
1175
|
previousVersion: context.previousVersion ?? void 0,
|
|
1126
|
-
date: context.date
|
|
1176
|
+
date: context.date,
|
|
1177
|
+
categories: config.llm.categories,
|
|
1178
|
+
style: config.llm.style
|
|
1127
1179
|
};
|
|
1128
1180
|
const enhanced = {
|
|
1129
1181
|
entries: context.entries
|
|
1130
1182
|
};
|
|
1131
1183
|
try {
|
|
1184
|
+
(0, import_core7.info)(`Using LLM provider: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
|
|
1185
|
+
if (config.llm.baseURL) {
|
|
1186
|
+
(0, import_core7.info)(`LLM base URL: ${config.llm.baseURL}`);
|
|
1187
|
+
}
|
|
1132
1188
|
const rawProvider = createProvider(config.llm);
|
|
1133
1189
|
const retryOpts = config.llm.retry ?? LLM_DEFAULTS.retry;
|
|
1134
1190
|
const provider = {
|
|
1135
1191
|
name: rawProvider.name,
|
|
1136
1192
|
complete: (prompt, opts) => withRetry(() => rawProvider.complete(prompt, opts), retryOpts)
|
|
1137
1193
|
};
|
|
1194
|
+
const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
|
|
1195
|
+
(0, import_core7.info)(`Running LLM tasks: ${activeTasks.join(", ")}`);
|
|
1138
1196
|
if (tasks.enhance) {
|
|
1139
|
-
(0, import_core7.
|
|
1197
|
+
(0, import_core7.info)("Enhancing entries with LLM...");
|
|
1140
1198
|
enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
|
|
1199
|
+
(0, import_core7.info)(`Enhanced ${enhanced.entries.length} entries`);
|
|
1141
1200
|
}
|
|
1142
1201
|
if (tasks.summarize) {
|
|
1143
|
-
(0, import_core7.
|
|
1202
|
+
(0, import_core7.info)("Summarizing entries with LLM...");
|
|
1144
1203
|
enhanced.summary = await summarizeEntries(provider, enhanced.entries, llmContext);
|
|
1204
|
+
if (enhanced.summary) {
|
|
1205
|
+
(0, import_core7.info)("Summary generated successfully");
|
|
1206
|
+
(0, import_core7.debug)(`Summary: ${enhanced.summary.substring(0, 100)}...`);
|
|
1207
|
+
} else {
|
|
1208
|
+
(0, import_core7.warn)("Summary generation returned empty result");
|
|
1209
|
+
}
|
|
1145
1210
|
}
|
|
1146
1211
|
if (tasks.categorize) {
|
|
1147
|
-
(0, import_core7.
|
|
1212
|
+
(0, import_core7.info)("Categorizing entries with LLM...");
|
|
1148
1213
|
const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
|
|
1149
1214
|
enhanced.categories = {};
|
|
1150
1215
|
for (const cat of categorized) {
|
|
1151
1216
|
enhanced.categories[cat.category] = cat.entries;
|
|
1152
1217
|
}
|
|
1218
|
+
(0, import_core7.info)(`Created ${categorized.length} categories`);
|
|
1153
1219
|
}
|
|
1154
1220
|
if (tasks.releaseNotes) {
|
|
1155
|
-
(0, import_core7.
|
|
1221
|
+
(0, import_core7.info)("Generating release notes with LLM...");
|
|
1156
1222
|
enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
|
|
1223
|
+
if (enhanced.releaseNotes) {
|
|
1224
|
+
(0, import_core7.info)("Release notes generated successfully");
|
|
1225
|
+
} else {
|
|
1226
|
+
(0, import_core7.warn)("Release notes generation returned empty result");
|
|
1227
|
+
}
|
|
1157
1228
|
}
|
|
1158
1229
|
return {
|
|
1159
1230
|
...context,
|
|
@@ -1205,11 +1276,9 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
|
|
|
1205
1276
|
async function runPipeline(input, config, dryRun) {
|
|
1206
1277
|
(0, import_core7.debug)(`Processing ${input.packages.length} package(s)`);
|
|
1207
1278
|
let contexts = input.packages.map(createTemplateContext);
|
|
1208
|
-
if (config.llm && !process.env.CHANGELOG_NO_LLM
|
|
1279
|
+
if (config.llm && !process.env.CHANGELOG_NO_LLM) {
|
|
1209
1280
|
(0, import_core7.info)("Processing with LLM enhancement");
|
|
1210
1281
|
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
1282
|
}
|
|
1214
1283
|
for (const output of config.output) {
|
|
1215
1284
|
(0, import_core7.info)(`Generating ${output.format} output`);
|
|
@@ -1392,7 +1461,7 @@ function detectMonorepo(cwd) {
|
|
|
1392
1461
|
// src/cli.ts
|
|
1393
1462
|
var program = new import_commander.Command();
|
|
1394
1463
|
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) => {
|
|
1464
|
+
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
1465
|
setVerbosity(options.verbose);
|
|
1397
1466
|
if (options.quiet) (0, import_core9.setQuietMode)(true);
|
|
1398
1467
|
try {
|
|
@@ -1413,11 +1482,13 @@ program.command("generate", { isDefault: true }).description("Generate changelog
|
|
|
1413
1482
|
config.templates = { ...config.templates, engine: options.engine };
|
|
1414
1483
|
}
|
|
1415
1484
|
if (options.llm === false) {
|
|
1485
|
+
(0, import_core9.info)("LLM processing disabled via --no-llm flag");
|
|
1416
1486
|
delete config.llm;
|
|
1417
|
-
} else if (options.llmProvider || options.llmModel || options.llmTasks) {
|
|
1487
|
+
} else if (options.llmProvider || options.llmModel || options.llmBaseUrl || options.llmTasks) {
|
|
1418
1488
|
config.llm = config.llm ?? { provider: "openai-compatible", model: "" };
|
|
1419
1489
|
if (options.llmProvider) config.llm.provider = options.llmProvider;
|
|
1420
1490
|
if (options.llmModel) config.llm.model = options.llmModel;
|
|
1491
|
+
if (options.llmBaseUrl) config.llm.baseURL = options.llmBaseUrl;
|
|
1421
1492
|
if (options.llmTasks) {
|
|
1422
1493
|
const taskNames = options.llmTasks.split(",").map((t) => t.trim());
|
|
1423
1494
|
config.llm.tasks = {
|
|
@@ -1427,6 +1498,14 @@ program.command("generate", { isDefault: true }).description("Generate changelog
|
|
|
1427
1498
|
releaseNotes: taskNames.includes("release-notes") || taskNames.includes("releaseNotes")
|
|
1428
1499
|
};
|
|
1429
1500
|
}
|
|
1501
|
+
(0, import_core9.info)(`LLM configured: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
|
|
1502
|
+
if (config.llm.baseURL) {
|
|
1503
|
+
(0, import_core9.info)(`LLM base URL: ${config.llm.baseURL}`);
|
|
1504
|
+
}
|
|
1505
|
+
const taskList = Object.entries(config.llm.tasks || {}).filter(([, enabled]) => enabled).map(([name]) => name).join(", ");
|
|
1506
|
+
if (taskList) {
|
|
1507
|
+
(0, import_core9.info)(`LLM tasks: ${taskList}`);
|
|
1508
|
+
}
|
|
1430
1509
|
}
|
|
1431
1510
|
let inputJson;
|
|
1432
1511
|
if (options.input) {
|
|
@@ -1458,7 +1537,11 @@ program.command("generate", { isDefault: true }).description("Generate changelog
|
|
|
1458
1537
|
} else {
|
|
1459
1538
|
await runPipeline(input, config, options.dryRun ?? false);
|
|
1460
1539
|
}
|
|
1461
|
-
(
|
|
1540
|
+
if (options.dryRun) {
|
|
1541
|
+
(0, import_core9.info)("Dry run complete - no files were written");
|
|
1542
|
+
} else {
|
|
1543
|
+
(0, import_core9.success)("Changelog generation complete");
|
|
1544
|
+
}
|
|
1462
1545
|
} catch (err) {
|
|
1463
1546
|
handleError(err);
|
|
1464
1547
|
}
|
|
@@ -1514,8 +1597,8 @@ function increaseVerbosity(_, previous) {
|
|
|
1514
1597
|
return previous + 1;
|
|
1515
1598
|
}
|
|
1516
1599
|
function setVerbosity(level) {
|
|
1517
|
-
const levels = ["
|
|
1518
|
-
(0, import_core9.setLogLevel)(levels[Math.min(level, levels.length - 1)] ?? "
|
|
1600
|
+
const levels = ["info", "debug", "trace"];
|
|
1601
|
+
(0, import_core9.setLogLevel)(levels[Math.min(level, levels.length - 1)] ?? "info");
|
|
1519
1602
|
}
|
|
1520
1603
|
async function readStdin() {
|
|
1521
1604
|
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-NHDLQLG2.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,61 @@ 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, indices] of Object.entries(categoryMap)) {
|
|
460
|
+
const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
|
|
461
|
+
if (categoryEntries.length > 0) {
|
|
462
|
+
result.push({ category, entries: categoryEntries });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
const categoryMap = parsed;
|
|
467
|
+
for (const [category, indices] of Object.entries(categoryMap)) {
|
|
468
|
+
const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
|
|
469
|
+
if (categoryEntries.length > 0) {
|
|
470
|
+
result.push({ category, entries: categoryEntries });
|
|
471
|
+
}
|
|
428
472
|
}
|
|
429
473
|
}
|
|
430
474
|
return result;
|
|
@@ -442,10 +486,10 @@ Given a technical commit message, rewrite it as a clear, user-friendly changelog
|
|
|
442
486
|
|
|
443
487
|
Rules:
|
|
444
488
|
- Be concise (1-2 sentences max)
|
|
445
|
-
- Use present tense ("Add feature" not "Added feature")
|
|
446
489
|
- Focus on user impact, not implementation details
|
|
447
490
|
- Don't use technical jargon unless necessary
|
|
448
491
|
- Preserve the scope if mentioned (e.g., "core:", "api:")
|
|
492
|
+
{{style}}
|
|
449
493
|
|
|
450
494
|
Original entry:
|
|
451
495
|
Type: {{type}}
|
|
@@ -454,7 +498,8 @@ Description: {{description}}
|
|
|
454
498
|
|
|
455
499
|
Rewritten description (only output the new description, nothing else):`;
|
|
456
500
|
async function enhanceEntry(provider, entry, _context) {
|
|
457
|
-
const
|
|
501
|
+
const styleText = _context.style ? `- ${_context.style}` : '- Use present tense ("Add feature" not "Added feature")';
|
|
502
|
+
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
503
|
const response = await provider.complete(prompt);
|
|
459
504
|
return response.trim();
|
|
460
505
|
}
|
|
@@ -556,6 +601,7 @@ function createProvider(config) {
|
|
|
556
601
|
});
|
|
557
602
|
case "ollama":
|
|
558
603
|
return new OllamaProvider({
|
|
604
|
+
apiKey,
|
|
559
605
|
baseURL: config.baseURL,
|
|
560
606
|
model: config.model
|
|
561
607
|
});
|
|
@@ -628,6 +674,10 @@ function formatVersion(context) {
|
|
|
628
674
|
lines.push(`[Full Changelog](${context.compareUrl})`);
|
|
629
675
|
lines.push("");
|
|
630
676
|
}
|
|
677
|
+
if (context.enhanced?.summary) {
|
|
678
|
+
lines.push(context.enhanced.summary);
|
|
679
|
+
lines.push("");
|
|
680
|
+
}
|
|
631
681
|
const grouped = groupEntriesByType(context.entries);
|
|
632
682
|
for (const [type, entries] of grouped) {
|
|
633
683
|
if (entries.length === 0) continue;
|
|
@@ -1173,37 +1223,58 @@ async function processWithLLM(context, config) {
|
|
|
1173
1223
|
packageName: context.packageName,
|
|
1174
1224
|
version: context.version,
|
|
1175
1225
|
previousVersion: context.previousVersion ?? void 0,
|
|
1176
|
-
date: context.date
|
|
1226
|
+
date: context.date,
|
|
1227
|
+
categories: config.llm.categories,
|
|
1228
|
+
style: config.llm.style
|
|
1177
1229
|
};
|
|
1178
1230
|
const enhanced = {
|
|
1179
1231
|
entries: context.entries
|
|
1180
1232
|
};
|
|
1181
1233
|
try {
|
|
1234
|
+
(0, import_core7.info)(`Using LLM provider: ${config.llm.provider}${config.llm.model ? ` (${config.llm.model})` : ""}`);
|
|
1235
|
+
if (config.llm.baseURL) {
|
|
1236
|
+
(0, import_core7.info)(`LLM base URL: ${config.llm.baseURL}`);
|
|
1237
|
+
}
|
|
1182
1238
|
const rawProvider = createProvider(config.llm);
|
|
1183
1239
|
const retryOpts = config.llm.retry ?? LLM_DEFAULTS.retry;
|
|
1184
1240
|
const provider = {
|
|
1185
1241
|
name: rawProvider.name,
|
|
1186
1242
|
complete: (prompt, opts) => withRetry(() => rawProvider.complete(prompt, opts), retryOpts)
|
|
1187
1243
|
};
|
|
1244
|
+
const activeTasks = Object.entries(tasks).filter(([, enabled]) => enabled).map(([name]) => name);
|
|
1245
|
+
(0, import_core7.info)(`Running LLM tasks: ${activeTasks.join(", ")}`);
|
|
1188
1246
|
if (tasks.enhance) {
|
|
1189
|
-
(0, import_core7.
|
|
1247
|
+
(0, import_core7.info)("Enhancing entries with LLM...");
|
|
1190
1248
|
enhanced.entries = await enhanceEntries(provider, context.entries, llmContext, config.llm.concurrency);
|
|
1249
|
+
(0, import_core7.info)(`Enhanced ${enhanced.entries.length} entries`);
|
|
1191
1250
|
}
|
|
1192
1251
|
if (tasks.summarize) {
|
|
1193
|
-
(0, import_core7.
|
|
1252
|
+
(0, import_core7.info)("Summarizing entries with LLM...");
|
|
1194
1253
|
enhanced.summary = await summarizeEntries(provider, enhanced.entries, llmContext);
|
|
1254
|
+
if (enhanced.summary) {
|
|
1255
|
+
(0, import_core7.info)("Summary generated successfully");
|
|
1256
|
+
(0, import_core7.debug)(`Summary: ${enhanced.summary.substring(0, 100)}...`);
|
|
1257
|
+
} else {
|
|
1258
|
+
(0, import_core7.warn)("Summary generation returned empty result");
|
|
1259
|
+
}
|
|
1195
1260
|
}
|
|
1196
1261
|
if (tasks.categorize) {
|
|
1197
|
-
(0, import_core7.
|
|
1262
|
+
(0, import_core7.info)("Categorizing entries with LLM...");
|
|
1198
1263
|
const categorized = await categorizeEntries(provider, enhanced.entries, llmContext);
|
|
1199
1264
|
enhanced.categories = {};
|
|
1200
1265
|
for (const cat of categorized) {
|
|
1201
1266
|
enhanced.categories[cat.category] = cat.entries;
|
|
1202
1267
|
}
|
|
1268
|
+
(0, import_core7.info)(`Created ${categorized.length} categories`);
|
|
1203
1269
|
}
|
|
1204
1270
|
if (tasks.releaseNotes) {
|
|
1205
|
-
(0, import_core7.
|
|
1271
|
+
(0, import_core7.info)("Generating release notes with LLM...");
|
|
1206
1272
|
enhanced.releaseNotes = await generateReleaseNotes(provider, enhanced.entries, llmContext);
|
|
1273
|
+
if (enhanced.releaseNotes) {
|
|
1274
|
+
(0, import_core7.info)("Release notes generated successfully");
|
|
1275
|
+
} else {
|
|
1276
|
+
(0, import_core7.warn)("Release notes generation returned empty result");
|
|
1277
|
+
}
|
|
1207
1278
|
}
|
|
1208
1279
|
return {
|
|
1209
1280
|
...context,
|
|
@@ -1255,11 +1326,9 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
|
|
|
1255
1326
|
async function runPipeline(input, config, dryRun) {
|
|
1256
1327
|
(0, import_core7.debug)(`Processing ${input.packages.length} package(s)`);
|
|
1257
1328
|
let contexts = input.packages.map(createTemplateContext);
|
|
1258
|
-
if (config.llm && !process.env.CHANGELOG_NO_LLM
|
|
1329
|
+
if (config.llm && !process.env.CHANGELOG_NO_LLM) {
|
|
1259
1330
|
(0, import_core7.info)("Processing with LLM enhancement");
|
|
1260
1331
|
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
1332
|
}
|
|
1264
1333
|
for (const output of config.output) {
|
|
1265
1334
|
(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 {
|