@cullit/core 1.8.0 → 1.10.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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { EnrichmentType, OutputFormat, AIConfig, RepoSource, CullConfig } from '@cullit/config';
2
- export { AIConfig, AIProvider, Audience, BitbucketConfig, ConfluenceConfig, CullConfig, EnrichmentType, GitLabConfig, JiraConfig, LinearConfig, NotionConfig, OpenClawConfig, OutputFormat, PublishTarget, PublisherType, RepoSource, SourceConfig, Tone } from '@cullit/config';
2
+ export { AIConfig, AIProvider, Audience, BitbucketConfig, ConfluenceConfig, CullConfig, EnrichmentType, GitLabConfig, JiraConfig, LinearConfig, NotionConfig, OpenClawConfig, OutputFormat, PublishTarget, PublisherType, RepoSource, SourceConfig, TemplateConfig, TemplateProfile, Tone } from '@cullit/config';
3
3
 
4
4
  interface GitCommit {
5
5
  hash: string;
@@ -71,7 +71,7 @@ interface PipelineResult {
71
71
  duration: number;
72
72
  }
73
73
 
74
- declare const VERSION = "1.8.0";
74
+ declare const VERSION = "1.10.0";
75
75
  declare const DEFAULT_CATEGORIES: string[];
76
76
  declare const DEFAULT_MODELS: Record<string, string>;
77
77
  declare const AI_PROVIDERS: readonly ["anthropic", "openai", "gemini", "ollama", "openclaw", "none"];
@@ -159,6 +159,7 @@ declare function registerFormatter(format: string, fn: FormatterFn): void;
159
159
  declare function getFormatter(format: string): FormatterFn | undefined;
160
160
  declare function listFormatters(): string[];
161
161
  declare function formatNotes(notes: ReleaseNotes, format: OutputFormat): string;
162
+ declare function escapeHtml(str: string): string;
162
163
 
163
164
  /**
164
165
  * Outputs release notes to stdout (default).
@@ -218,7 +219,7 @@ declare function analyzeReleaseReadiness(cwd?: string): ReleaseAdvisory;
218
219
  * validateLicense() performs async remote validation with caching.
219
220
  * resolveLicense() remains sync for quick format-only checks (display).
220
221
  */
221
- type LicenseTier = 'free' | 'pro';
222
+ type LicenseTier = 'free' | 'pro' | 'team' | 'enterprise';
222
223
  interface LicenseStatus {
223
224
  tier: LicenseTier;
224
225
  valid: boolean;
@@ -241,6 +242,7 @@ declare function validateLicense(): Promise<LicenseStatus>;
241
242
  declare function isProviderAllowed(provider: string, license: LicenseStatus): boolean;
242
243
  /**
243
244
  * Check whether the current license allows the requested publisher.
245
+ * Confluence, Notion, and Teams require Team tier or above.
244
246
  */
245
247
  declare function isPublisherAllowed(publisherType: string, license: LicenseStatus): boolean;
246
248
  /**
@@ -259,6 +261,15 @@ interface UsageLimits {
259
261
  * Get usage limits for a license tier.
260
262
  */
261
263
  declare function getTierLimits(tier: string): UsageLimits;
264
+ type TeamFeature = 'drafts' | 'approvals' | 'shared_history' | 'project_templates' | 'hosted_changelog' | 'branded_widget' | 'team_publishers' | 'org_settings' | 'audit_logs' | 'sso';
265
+ /**
266
+ * Check whether a license tier grants access to a Team/Enterprise feature.
267
+ */
268
+ declare function isFeatureAllowed(feature: TeamFeature, tier: string): boolean;
269
+ /**
270
+ * Build a gating summary for a tier — which features are unlocked.
271
+ */
272
+ declare function getFeatureGating(tier: string): Record<TeamFeature, boolean>;
262
273
  /**
263
274
  * Report a generation event to the metering service.
264
275
  * Non-blocking — failures are logged but never block the pipeline.
@@ -271,7 +282,7 @@ declare function reportUsage(project?: string): Promise<void>;
271
282
  * Core registers: git collector, template generator, stdout/file publishers.
272
283
  * Pro registers: AI generator, Jira/Linear collectors + enrichers, Slack/Discord/GitHub publishers.
273
284
  *
274
- * The CLI calls `await import('@cullit/pro')` to load pro plugins if installed.
285
+ * Paid plugin registration is preloaded by the licensed distribution package.
275
286
  */
276
287
 
277
288
  type CollectorFactory = (...args: any[]) => Collector;
@@ -308,6 +319,7 @@ declare function runPipeline(from: string, to: string, config: CullConfig, optio
308
319
  format?: OutputFormat;
309
320
  dryRun?: boolean;
310
321
  logger?: Logger;
322
+ templateProfile?: string;
311
323
  }): Promise<PipelineResult>;
312
324
 
313
- export { AI_PROVIDERS, AUDIENCES, CHANGE_CATEGORIES, type ChangeCategory, type ChangeEntry, type Collector, type CollectorFactory, DEFAULT_CATEGORIES, DEFAULT_MODELS, ENRICHMENT_TYPES, type EnrichedContext, type EnrichedTicket, type Enricher, type EnricherFactory, FilePublisher, type Generator, type GeneratorFactory, GitCollector, type GitCommit, type GitDiff, type LicenseStatus, type LicenseTier, type LogLevel, type Logger, MultiRepoCollector, OUTPUT_FORMATS, PUBLISHER_TYPES, type PipelineResult, type Publisher, type PublisherFactory, type ReleaseAdvisory, type ReleaseNotes, SOURCE_TYPES, type SemverBump, StdoutPublisher, TONES, TemplateGenerator, type UsageLimits, VERSION, analyzeReleaseReadiness, createLogger, fetchWithTimeout, formatNotes, getCollector, getEnricher, getFormatter, getGenerator, getLatestTag, getPublisher, getRecentTags, getTierLimits, hasCollector, hasEnricher, hasGenerator, hasPublisher, isEnrichmentAllowed, isProviderAllowed, isPublisherAllowed, listCollectors, listEnrichers, listFormatters, listGenerators, listPublishers, registerCollector, registerEnricher, registerFormatter, registerGenerator, registerPublisher, reportUsage, resolveLicense, runPipeline, upgradeMessage, validateLicense };
325
+ export { AI_PROVIDERS, AUDIENCES, CHANGE_CATEGORIES, type ChangeCategory, type ChangeEntry, type Collector, type CollectorFactory, DEFAULT_CATEGORIES, DEFAULT_MODELS, ENRICHMENT_TYPES, type EnrichedContext, type EnrichedTicket, type Enricher, type EnricherFactory, FilePublisher, type Generator, type GeneratorFactory, GitCollector, type GitCommit, type GitDiff, type LicenseStatus, type LicenseTier, type LogLevel, type Logger, MultiRepoCollector, OUTPUT_FORMATS, PUBLISHER_TYPES, type PipelineResult, type Publisher, type PublisherFactory, type ReleaseAdvisory, type ReleaseNotes, SOURCE_TYPES, type SemverBump, StdoutPublisher, TONES, type TeamFeature, TemplateGenerator, type UsageLimits, VERSION, analyzeReleaseReadiness, createLogger, escapeHtml, fetchWithTimeout, formatNotes, getCollector, getEnricher, getFeatureGating, getFormatter, getGenerator, getLatestTag, getPublisher, getRecentTags, getTierLimits, hasCollector, hasEnricher, hasGenerator, hasPublisher, isEnrichmentAllowed, isFeatureAllowed, isProviderAllowed, isPublisherAllowed, listCollectors, listEnrichers, listFormatters, listGenerators, listPublishers, registerCollector, registerEnricher, registerFormatter, registerGenerator, registerPublisher, reportUsage, resolveLicense, runPipeline, upgradeMessage, validateLicense };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/constants.ts
2
- var VERSION = "1.8.0";
2
+ var VERSION = "1.10.0";
3
3
  var DEFAULT_CATEGORIES = ["features", "fixes", "breaking", "improvements", "chores"];
4
4
  var DEFAULT_MODELS = {
5
5
  anthropic: "claude-sonnet-4-20250514",
@@ -72,7 +72,8 @@ var GitCollector = class {
72
72
  { cwd: this.cwd, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
73
73
  );
74
74
  } catch (error) {
75
- const stderr = error?.stderr?.toString?.() || "";
75
+ const errWithStderr = typeof error === "object" && error !== null && "stderr" in error ? error : void 0;
76
+ const stderr = errWithStderr?.stderr?.toString?.() || "";
76
77
  const hint = stderr.includes("unknown revision") ? 'Check that both refs exist (run "cullit tags" to see tags).' : stderr.includes("not a git repository") ? "Run this command inside a git repository." : `Make sure both refs exist and you're in a git repository.`;
77
78
  throw new Error(
78
79
  `Failed to read git log between ${from} and ${to}. ${hint}`
@@ -417,12 +418,15 @@ function formatNotes(notes, format) {
417
418
  const fn = formatters.get(format) || formatters.get("markdown");
418
419
  return fn(notes);
419
420
  }
421
+ function sanitizeForMarkdown(text) {
422
+ return escapeHtml(text).replace(/\r?\n/g, " ");
423
+ }
420
424
  function formatMarkdown(notes) {
421
425
  const lines = [];
422
- lines.push(`## ${notes.version} \u2014 ${notes.date}`);
426
+ lines.push(`## ${sanitizeForMarkdown(notes.version)} \u2014 ${sanitizeForMarkdown(notes.date)}`);
423
427
  lines.push("");
424
428
  if (notes.summary) {
425
- lines.push(notes.summary);
429
+ lines.push(sanitizeForMarkdown(notes.summary));
426
430
  lines.push("");
427
431
  }
428
432
  const grouped = groupByCategory(notes);
@@ -432,8 +436,8 @@ function formatMarkdown(notes) {
432
436
  lines.push(`### ${CATEGORY_LABELS[category]}`);
433
437
  lines.push("");
434
438
  for (const entry of entries) {
435
- let line = `- ${entry.description}`;
436
- if (entry.ticketKey) line += ` (${entry.ticketKey})`;
439
+ let line = `- ${sanitizeForMarkdown(entry.description)}`;
440
+ if (entry.ticketKey) line += ` (${sanitizeForMarkdown(entry.ticketKey)})`;
437
441
  lines.push(line);
438
442
  }
439
443
  lines.push("");
@@ -441,7 +445,7 @@ function formatMarkdown(notes) {
441
445
  if (notes.contributors?.length) {
442
446
  lines.push("### Contributors");
443
447
  lines.push("");
444
- lines.push(notes.contributors.map((c) => `@${c}`).join(", "));
448
+ lines.push(notes.contributors.map((c) => `@${sanitizeForMarkdown(c)}`).join(", "));
445
449
  lines.push("");
446
450
  }
447
451
  if (notes.metadata) {
@@ -713,6 +717,7 @@ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT) {
713
717
  // src/gate.ts
714
718
  var FREE_PROVIDERS = /* @__PURE__ */ new Set(["none"]);
715
719
  var FREE_PUBLISHERS = /* @__PURE__ */ new Set(["stdout", "file"]);
720
+ var TEAM_ONLY_PUBLISHERS = /* @__PURE__ */ new Set(["confluence", "notion", "teams"]);
716
721
  var LICENSE_CACHE_TTL = 24 * 60 * 60 * 1e3;
717
722
  var LICENSE_FAILURE_CACHE_TTL = 5 * 60 * 1e3;
718
723
  var cachedValidation = null;
@@ -750,7 +755,7 @@ async function validateLicense() {
750
755
  if (res.ok) {
751
756
  const data = await res.json();
752
757
  const status2 = {
753
- tier: data.tier === "pro" ? "pro" : "free",
758
+ tier: data.tier === "team" || data.tier === "enterprise" ? data.tier : data.tier === "pro" ? "pro" : "free",
754
759
  valid: data.valid !== false,
755
760
  message: data.message
756
761
  };
@@ -768,19 +773,22 @@ async function validateLicense() {
768
773
  if (cachedValidation && cachedValidation.key === key) {
769
774
  return cachedValidation.status;
770
775
  }
771
- return { tier: "pro", valid: true, message: "Offline validation \u2014 using cached license." };
776
+ return { tier: "free", valid: true, message: "License validation unavailable offline. Run while connected to activate your Pro license." };
772
777
  }
773
778
  }
774
779
  function isProviderAllowed(provider, license) {
775
- if (license.tier === "pro" && license.valid) return true;
780
+ if (license.tier !== "free" && license.valid) return true;
776
781
  return FREE_PROVIDERS.has(provider);
777
782
  }
778
783
  function isPublisherAllowed(publisherType, license) {
779
- if (license.tier === "pro" && license.valid) return true;
784
+ if (TEAM_ONLY_PUBLISHERS.has(publisherType)) {
785
+ return (license.tier === "team" || license.tier === "enterprise") && license.valid;
786
+ }
787
+ if (license.tier !== "free" && license.valid) return true;
780
788
  return FREE_PUBLISHERS.has(publisherType);
781
789
  }
782
790
  function isEnrichmentAllowed(license) {
783
- return license.tier === "pro" && license.valid;
791
+ return license.tier !== "free" && license.valid;
784
792
  }
785
793
  function upgradeMessage(feature) {
786
794
  return `\u{1F512} ${feature} requires a Cullit Pro license.
@@ -788,14 +796,37 @@ function upgradeMessage(feature) {
788
796
  Then set CULLIT_API_KEY in your environment.`;
789
797
  }
790
798
  var TIER_LIMITS = {
791
- free: { generationsPerMonth: 3, maxProjects: 1 },
792
- pro: { generationsPerMonth: 500, maxProjects: 5 },
793
- team: { generationsPerMonth: 2e3, maxProjects: 25 },
799
+ free: { generationsPerMonth: 5, maxProjects: 3 },
800
+ pro: { generationsPerMonth: 500, maxProjects: 100 },
801
+ team: { generationsPerMonth: 2e3, maxProjects: 250 },
794
802
  enterprise: { generationsPerMonth: Infinity, maxProjects: Infinity }
795
803
  };
796
804
  function getTierLimits(tier) {
797
805
  return TIER_LIMITS[tier] || TIER_LIMITS.free;
798
806
  }
807
+ var FEATURE_TIERS = {
808
+ drafts: /* @__PURE__ */ new Set(["team", "enterprise"]),
809
+ approvals: /* @__PURE__ */ new Set(["team", "enterprise"]),
810
+ shared_history: /* @__PURE__ */ new Set(["team", "enterprise"]),
811
+ project_templates: /* @__PURE__ */ new Set(["team", "enterprise"]),
812
+ hosted_changelog: /* @__PURE__ */ new Set(["pro", "team", "enterprise"]),
813
+ branded_widget: /* @__PURE__ */ new Set(["team", "enterprise"]),
814
+ team_publishers: /* @__PURE__ */ new Set(["team", "enterprise"]),
815
+ org_settings: /* @__PURE__ */ new Set(["team", "enterprise"]),
816
+ audit_logs: /* @__PURE__ */ new Set(["enterprise"]),
817
+ sso: /* @__PURE__ */ new Set(["enterprise"])
818
+ };
819
+ function isFeatureAllowed(feature, tier) {
820
+ const allowed = FEATURE_TIERS[feature];
821
+ return allowed ? allowed.has(tier) : false;
822
+ }
823
+ function getFeatureGating(tier) {
824
+ const result = {};
825
+ for (const feature of Object.keys(FEATURE_TIERS)) {
826
+ result[feature] = isFeatureAllowed(feature, tier);
827
+ }
828
+ return result;
829
+ }
799
830
  async function reportUsage(project = "default") {
800
831
  const key = process.env.CULLIT_API_KEY?.trim();
801
832
  const meterUrl = process.env.CULLIT_METER_URL?.trim();
@@ -880,13 +911,77 @@ registerCollector("multi-repo", (config) => {
880
911
  registerGenerator("none", () => new TemplateGenerator());
881
912
  registerPublisher("stdout", (_target) => new StdoutPublisher());
882
913
  registerPublisher("file", (target) => new FilePublisher(target.path));
914
+ function normalizeSectionOrder(value) {
915
+ if (!Array.isArray(value)) return void 0;
916
+ const order = value.filter((v) => typeof v === "string").map((v) => v.trim()).filter(Boolean);
917
+ return order.length ? order : void 0;
918
+ }
919
+ function toResolvedTemplate(profile) {
920
+ if (!profile) return {};
921
+ const p = profile;
922
+ return {
923
+ format: typeof p.format === "string" ? p.format : void 0,
924
+ sectionOrder: normalizeSectionOrder(p.sectionOrder),
925
+ includeContributors: typeof p.includeContributors === "boolean" ? p.includeContributors : void 0,
926
+ includeMetadata: typeof p.includeMetadata === "boolean" ? p.includeMetadata : void 0,
927
+ summaryPrefix: typeof p.summaryPrefix === "string" ? p.summaryPrefix : void 0
928
+ };
929
+ }
930
+ function mergeTemplates(...templates) {
931
+ const merged = {};
932
+ for (const template of templates) {
933
+ if (!template) continue;
934
+ if (template.name) merged.name = template.name;
935
+ if (template.format) merged.format = template.format;
936
+ if (template.sectionOrder) merged.sectionOrder = template.sectionOrder;
937
+ if (typeof template.includeContributors === "boolean") merged.includeContributors = template.includeContributors;
938
+ if (typeof template.includeMetadata === "boolean") merged.includeMetadata = template.includeMetadata;
939
+ if (typeof template.summaryPrefix === "string") merged.summaryPrefix = template.summaryPrefix;
940
+ }
941
+ return merged;
942
+ }
943
+ function getTemplateProfileByName(config, name) {
944
+ if (!name) return void 0;
945
+ const profile = config.templates?.find((t) => t.name === name);
946
+ if (!profile) return void 0;
947
+ return { name, ...toResolvedTemplate(profile) };
948
+ }
949
+ function applyTemplateToNotes(notes, template) {
950
+ const next = {
951
+ ...notes,
952
+ changes: [...notes.changes]
953
+ };
954
+ if (template.sectionOrder?.length) {
955
+ const index = /* @__PURE__ */ new Map();
956
+ template.sectionOrder.forEach((category, i) => index.set(category, i));
957
+ next.changes = next.changes.map((change, i) => ({ change, i })).sort((a, b) => {
958
+ const ai = index.has(a.change.category) ? index.get(a.change.category) : Number.MAX_SAFE_INTEGER;
959
+ const bi = index.has(b.change.category) ? index.get(b.change.category) : Number.MAX_SAFE_INTEGER;
960
+ if (ai !== bi) return ai - bi;
961
+ return a.i - b.i;
962
+ }).map(({ change }) => change);
963
+ }
964
+ if (template.summaryPrefix) {
965
+ const currentSummary = next.summary || "";
966
+ next.summary = `${template.summaryPrefix}${currentSummary ? ` ${currentSummary}` : ""}`.trim();
967
+ }
968
+ if (template.includeContributors === false) {
969
+ delete next.contributors;
970
+ }
971
+ if (template.includeMetadata === false) {
972
+ delete next.metadata;
973
+ }
974
+ return next;
975
+ }
883
976
  async function runPipeline(from, to, config, options = {}) {
884
977
  const startTime = Date.now();
885
- const format = options.format || "markdown";
886
978
  const log = options.logger || createLogger("normal");
887
979
  const license = await validateLicense();
888
980
  if (!license.valid) {
889
- throw new Error(license.message || "Invalid CULLIT_API_KEY");
981
+ if (!isProviderAllowed(config.ai.provider, license)) {
982
+ throw new Error(license.message || "Invalid CULLIT_API_KEY");
983
+ }
984
+ log.warn(`\u26A0 ${license.message || "Invalid CULLIT_API_KEY \u2014 running in free mode."}`);
890
985
  }
891
986
  if (!isProviderAllowed(config.ai.provider, license)) {
892
987
  throw new Error(upgradeMessage(`AI provider "${config.ai.provider}"`));
@@ -894,7 +989,7 @@ async function runPipeline(from, to, config, options = {}) {
894
989
  const collectorFactory = getCollector(config.source.type);
895
990
  if (!collectorFactory) {
896
991
  throw new Error(
897
- `Source type "${config.source.type}" is not available. ` + (config.source.type !== "local" ? "Install @cullit/pro to use this source." : "Valid sources: local")
992
+ `Source type "${config.source.type}" is not available. ` + (config.source.type !== "local" ? "Install @cullit/licensed (private distribution) to use this source." : "Valid sources: local")
898
993
  );
899
994
  }
900
995
  const sourceLabel = config.source.type === "local" ? `commits between ${from}..${to}` : `items from ${config.source.type}`;
@@ -916,7 +1011,7 @@ async function runPipeline(from, to, config, options = {}) {
916
1011
  }
917
1012
  const enricherFactory = getEnricher(source);
918
1013
  if (!enricherFactory) {
919
- log.info(`\xBB Skipping ${source} enrichment \u2014 install @cullit/pro to enable`);
1014
+ log.info(`\xBB Skipping ${source} enrichment \u2014 install @cullit/licensed to enable`);
920
1015
  continue;
921
1016
  }
922
1017
  log.info(`\xBB Enriching from ${source}...`);
@@ -940,7 +1035,7 @@ async function runPipeline(from, to, config, options = {}) {
940
1035
  const generatorFactory = getGenerator(config.ai.provider);
941
1036
  if (!generatorFactory) {
942
1037
  throw new Error(
943
- `AI provider "${config.ai.provider}" is not available. ` + (config.ai.provider !== "none" ? "Install @cullit/pro to use AI providers." : "")
1038
+ `AI provider "${config.ai.provider}" is not available. ` + (config.ai.provider !== "none" ? "Install @cullit/licensed (private distribution) to use AI providers." : "")
944
1039
  );
945
1040
  }
946
1041
  let generator;
@@ -951,8 +1046,16 @@ async function runPipeline(from, to, config, options = {}) {
951
1046
  }
952
1047
  const notes = await generator.generate(context, config.ai);
953
1048
  log.info(`\xBB Generated ${notes.changes.length} change entries`);
954
- const formatted = formatNotes(notes, format);
1049
+ const selectedTemplateName = options.templateProfile || config.template?.default;
1050
+ const baseTemplate = mergeTemplates(
1051
+ toResolvedTemplate(config.template),
1052
+ getTemplateProfileByName(config, selectedTemplateName)
1053
+ );
1054
+ const format = options.format || baseTemplate.format || "markdown";
1055
+ const templatedNotes = applyTemplateToNotes(notes, baseTemplate);
1056
+ const formatted = formatNotes(templatedNotes, format);
955
1057
  const publishedTo = [];
1058
+ const renderedCache = /* @__PURE__ */ new Map();
956
1059
  if (!options.dryRun) {
957
1060
  for (const target of config.publish) {
958
1061
  try {
@@ -962,11 +1065,40 @@ async function runPipeline(from, to, config, options = {}) {
962
1065
  }
963
1066
  const publisherFactory = getPublisher(target.type);
964
1067
  if (!publisherFactory) {
965
- log.info(`\xBB Skipping ${target.type} \u2014 install @cullit/pro to enable`);
1068
+ log.info(`\xBB Skipping ${target.type} \u2014 install @cullit/licensed to enable`);
966
1069
  continue;
967
1070
  }
968
1071
  const publisher = publisherFactory(target);
969
- await publisher.publish(notes, format, formatted);
1072
+ const targetTemplate = mergeTemplates(
1073
+ baseTemplate,
1074
+ getTemplateProfileByName(config, typeof target.templateProfile === "string" ? target.templateProfile : void 0),
1075
+ {
1076
+ format: typeof target.format === "string" ? target.format : void 0,
1077
+ sectionOrder: normalizeSectionOrder(target.sectionOrder)
1078
+ }
1079
+ );
1080
+ const targetFormat = targetTemplate.format || format;
1081
+ const cacheKey = JSON.stringify({
1082
+ f: targetFormat,
1083
+ o: targetTemplate.sectionOrder || null,
1084
+ c: targetTemplate.includeContributors,
1085
+ m: targetTemplate.includeMetadata,
1086
+ s: targetTemplate.summaryPrefix
1087
+ });
1088
+ let cached = renderedCache.get(cacheKey);
1089
+ if (!cached) {
1090
+ const targetNotes = applyTemplateToNotes(notes, targetTemplate);
1091
+ const targetOutput = cacheKey === JSON.stringify({
1092
+ f: format,
1093
+ o: baseTemplate.sectionOrder || null,
1094
+ c: baseTemplate.includeContributors,
1095
+ m: baseTemplate.includeMetadata,
1096
+ s: baseTemplate.summaryPrefix
1097
+ }) ? formatted : formatNotes(targetNotes, targetFormat);
1098
+ cached = { notes: targetNotes, output: targetOutput, format: targetFormat };
1099
+ renderedCache.set(cacheKey, cached);
1100
+ }
1101
+ await publisher.publish(cached.notes, cached.format, cached.output);
970
1102
  publishedTo.push(target.type === "file" ? `file:${target.path}` : target.type);
971
1103
  } catch (err) {
972
1104
  log.error(`\u2717 Failed to publish to ${target.type}: ${err.message}`);
@@ -980,7 +1112,7 @@ async function runPipeline(from, to, config, options = {}) {
980
1112
  const duration = Date.now() - startTime;
981
1113
  log.info(`
982
1114
  \u2713 Done in ${(duration / 1e3).toFixed(1)}s`);
983
- return { notes, formatted, publishedTo, duration };
1115
+ return { notes: templatedNotes, formatted, publishedTo, duration };
984
1116
  }
985
1117
  export {
986
1118
  AI_PROVIDERS,
@@ -1001,10 +1133,12 @@ export {
1001
1133
  VERSION,
1002
1134
  analyzeReleaseReadiness,
1003
1135
  createLogger,
1136
+ escapeHtml,
1004
1137
  fetchWithTimeout,
1005
1138
  formatNotes,
1006
1139
  getCollector,
1007
1140
  getEnricher,
1141
+ getFeatureGating,
1008
1142
  getFormatter,
1009
1143
  getGenerator,
1010
1144
  getLatestTag,
@@ -1016,6 +1150,7 @@ export {
1016
1150
  hasGenerator,
1017
1151
  hasPublisher,
1018
1152
  isEnrichmentAllowed,
1153
+ isFeatureAllowed,
1019
1154
  isProviderAllowed,
1020
1155
  isPublisherAllowed,
1021
1156
  listCollectors,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cullit/core",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "type": "module",
5
5
  "description": "Core engine for Cullit — AI-powered release note generation.",
6
6
  "license": "MIT",
@@ -16,6 +16,10 @@
16
16
  "ai",
17
17
  "automation"
18
18
  ],
19
+ "homepage": "https://cullit.io",
20
+ "bugs": {
21
+ "url": "https://github.com/mttaylor/cullit/issues"
22
+ },
19
23
  "main": "./dist/index.js",
20
24
  "types": "./dist/index.d.ts",
21
25
  "exports": {
@@ -28,10 +32,13 @@
28
32
  "dist"
29
33
  ],
30
34
  "engines": {
31
- "node": ">=18"
35
+ "node": ">=22"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
32
39
  },
33
40
  "dependencies": {
34
- "@cullit/config": "1.8.0"
41
+ "@cullit/config": "1.10.0"
35
42
  },
36
43
  "scripts": {
37
44
  "build": "tsup src/index.ts --format esm --dts --clean",