@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/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 response = await fetch(`${this.baseURL}/api/generate`, {
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.response) {
271
+ if (!data.message?.content) {
265
272
  throw new LLMError("Empty response from Ollama");
266
273
  }
267
- return data.response;
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 CATEGORIZE_PROMPT = `You are categorizing changelog entries for a software release.
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
- async function categorizeEntries(provider, entries, _context) {
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 prompt = CATEGORIZE_PROMPT.replace("{{entries}}", entriesText);
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
- for (const [category, indices] of Object.entries(parsed)) {
372
- const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
373
- if (categoryEntries.length > 0) {
374
- result.push({
375
- category,
376
- entries: categoryEntries
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 prompt = ENHANCE_PROMPT.replace("{{type}}", entry.type).replace("{{#if scope}}Scope: {{scope}}{{/if}}", entry.scope ? `Scope: ${entry.scope}` : "").replace("{{description}}", entry.description);
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.debug)("Enhancing entries with LLM");
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.debug)("Summarizing entries with LLM");
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.debug)("Categorizing entries with LLM");
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.debug)("Generating release notes with LLM");
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 && !dryRun) {
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
- (0, import_core9.success)("Changelog generation complete");
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 = ["error", "warn", "info", "debug", "trace"];
1518
- (0, import_core9.setLogLevel)(levels[Math.min(level, levels.length - 1)] ?? "error");
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-GN5RQW3G.js";
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
- success("Changelog generation complete");
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 = ["error", "warn", "info", "debug", "trace"];
146
- setLogLevel(levels[Math.min(level, levels.length - 1)] ?? "error");
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 response = await fetch(`${this.baseURL}/api/generate`, {
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.response) {
321
+ if (!data.message?.content) {
315
322
  throw new LLMError("Empty response from Ollama");
316
323
  }
317
- return data.response;
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 CATEGORIZE_PROMPT = `You are categorizing changelog entries for a software release.
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
- async function categorizeEntries(provider, entries, _context) {
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 prompt = CATEGORIZE_PROMPT.replace("{{entries}}", entriesText);
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
- for (const [category, indices] of Object.entries(parsed)) {
422
- const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
423
- if (categoryEntries.length > 0) {
424
- result.push({
425
- category,
426
- entries: categoryEntries
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 prompt = ENHANCE_PROMPT.replace("{{type}}", entry.type).replace("{{#if scope}}Scope: {{scope}}{{/if}}", entry.scope ? `Scope: ${entry.scope}` : "").replace("{{description}}", entry.description);
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.debug)("Enhancing entries with LLM");
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.debug)("Summarizing entries with LLM");
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.debug)("Categorizing entries with LLM");
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.debug)("Generating release notes with LLM");
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 && !dryRun) {
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 {