@cullit/core 1.8.0 → 1.9.2

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.9.2";
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"];
@@ -218,7 +218,7 @@ declare function analyzeReleaseReadiness(cwd?: string): ReleaseAdvisory;
218
218
  * validateLicense() performs async remote validation with caching.
219
219
  * resolveLicense() remains sync for quick format-only checks (display).
220
220
  */
221
- type LicenseTier = 'free' | 'pro';
221
+ type LicenseTier = 'free' | 'pro' | 'team' | 'enterprise';
222
222
  interface LicenseStatus {
223
223
  tier: LicenseTier;
224
224
  valid: boolean;
@@ -241,6 +241,7 @@ declare function validateLicense(): Promise<LicenseStatus>;
241
241
  declare function isProviderAllowed(provider: string, license: LicenseStatus): boolean;
242
242
  /**
243
243
  * Check whether the current license allows the requested publisher.
244
+ * Confluence, Notion, and Teams require Team tier or above.
244
245
  */
245
246
  declare function isPublisherAllowed(publisherType: string, license: LicenseStatus): boolean;
246
247
  /**
@@ -259,6 +260,15 @@ interface UsageLimits {
259
260
  * Get usage limits for a license tier.
260
261
  */
261
262
  declare function getTierLimits(tier: string): UsageLimits;
263
+ type TeamFeature = 'drafts' | 'approvals' | 'shared_history' | 'project_templates' | 'hosted_changelog' | 'branded_widget' | 'team_publishers' | 'org_settings' | 'audit_logs' | 'sso';
264
+ /**
265
+ * Check whether a license tier grants access to a Team/Enterprise feature.
266
+ */
267
+ declare function isFeatureAllowed(feature: TeamFeature, tier: string): boolean;
268
+ /**
269
+ * Build a gating summary for a tier — which features are unlocked.
270
+ */
271
+ declare function getFeatureGating(tier: string): Record<TeamFeature, boolean>;
262
272
  /**
263
273
  * Report a generation event to the metering service.
264
274
  * Non-blocking — failures are logged but never block the pipeline.
@@ -308,6 +318,7 @@ declare function runPipeline(from: string, to: string, config: CullConfig, optio
308
318
  format?: OutputFormat;
309
319
  dryRun?: boolean;
310
320
  logger?: Logger;
321
+ templateProfile?: string;
311
322
  }): Promise<PipelineResult>;
312
323
 
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 };
324
+ 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, 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.9.2";
3
3
  var DEFAULT_CATEGORIES = ["features", "fixes", "breaking", "improvements", "chores"];
4
4
  var DEFAULT_MODELS = {
5
5
  anthropic: "claude-sonnet-4-20250514",
@@ -417,12 +417,15 @@ function formatNotes(notes, format) {
417
417
  const fn = formatters.get(format) || formatters.get("markdown");
418
418
  return fn(notes);
419
419
  }
420
+ function escapeMarkdown(text) {
421
+ return escapeHtml(text).replace(/\r?\n/g, " ");
422
+ }
420
423
  function formatMarkdown(notes) {
421
424
  const lines = [];
422
- lines.push(`## ${notes.version} \u2014 ${notes.date}`);
425
+ lines.push(`## ${escapeMarkdown(notes.version)} \u2014 ${escapeMarkdown(notes.date)}`);
423
426
  lines.push("");
424
427
  if (notes.summary) {
425
- lines.push(notes.summary);
428
+ lines.push(escapeMarkdown(notes.summary));
426
429
  lines.push("");
427
430
  }
428
431
  const grouped = groupByCategory(notes);
@@ -432,8 +435,8 @@ function formatMarkdown(notes) {
432
435
  lines.push(`### ${CATEGORY_LABELS[category]}`);
433
436
  lines.push("");
434
437
  for (const entry of entries) {
435
- let line = `- ${entry.description}`;
436
- if (entry.ticketKey) line += ` (${entry.ticketKey})`;
438
+ let line = `- ${escapeMarkdown(entry.description)}`;
439
+ if (entry.ticketKey) line += ` (${escapeMarkdown(entry.ticketKey)})`;
437
440
  lines.push(line);
438
441
  }
439
442
  lines.push("");
@@ -441,7 +444,7 @@ function formatMarkdown(notes) {
441
444
  if (notes.contributors?.length) {
442
445
  lines.push("### Contributors");
443
446
  lines.push("");
444
- lines.push(notes.contributors.map((c) => `@${c}`).join(", "));
447
+ lines.push(notes.contributors.map((c) => `@${escapeMarkdown(c)}`).join(", "));
445
448
  lines.push("");
446
449
  }
447
450
  if (notes.metadata) {
@@ -713,6 +716,7 @@ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT) {
713
716
  // src/gate.ts
714
717
  var FREE_PROVIDERS = /* @__PURE__ */ new Set(["none"]);
715
718
  var FREE_PUBLISHERS = /* @__PURE__ */ new Set(["stdout", "file"]);
719
+ var TEAM_ONLY_PUBLISHERS = /* @__PURE__ */ new Set(["confluence", "notion", "teams"]);
716
720
  var LICENSE_CACHE_TTL = 24 * 60 * 60 * 1e3;
717
721
  var LICENSE_FAILURE_CACHE_TTL = 5 * 60 * 1e3;
718
722
  var cachedValidation = null;
@@ -750,7 +754,7 @@ async function validateLicense() {
750
754
  if (res.ok) {
751
755
  const data = await res.json();
752
756
  const status2 = {
753
- tier: data.tier === "pro" ? "pro" : "free",
757
+ tier: data.tier === "team" || data.tier === "enterprise" ? data.tier : data.tier === "pro" ? "pro" : "free",
754
758
  valid: data.valid !== false,
755
759
  message: data.message
756
760
  };
@@ -772,15 +776,18 @@ async function validateLicense() {
772
776
  }
773
777
  }
774
778
  function isProviderAllowed(provider, license) {
775
- if (license.tier === "pro" && license.valid) return true;
779
+ if (license.tier !== "free" && license.valid) return true;
776
780
  return FREE_PROVIDERS.has(provider);
777
781
  }
778
782
  function isPublisherAllowed(publisherType, license) {
779
- if (license.tier === "pro" && license.valid) return true;
783
+ if (TEAM_ONLY_PUBLISHERS.has(publisherType)) {
784
+ return (license.tier === "team" || license.tier === "enterprise") && license.valid;
785
+ }
786
+ if (license.tier !== "free" && license.valid) return true;
780
787
  return FREE_PUBLISHERS.has(publisherType);
781
788
  }
782
789
  function isEnrichmentAllowed(license) {
783
- return license.tier === "pro" && license.valid;
790
+ return license.tier !== "free" && license.valid;
784
791
  }
785
792
  function upgradeMessage(feature) {
786
793
  return `\u{1F512} ${feature} requires a Cullit Pro license.
@@ -788,14 +795,37 @@ function upgradeMessage(feature) {
788
795
  Then set CULLIT_API_KEY in your environment.`;
789
796
  }
790
797
  var TIER_LIMITS = {
791
- free: { generationsPerMonth: 3, maxProjects: 1 },
792
- pro: { generationsPerMonth: 500, maxProjects: 5 },
793
- team: { generationsPerMonth: 2e3, maxProjects: 25 },
798
+ free: { generationsPerMonth: 5, maxProjects: 3 },
799
+ pro: { generationsPerMonth: 500, maxProjects: 100 },
800
+ team: { generationsPerMonth: 2e3, maxProjects: 250 },
794
801
  enterprise: { generationsPerMonth: Infinity, maxProjects: Infinity }
795
802
  };
796
803
  function getTierLimits(tier) {
797
804
  return TIER_LIMITS[tier] || TIER_LIMITS.free;
798
805
  }
806
+ var FEATURE_TIERS = {
807
+ drafts: /* @__PURE__ */ new Set(["team", "enterprise"]),
808
+ approvals: /* @__PURE__ */ new Set(["team", "enterprise"]),
809
+ shared_history: /* @__PURE__ */ new Set(["team", "enterprise"]),
810
+ project_templates: /* @__PURE__ */ new Set(["team", "enterprise"]),
811
+ hosted_changelog: /* @__PURE__ */ new Set(["pro", "team", "enterprise"]),
812
+ branded_widget: /* @__PURE__ */ new Set(["team", "enterprise"]),
813
+ team_publishers: /* @__PURE__ */ new Set(["team", "enterprise"]),
814
+ org_settings: /* @__PURE__ */ new Set(["team", "enterprise"]),
815
+ audit_logs: /* @__PURE__ */ new Set(["enterprise"]),
816
+ sso: /* @__PURE__ */ new Set(["enterprise"])
817
+ };
818
+ function isFeatureAllowed(feature, tier) {
819
+ const allowed = FEATURE_TIERS[feature];
820
+ return allowed ? allowed.has(tier) : false;
821
+ }
822
+ function getFeatureGating(tier) {
823
+ const result = {};
824
+ for (const feature of Object.keys(FEATURE_TIERS)) {
825
+ result[feature] = isFeatureAllowed(feature, tier);
826
+ }
827
+ return result;
828
+ }
799
829
  async function reportUsage(project = "default") {
800
830
  const key = process.env.CULLIT_API_KEY?.trim();
801
831
  const meterUrl = process.env.CULLIT_METER_URL?.trim();
@@ -880,9 +910,70 @@ registerCollector("multi-repo", (config) => {
880
910
  registerGenerator("none", () => new TemplateGenerator());
881
911
  registerPublisher("stdout", (_target) => new StdoutPublisher());
882
912
  registerPublisher("file", (target) => new FilePublisher(target.path));
913
+ function normalizeSectionOrder(value) {
914
+ if (!Array.isArray(value)) return void 0;
915
+ const order = value.filter((v) => typeof v === "string").map((v) => v.trim()).filter(Boolean);
916
+ return order.length ? order : void 0;
917
+ }
918
+ function toResolvedTemplate(profile) {
919
+ if (!profile) return {};
920
+ const p = profile;
921
+ return {
922
+ format: typeof p.format === "string" ? p.format : void 0,
923
+ sectionOrder: normalizeSectionOrder(p.sectionOrder),
924
+ includeContributors: typeof p.includeContributors === "boolean" ? p.includeContributors : void 0,
925
+ includeMetadata: typeof p.includeMetadata === "boolean" ? p.includeMetadata : void 0,
926
+ summaryPrefix: typeof p.summaryPrefix === "string" ? p.summaryPrefix : void 0
927
+ };
928
+ }
929
+ function mergeTemplates(...templates) {
930
+ const merged = {};
931
+ for (const template of templates) {
932
+ if (!template) continue;
933
+ if (template.name) merged.name = template.name;
934
+ if (template.format) merged.format = template.format;
935
+ if (template.sectionOrder) merged.sectionOrder = template.sectionOrder;
936
+ if (typeof template.includeContributors === "boolean") merged.includeContributors = template.includeContributors;
937
+ if (typeof template.includeMetadata === "boolean") merged.includeMetadata = template.includeMetadata;
938
+ if (typeof template.summaryPrefix === "string") merged.summaryPrefix = template.summaryPrefix;
939
+ }
940
+ return merged;
941
+ }
942
+ function getTemplateProfileByName(config, name) {
943
+ if (!name) return void 0;
944
+ const profile = config.templates?.find((t) => t.name === name);
945
+ if (!profile) return void 0;
946
+ return { name, ...toResolvedTemplate(profile) };
947
+ }
948
+ function applyTemplateToNotes(notes, template) {
949
+ const next = {
950
+ ...notes,
951
+ changes: [...notes.changes]
952
+ };
953
+ if (template.sectionOrder?.length) {
954
+ const index = /* @__PURE__ */ new Map();
955
+ template.sectionOrder.forEach((category, i) => index.set(category, i));
956
+ next.changes = next.changes.map((change, i) => ({ change, i })).sort((a, b) => {
957
+ const ai = index.has(a.change.category) ? index.get(a.change.category) : Number.MAX_SAFE_INTEGER;
958
+ const bi = index.has(b.change.category) ? index.get(b.change.category) : Number.MAX_SAFE_INTEGER;
959
+ if (ai !== bi) return ai - bi;
960
+ return a.i - b.i;
961
+ }).map(({ change }) => change);
962
+ }
963
+ if (template.summaryPrefix) {
964
+ const currentSummary = next.summary || "";
965
+ next.summary = `${template.summaryPrefix}${currentSummary ? ` ${currentSummary}` : ""}`.trim();
966
+ }
967
+ if (template.includeContributors === false) {
968
+ delete next.contributors;
969
+ }
970
+ if (template.includeMetadata === false) {
971
+ delete next.metadata;
972
+ }
973
+ return next;
974
+ }
883
975
  async function runPipeline(from, to, config, options = {}) {
884
976
  const startTime = Date.now();
885
- const format = options.format || "markdown";
886
977
  const log = options.logger || createLogger("normal");
887
978
  const license = await validateLicense();
888
979
  if (!license.valid) {
@@ -951,8 +1042,16 @@ async function runPipeline(from, to, config, options = {}) {
951
1042
  }
952
1043
  const notes = await generator.generate(context, config.ai);
953
1044
  log.info(`\xBB Generated ${notes.changes.length} change entries`);
954
- const formatted = formatNotes(notes, format);
1045
+ const selectedTemplateName = options.templateProfile || config.template?.default;
1046
+ const baseTemplate = mergeTemplates(
1047
+ toResolvedTemplate(config.template),
1048
+ getTemplateProfileByName(config, selectedTemplateName)
1049
+ );
1050
+ const format = options.format || baseTemplate.format || "markdown";
1051
+ const templatedNotes = applyTemplateToNotes(notes, baseTemplate);
1052
+ const formatted = formatNotes(templatedNotes, format);
955
1053
  const publishedTo = [];
1054
+ const renderedCache = /* @__PURE__ */ new Map();
956
1055
  if (!options.dryRun) {
957
1056
  for (const target of config.publish) {
958
1057
  try {
@@ -966,7 +1065,36 @@ async function runPipeline(from, to, config, options = {}) {
966
1065
  continue;
967
1066
  }
968
1067
  const publisher = publisherFactory(target);
969
- await publisher.publish(notes, format, formatted);
1068
+ const targetTemplate = mergeTemplates(
1069
+ baseTemplate,
1070
+ getTemplateProfileByName(config, typeof target.templateProfile === "string" ? target.templateProfile : void 0),
1071
+ {
1072
+ format: typeof target.format === "string" ? target.format : void 0,
1073
+ sectionOrder: normalizeSectionOrder(target.sectionOrder)
1074
+ }
1075
+ );
1076
+ const targetFormat = targetTemplate.format || format;
1077
+ const cacheKey = JSON.stringify({
1078
+ f: targetFormat,
1079
+ o: targetTemplate.sectionOrder || null,
1080
+ c: targetTemplate.includeContributors,
1081
+ m: targetTemplate.includeMetadata,
1082
+ s: targetTemplate.summaryPrefix
1083
+ });
1084
+ let cached = renderedCache.get(cacheKey);
1085
+ if (!cached) {
1086
+ const targetNotes = applyTemplateToNotes(notes, targetTemplate);
1087
+ const targetOutput = cacheKey === JSON.stringify({
1088
+ f: format,
1089
+ o: baseTemplate.sectionOrder || null,
1090
+ c: baseTemplate.includeContributors,
1091
+ m: baseTemplate.includeMetadata,
1092
+ s: baseTemplate.summaryPrefix
1093
+ }) ? formatted : formatNotes(targetNotes, targetFormat);
1094
+ cached = { notes: targetNotes, output: targetOutput, format: targetFormat };
1095
+ renderedCache.set(cacheKey, cached);
1096
+ }
1097
+ await publisher.publish(cached.notes, cached.format, cached.output);
970
1098
  publishedTo.push(target.type === "file" ? `file:${target.path}` : target.type);
971
1099
  } catch (err) {
972
1100
  log.error(`\u2717 Failed to publish to ${target.type}: ${err.message}`);
@@ -980,7 +1108,7 @@ async function runPipeline(from, to, config, options = {}) {
980
1108
  const duration = Date.now() - startTime;
981
1109
  log.info(`
982
1110
  \u2713 Done in ${(duration / 1e3).toFixed(1)}s`);
983
- return { notes, formatted, publishedTo, duration };
1111
+ return { notes: templatedNotes, formatted, publishedTo, duration };
984
1112
  }
985
1113
  export {
986
1114
  AI_PROVIDERS,
@@ -1005,6 +1133,7 @@ export {
1005
1133
  formatNotes,
1006
1134
  getCollector,
1007
1135
  getEnricher,
1136
+ getFeatureGating,
1008
1137
  getFormatter,
1009
1138
  getGenerator,
1010
1139
  getLatestTag,
@@ -1016,6 +1145,7 @@ export {
1016
1145
  hasGenerator,
1017
1146
  hasPublisher,
1018
1147
  isEnrichmentAllowed,
1148
+ isFeatureAllowed,
1019
1149
  isProviderAllowed,
1020
1150
  isPublisherAllowed,
1021
1151
  listCollectors,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cullit/core",
3
- "version": "1.8.0",
3
+ "version": "1.9.2",
4
4
  "type": "module",
5
5
  "description": "Core engine for Cullit — AI-powered release note generation.",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "node": ">=18"
32
32
  },
33
33
  "dependencies": {
34
- "@cullit/config": "1.8.0"
34
+ "@cullit/config": "1.9.2"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsup src/index.ts --format esm --dts --clean",