@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/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,61 @@ 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, 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 prompt = ENHANCE_PROMPT.replace("{{type}}", entry.type).replace("{{#if scope}}Scope: {{scope}}{{/if}}", entry.scope ? `Scope: ${entry.scope}` : "").replace("{{description}}", entry.description);
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.debug)("Enhancing entries with LLM");
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.debug)("Summarizing entries with LLM");
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.debug)("Categorizing entries with LLM");
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.debug)("Generating release notes with LLM");
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 && !dryRun) {
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
- (0, import_core9.success)("Changelog generation complete");
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 = ["error", "warn", "info", "debug", "trace"];
1518
- (0, import_core9.setLogLevel)(levels[Math.min(level, levels.length - 1)] ?? "error");
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-GN5RQW3G.js";
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
- 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,61 @@ 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, 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 prompt = ENHANCE_PROMPT.replace("{{type}}", entry.type).replace("{{#if scope}}Scope: {{scope}}{{/if}}", entry.scope ? `Scope: ${entry.scope}` : "").replace("{{description}}", entry.description);
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.debug)("Enhancing entries with LLM");
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.debug)("Summarizing entries with LLM");
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.debug)("Categorizing entries with LLM");
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.debug)("Generating release notes with LLM");
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 && !dryRun) {
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 {