@cullit/core 0.4.0 → 1.0.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, CullConfig } from '@cullit/config';
2
- export { AIConfig, AIProvider, Audience, CullConfig, EnrichmentType, JiraConfig, LinearConfig, OpenClawConfig, OutputFormat, PublishTarget, PublisherType, SourceConfig, Tone } from '@cullit/config';
2
+ export { AIConfig, AIProvider, Audience, BitbucketConfig, ConfluenceConfig, CullConfig, EnrichmentType, GitLabConfig, JiraConfig, LinearConfig, NotionConfig, OpenClawConfig, OutputFormat, PublishTarget, PublisherType, SourceConfig, Tone } from '@cullit/config';
3
3
 
4
4
  interface GitCommit {
5
5
  hash: string;
@@ -31,7 +31,7 @@ interface EnrichedContext {
31
31
  diff: GitDiff;
32
32
  tickets: EnrichedTicket[];
33
33
  }
34
- type ChangeCategory = 'features' | 'fixes' | 'breaking' | 'improvements' | 'chores' | 'other';
34
+ type ChangeCategory = string;
35
35
  interface ChangeEntry {
36
36
  description: string;
37
37
  category: ChangeCategory;
@@ -71,9 +71,17 @@ interface PipelineResult {
71
71
  duration: number;
72
72
  }
73
73
 
74
- declare const VERSION = "0.4.0";
74
+ declare const VERSION = "1.0.0";
75
75
  declare const DEFAULT_CATEGORIES: string[];
76
76
  declare const DEFAULT_MODELS: Record<string, string>;
77
+ declare const AI_PROVIDERS: readonly ["anthropic", "openai", "gemini", "ollama", "openclaw", "none"];
78
+ declare const OUTPUT_FORMATS: readonly ["markdown", "html", "json"];
79
+ declare const PUBLISHER_TYPES: readonly ["stdout", "file", "slack", "discord", "github-release", "teams", "confluence", "notion", "gitlab-release", "changelog"];
80
+ declare const ENRICHMENT_TYPES: readonly ["jira", "linear"];
81
+ declare const CHANGE_CATEGORIES: readonly ["features", "fixes", "breaking", "improvements", "chores", "other"];
82
+ declare const AUDIENCES: readonly ["developer", "end-user", "executive"];
83
+ declare const TONES: readonly ["professional", "casual", "terse"];
84
+ declare const SOURCE_TYPES: readonly ["local", "jira", "linear", "gitlab", "bitbucket"];
77
85
 
78
86
  type LogLevel = 'quiet' | 'normal' | 'verbose';
79
87
  interface Logger {
@@ -129,6 +137,10 @@ declare class TemplateGenerator implements Generator {
129
137
  private buildSummary;
130
138
  }
131
139
 
140
+ type FormatterFn = (notes: ReleaseNotes) => string;
141
+ declare function registerFormatter(format: string, fn: FormatterFn): void;
142
+ declare function getFormatter(format: string): FormatterFn | undefined;
143
+ declare function listFormatters(): string[];
132
144
  declare function formatNotes(notes: ReleaseNotes, format: OutputFormat): string;
133
145
 
134
146
  /**
@@ -185,6 +197,9 @@ declare function analyzeReleaseReadiness(cwd?: string): ReleaseAdvisory;
185
197
  *
186
198
  * Free tier (no key): provider=none, publish to stdout/file only
187
199
  * Pro tier (with key): all providers, all publishers, all enrichments
200
+ *
201
+ * validateLicense() performs async remote validation with caching.
202
+ * resolveLicense() remains sync for quick format-only checks (display).
188
203
  */
189
204
  type LicenseTier = 'free' | 'pro';
190
205
  interface LicenseStatus {
@@ -194,9 +209,15 @@ interface LicenseStatus {
194
209
  }
195
210
  /**
196
211
  * Resolve the user's license tier from CULLIT_API_KEY env var.
197
- * Validates the key format and caches verification result.
212
+ * Sync format-only check use for display, not enforcement.
198
213
  */
199
214
  declare function resolveLicense(): LicenseStatus;
215
+ /**
216
+ * Validate the license asynchronously with remote server validation.
217
+ * Falls back to format-only check if offline or no validation URL configured.
218
+ * Results are cached for 24 hours per key.
219
+ */
220
+ declare function validateLicense(): Promise<LicenseStatus>;
200
221
  /**
201
222
  * Check whether the current license allows the requested provider.
202
223
  */
@@ -213,6 +234,19 @@ declare function isEnrichmentAllowed(license: LicenseStatus): boolean;
213
234
  * Build a human-readable upgrade message for a gated feature.
214
235
  */
215
236
  declare function upgradeMessage(feature: string): string;
237
+ interface UsageLimits {
238
+ generationsPerMonth: number;
239
+ maxProjects: number;
240
+ }
241
+ /**
242
+ * Get usage limits for a license tier.
243
+ */
244
+ declare function getTierLimits(tier: string): UsageLimits;
245
+ /**
246
+ * Report a generation event to the metering service.
247
+ * Non-blocking — failures are logged but never block the pipeline.
248
+ */
249
+ declare function reportUsage(project?: string): Promise<void>;
216
250
 
217
251
  /**
218
252
  * Plugin Registry — the seam between free (core) and pro features.
@@ -239,6 +273,10 @@ declare function hasGenerator(provider: string): boolean;
239
273
  declare function hasCollector(type: string): boolean;
240
274
  declare function hasPublisher(type: string): boolean;
241
275
  declare function hasEnricher(type: string): boolean;
276
+ declare function listCollectors(): string[];
277
+ declare function listEnrichers(): string[];
278
+ declare function listGenerators(): string[];
279
+ declare function listPublishers(): string[];
242
280
 
243
281
  /**
244
282
  * Shared fetch wrapper with timeout support.
@@ -255,4 +293,4 @@ declare function runPipeline(from: string, to: string, config: CullConfig, optio
255
293
  logger?: Logger;
256
294
  }): Promise<PipelineResult>;
257
295
 
258
- export { type ChangeCategory, type ChangeEntry, type Collector, type CollectorFactory, DEFAULT_CATEGORIES, DEFAULT_MODELS, 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, type PipelineResult, type Publisher, type PublisherFactory, type ReleaseAdvisory, type ReleaseNotes, type SemverBump, StdoutPublisher, TemplateGenerator, VERSION, analyzeReleaseReadiness, createLogger, fetchWithTimeout, formatNotes, getCollector, getEnricher, getGenerator, getLatestTag, getPublisher, getRecentTags, hasCollector, hasEnricher, hasGenerator, hasPublisher, isEnrichmentAllowed, isProviderAllowed, isPublisherAllowed, registerCollector, registerEnricher, registerGenerator, registerPublisher, resolveLicense, runPipeline, upgradeMessage };
296
+ 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, 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 };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/constants.ts
2
- var VERSION = "0.4.0";
2
+ var VERSION = "1.0.0";
3
3
  var DEFAULT_CATEGORIES = ["features", "fixes", "breaking", "improvements", "chores"];
4
4
  var DEFAULT_MODELS = {
5
5
  anthropic: "claude-sonnet-4-20250514",
@@ -8,6 +8,14 @@ var DEFAULT_MODELS = {
8
8
  ollama: "llama3.1",
9
9
  openclaw: "anthropic/claude-sonnet-4-6"
10
10
  };
11
+ var AI_PROVIDERS = ["anthropic", "openai", "gemini", "ollama", "openclaw", "none"];
12
+ var OUTPUT_FORMATS = ["markdown", "html", "json"];
13
+ var PUBLISHER_TYPES = ["stdout", "file", "slack", "discord", "github-release", "teams", "confluence", "notion", "gitlab-release", "changelog"];
14
+ var ENRICHMENT_TYPES = ["jira", "linear"];
15
+ var CHANGE_CATEGORIES = ["features", "fixes", "breaking", "improvements", "chores", "other"];
16
+ var AUDIENCES = ["developer", "end-user", "executive"];
17
+ var TONES = ["professional", "casual", "terse"];
18
+ var SOURCE_TYPES = ["local", "jira", "linear", "gitlab", "bitbucket"];
11
19
 
12
20
  // src/logger.ts
13
21
  function createLogger(level = "normal") {
@@ -289,6 +297,16 @@ var TemplateGenerator = class {
289
297
  };
290
298
 
291
299
  // src/formatter.ts
300
+ var formatters = /* @__PURE__ */ new Map();
301
+ function registerFormatter(format, fn) {
302
+ formatters.set(format, fn);
303
+ }
304
+ function getFormatter(format) {
305
+ return formatters.get(format);
306
+ }
307
+ function listFormatters() {
308
+ return Array.from(formatters.keys());
309
+ }
292
310
  var CATEGORY_LABELS = {
293
311
  features: "\u2728 Features",
294
312
  fixes: "\u{1F41B} Bug Fixes",
@@ -306,16 +324,8 @@ var CATEGORY_ORDER = [
306
324
  "other"
307
325
  ];
308
326
  function formatNotes(notes, format) {
309
- switch (format) {
310
- case "markdown":
311
- return formatMarkdown(notes);
312
- case "html":
313
- return formatHTML(notes);
314
- case "json":
315
- return JSON.stringify(notes, null, 2);
316
- default:
317
- return formatMarkdown(notes);
318
- }
327
+ const fn = formatters.get(format) || formatters.get("markdown");
328
+ return fn(notes);
319
329
  }
320
330
  function formatMarkdown(notes) {
321
331
  const lines = [];
@@ -386,6 +396,9 @@ function groupByCategory(notes) {
386
396
  }
387
397
  return grouped;
388
398
  }
399
+ registerFormatter("markdown", formatMarkdown);
400
+ registerFormatter("html", formatHTML);
401
+ registerFormatter("json", (notes) => JSON.stringify(notes, null, 2));
389
402
 
390
403
  // src/publishers/index.ts
391
404
  import { writeFileSync } from "fs";
@@ -531,9 +544,28 @@ function analyzeReleaseReadiness(cwd = process.cwd()) {
531
544
  };
532
545
  }
533
546
 
547
+ // src/fetch.ts
548
+ var DEFAULT_TIMEOUT = 3e4;
549
+ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT) {
550
+ const controller = new AbortController();
551
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
552
+ try {
553
+ return await fetch(url, { ...init, signal: controller.signal });
554
+ } catch (err) {
555
+ if (err.name === "AbortError") {
556
+ throw new Error(`Request to ${new URL(url).hostname} timed out after ${timeoutMs / 1e3}s`);
557
+ }
558
+ throw err;
559
+ } finally {
560
+ clearTimeout(timer);
561
+ }
562
+ }
563
+
534
564
  // src/gate.ts
535
565
  var FREE_PROVIDERS = /* @__PURE__ */ new Set(["none"]);
536
566
  var FREE_PUBLISHERS = /* @__PURE__ */ new Set(["stdout", "file"]);
567
+ var LICENSE_CACHE_TTL = 24 * 60 * 60 * 1e3;
568
+ var cachedValidation = null;
537
569
  function resolveLicense() {
538
570
  const key = process.env.CULLIT_API_KEY?.trim();
539
571
  if (!key) {
@@ -544,6 +576,48 @@ function resolveLicense() {
544
576
  }
545
577
  return { tier: "pro", valid: true };
546
578
  }
579
+ async function validateLicense() {
580
+ const key = process.env.CULLIT_API_KEY?.trim();
581
+ const validationUrl = process.env.CULLIT_LICENSE_URL?.trim();
582
+ if (!key) {
583
+ return { tier: "free", valid: true };
584
+ }
585
+ if (!/^clt_[a-zA-Z0-9]{32,}$/.test(key)) {
586
+ return { tier: "free", valid: false, message: "Invalid CULLIT_API_KEY format. Expected: clt_<key>" };
587
+ }
588
+ if (cachedValidation && cachedValidation.key === key && Date.now() < cachedValidation.expiresAt) {
589
+ return cachedValidation.status;
590
+ }
591
+ if (!validationUrl) {
592
+ return { tier: "pro", valid: true };
593
+ }
594
+ try {
595
+ const res = await fetchWithTimeout(validationUrl, {
596
+ method: "POST",
597
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
598
+ body: JSON.stringify({ key })
599
+ }, 1e4);
600
+ if (res.ok) {
601
+ const data = await res.json();
602
+ const status2 = {
603
+ tier: data.tier === "pro" ? "pro" : "free",
604
+ valid: data.valid !== false,
605
+ message: data.message
606
+ };
607
+ cachedValidation = { status: status2, key, expiresAt: Date.now() + LICENSE_CACHE_TTL };
608
+ return status2;
609
+ }
610
+ const status = {
611
+ tier: "free",
612
+ valid: false,
613
+ message: "License validation failed. Check your API key at https://cullit.io/pricing"
614
+ };
615
+ cachedValidation = { status, key, expiresAt: Date.now() + LICENSE_CACHE_TTL };
616
+ return status;
617
+ } catch {
618
+ return { tier: "pro", valid: true };
619
+ }
620
+ }
547
621
  function isProviderAllowed(provider, license) {
548
622
  if (license.tier === "pro" && license.valid) return true;
549
623
  return FREE_PROVIDERS.has(provider);
@@ -560,6 +634,35 @@ function upgradeMessage(feature) {
560
634
  Get your API key at https://cullit.io/pricing
561
635
  Then set CULLIT_API_KEY in your environment.`;
562
636
  }
637
+ var TIER_LIMITS = {
638
+ free: { generationsPerMonth: 10, maxProjects: 1 },
639
+ pro: { generationsPerMonth: 500, maxProjects: 5 },
640
+ team: { generationsPerMonth: 2e3, maxProjects: 25 },
641
+ enterprise: { generationsPerMonth: Infinity, maxProjects: Infinity }
642
+ };
643
+ function getTierLimits(tier) {
644
+ return TIER_LIMITS[tier] || TIER_LIMITS.free;
645
+ }
646
+ async function reportUsage(project = "default") {
647
+ const key = process.env.CULLIT_API_KEY?.trim();
648
+ const meterUrl = process.env.CULLIT_METER_URL?.trim();
649
+ if (!meterUrl || !key) return;
650
+ try {
651
+ await fetchWithTimeout(meterUrl, {
652
+ method: "POST",
653
+ headers: {
654
+ "Content-Type": "application/json",
655
+ "Authorization": `Bearer ${key}`
656
+ },
657
+ body: JSON.stringify({
658
+ event: "generation",
659
+ project,
660
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
661
+ })
662
+ }, 5e3);
663
+ } catch {
664
+ }
665
+ }
563
666
 
564
667
  // src/registry.ts
565
668
  var collectors = /* @__PURE__ */ new Map();
@@ -602,34 +705,29 @@ function hasPublisher(type) {
602
705
  function hasEnricher(type) {
603
706
  return enrichers.has(type);
604
707
  }
605
-
606
- // src/fetch.ts
607
- var DEFAULT_TIMEOUT = 3e4;
608
- async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT) {
609
- const controller = new AbortController();
610
- const timer = setTimeout(() => controller.abort(), timeoutMs);
611
- try {
612
- return await fetch(url, { ...init, signal: controller.signal });
613
- } catch (err) {
614
- if (err.name === "AbortError") {
615
- throw new Error(`Request to ${new URL(url).hostname} timed out after ${timeoutMs / 1e3}s`);
616
- }
617
- throw err;
618
- } finally {
619
- clearTimeout(timer);
620
- }
708
+ function listCollectors() {
709
+ return Array.from(collectors.keys());
710
+ }
711
+ function listEnrichers() {
712
+ return Array.from(enrichers.keys());
713
+ }
714
+ function listGenerators() {
715
+ return Array.from(generators.keys());
716
+ }
717
+ function listPublishers() {
718
+ return Array.from(publishers.keys());
621
719
  }
622
720
 
623
721
  // src/index.ts
624
722
  registerCollector("local", () => new GitCollector());
625
723
  registerGenerator("none", () => new TemplateGenerator());
626
- registerPublisher("stdout", () => new StdoutPublisher());
627
- registerPublisher("file", (path) => new FilePublisher(path));
724
+ registerPublisher("stdout", (_target) => new StdoutPublisher());
725
+ registerPublisher("file", (target) => new FilePublisher(target.path));
628
726
  async function runPipeline(from, to, config, options = {}) {
629
727
  const startTime = Date.now();
630
728
  const format = options.format || "markdown";
631
729
  const log = options.logger || createLogger("normal");
632
- const license = resolveLicense();
730
+ const license = await validateLicense();
633
731
  if (!license.valid) {
634
732
  throw new Error(license.message || "Invalid CULLIT_API_KEY");
635
733
  }
@@ -639,20 +737,12 @@ async function runPipeline(from, to, config, options = {}) {
639
737
  const collectorFactory = getCollector(config.source.type);
640
738
  if (!collectorFactory) {
641
739
  throw new Error(
642
- `Source type "${config.source.type}" is not available. ` + (config.source.type === "jira" || config.source.type === "linear" ? "Install @cullit/pro to use this source." : "Valid sources: local")
740
+ `Source type "${config.source.type}" is not available. ` + (config.source.type !== "local" ? "Install @cullit/pro to use this source." : "Valid sources: local")
643
741
  );
644
742
  }
645
- const sourceLabel = config.source.type === "jira" ? "issues from Jira" : config.source.type === "linear" ? "issues from Linear" : `commits between ${from}..${to}`;
743
+ const sourceLabel = config.source.type === "local" ? `commits between ${from}..${to}` : `items from ${config.source.type}`;
646
744
  log.info(`\xBB Collecting ${sourceLabel}`);
647
- let collector;
648
- if (config.source.type === "jira") {
649
- if (!config.jira) throw new Error("Jira source requires jira config in .cullit.yml");
650
- collector = collectorFactory(config.jira);
651
- } else if (config.source.type === "linear") {
652
- collector = collectorFactory(config.linear?.apiKey);
653
- } else {
654
- collector = collectorFactory();
655
- }
745
+ const collector = collectorFactory(config);
656
746
  const diff = await collector.collect(from, to);
657
747
  const itemLabel = config.source.type === "jira" || config.source.type === "linear" ? "issues" : "commits";
658
748
  log.info(`\xBB Found ${diff.commits.length} ${itemLabel}${diff.filesChanged ? `, ${diff.filesChanged} files changed` : ""}`);
@@ -673,14 +763,7 @@ async function runPipeline(from, to, config, options = {}) {
673
763
  continue;
674
764
  }
675
765
  log.info(`\xBB Enriching from ${source}...`);
676
- let enricher;
677
- if (source === "jira" && config.jira) {
678
- enricher = enricherFactory(config.jira);
679
- } else if (source === "linear") {
680
- enricher = enricherFactory(config.linear?.apiKey);
681
- } else {
682
- continue;
683
- }
766
+ const enricher = enricherFactory(config);
684
767
  const enrichedTickets = await enricher.enrich(diff);
685
768
  tickets.push(...enrichedTickets);
686
769
  log.info(`\xBB ${source}: found ${enrichedTickets.length} ${source === "jira" ? "tickets" : "issues"}`);
@@ -725,26 +808,7 @@ async function runPipeline(from, to, config, options = {}) {
725
808
  log.info(`\xBB Skipping ${target.type} \u2014 install @cullit/pro to enable`);
726
809
  continue;
727
810
  }
728
- let publisher;
729
- switch (target.type) {
730
- case "stdout":
731
- publisher = publisherFactory();
732
- break;
733
- case "file":
734
- if (!target.path) continue;
735
- publisher = publisherFactory(target.path);
736
- break;
737
- case "slack":
738
- case "discord":
739
- if (!target.webhookUrl) continue;
740
- publisher = publisherFactory(target.webhookUrl);
741
- break;
742
- case "github-release":
743
- publisher = publisherFactory();
744
- break;
745
- default:
746
- continue;
747
- }
811
+ const publisher = publisherFactory(target);
748
812
  await publisher.publish(notes, format, formatted);
749
813
  publishedTo.push(target.type === "file" ? `file:${target.path}` : target.type);
750
814
  } catch (err) {
@@ -762,11 +826,19 @@ async function runPipeline(from, to, config, options = {}) {
762
826
  return { notes, formatted, publishedTo, duration };
763
827
  }
764
828
  export {
829
+ AI_PROVIDERS,
830
+ AUDIENCES,
831
+ CHANGE_CATEGORIES,
765
832
  DEFAULT_CATEGORIES,
766
833
  DEFAULT_MODELS,
834
+ ENRICHMENT_TYPES,
767
835
  FilePublisher,
768
836
  GitCollector,
837
+ OUTPUT_FORMATS,
838
+ PUBLISHER_TYPES,
839
+ SOURCE_TYPES,
769
840
  StdoutPublisher,
841
+ TONES,
770
842
  TemplateGenerator,
771
843
  VERSION,
772
844
  analyzeReleaseReadiness,
@@ -775,10 +847,12 @@ export {
775
847
  formatNotes,
776
848
  getCollector,
777
849
  getEnricher,
850
+ getFormatter,
778
851
  getGenerator,
779
852
  getLatestTag,
780
853
  getPublisher,
781
854
  getRecentTags,
855
+ getTierLimits,
782
856
  hasCollector,
783
857
  hasEnricher,
784
858
  hasGenerator,
@@ -786,11 +860,19 @@ export {
786
860
  isEnrichmentAllowed,
787
861
  isProviderAllowed,
788
862
  isPublisherAllowed,
863
+ listCollectors,
864
+ listEnrichers,
865
+ listFormatters,
866
+ listGenerators,
867
+ listPublishers,
789
868
  registerCollector,
790
869
  registerEnricher,
870
+ registerFormatter,
791
871
  registerGenerator,
792
872
  registerPublisher,
873
+ reportUsage,
793
874
  resolveLicense,
794
875
  runPipeline,
795
- upgradeMessage
876
+ upgradeMessage,
877
+ validateLicense
796
878
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cullit/core",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
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": "0.4.0"
34
+ "@cullit/config": "1.0.0"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsup src/index.ts --format esm --dts --clean",