@cullit/core 0.4.0 → 0.5.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
@@ -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 = "0.5.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"];
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"];
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
  */
@@ -239,6 +260,10 @@ declare function hasGenerator(provider: string): boolean;
239
260
  declare function hasCollector(type: string): boolean;
240
261
  declare function hasPublisher(type: string): boolean;
241
262
  declare function hasEnricher(type: string): boolean;
263
+ declare function listCollectors(): string[];
264
+ declare function listEnrichers(): string[];
265
+ declare function listGenerators(): string[];
266
+ declare function listPublishers(): string[];
242
267
 
243
268
  /**
244
269
  * Shared fetch wrapper with timeout support.
@@ -255,4 +280,4 @@ declare function runPipeline(from: string, to: string, config: CullConfig, optio
255
280
  logger?: Logger;
256
281
  }): Promise<PipelineResult>;
257
282
 
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 };
283
+ 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, VERSION, analyzeReleaseReadiness, createLogger, fetchWithTimeout, formatNotes, getCollector, getEnricher, getFormatter, getGenerator, getLatestTag, getPublisher, getRecentTags, hasCollector, hasEnricher, hasGenerator, hasPublisher, isEnrichmentAllowed, isProviderAllowed, isPublisherAllowed, listCollectors, listEnrichers, listFormatters, listGenerators, listPublishers, registerCollector, registerEnricher, registerFormatter, registerGenerator, registerPublisher, 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 = "0.5.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"];
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"];
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);
@@ -602,34 +676,29 @@ function hasPublisher(type) {
602
676
  function hasEnricher(type) {
603
677
  return enrichers.has(type);
604
678
  }
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
- }
679
+ function listCollectors() {
680
+ return Array.from(collectors.keys());
681
+ }
682
+ function listEnrichers() {
683
+ return Array.from(enrichers.keys());
684
+ }
685
+ function listGenerators() {
686
+ return Array.from(generators.keys());
687
+ }
688
+ function listPublishers() {
689
+ return Array.from(publishers.keys());
621
690
  }
622
691
 
623
692
  // src/index.ts
624
693
  registerCollector("local", () => new GitCollector());
625
694
  registerGenerator("none", () => new TemplateGenerator());
626
- registerPublisher("stdout", () => new StdoutPublisher());
627
- registerPublisher("file", (path) => new FilePublisher(path));
695
+ registerPublisher("stdout", (_target) => new StdoutPublisher());
696
+ registerPublisher("file", (target) => new FilePublisher(target.path));
628
697
  async function runPipeline(from, to, config, options = {}) {
629
698
  const startTime = Date.now();
630
699
  const format = options.format || "markdown";
631
700
  const log = options.logger || createLogger("normal");
632
- const license = resolveLicense();
701
+ const license = await validateLicense();
633
702
  if (!license.valid) {
634
703
  throw new Error(license.message || "Invalid CULLIT_API_KEY");
635
704
  }
@@ -639,20 +708,12 @@ async function runPipeline(from, to, config, options = {}) {
639
708
  const collectorFactory = getCollector(config.source.type);
640
709
  if (!collectorFactory) {
641
710
  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")
711
+ `Source type "${config.source.type}" is not available. ` + (config.source.type !== "local" ? "Install @cullit/pro to use this source." : "Valid sources: local")
643
712
  );
644
713
  }
645
- const sourceLabel = config.source.type === "jira" ? "issues from Jira" : config.source.type === "linear" ? "issues from Linear" : `commits between ${from}..${to}`;
714
+ const sourceLabel = config.source.type === "local" ? `commits between ${from}..${to}` : `items from ${config.source.type}`;
646
715
  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
- }
716
+ const collector = collectorFactory(config);
656
717
  const diff = await collector.collect(from, to);
657
718
  const itemLabel = config.source.type === "jira" || config.source.type === "linear" ? "issues" : "commits";
658
719
  log.info(`\xBB Found ${diff.commits.length} ${itemLabel}${diff.filesChanged ? `, ${diff.filesChanged} files changed` : ""}`);
@@ -673,14 +734,7 @@ async function runPipeline(from, to, config, options = {}) {
673
734
  continue;
674
735
  }
675
736
  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
- }
737
+ const enricher = enricherFactory(config);
684
738
  const enrichedTickets = await enricher.enrich(diff);
685
739
  tickets.push(...enrichedTickets);
686
740
  log.info(`\xBB ${source}: found ${enrichedTickets.length} ${source === "jira" ? "tickets" : "issues"}`);
@@ -725,26 +779,7 @@ async function runPipeline(from, to, config, options = {}) {
725
779
  log.info(`\xBB Skipping ${target.type} \u2014 install @cullit/pro to enable`);
726
780
  continue;
727
781
  }
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
- }
782
+ const publisher = publisherFactory(target);
748
783
  await publisher.publish(notes, format, formatted);
749
784
  publishedTo.push(target.type === "file" ? `file:${target.path}` : target.type);
750
785
  } catch (err) {
@@ -762,11 +797,19 @@ async function runPipeline(from, to, config, options = {}) {
762
797
  return { notes, formatted, publishedTo, duration };
763
798
  }
764
799
  export {
800
+ AI_PROVIDERS,
801
+ AUDIENCES,
802
+ CHANGE_CATEGORIES,
765
803
  DEFAULT_CATEGORIES,
766
804
  DEFAULT_MODELS,
805
+ ENRICHMENT_TYPES,
767
806
  FilePublisher,
768
807
  GitCollector,
808
+ OUTPUT_FORMATS,
809
+ PUBLISHER_TYPES,
810
+ SOURCE_TYPES,
769
811
  StdoutPublisher,
812
+ TONES,
770
813
  TemplateGenerator,
771
814
  VERSION,
772
815
  analyzeReleaseReadiness,
@@ -775,6 +818,7 @@ export {
775
818
  formatNotes,
776
819
  getCollector,
777
820
  getEnricher,
821
+ getFormatter,
778
822
  getGenerator,
779
823
  getLatestTag,
780
824
  getPublisher,
@@ -786,11 +830,18 @@ export {
786
830
  isEnrichmentAllowed,
787
831
  isProviderAllowed,
788
832
  isPublisherAllowed,
833
+ listCollectors,
834
+ listEnrichers,
835
+ listFormatters,
836
+ listGenerators,
837
+ listPublishers,
789
838
  registerCollector,
790
839
  registerEnricher,
840
+ registerFormatter,
791
841
  registerGenerator,
792
842
  registerPublisher,
793
843
  resolveLicense,
794
844
  runPipeline,
795
- upgradeMessage
845
+ upgradeMessage,
846
+ validateLicense
796
847
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cullit/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.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": "0.5.0"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsup src/index.ts --format esm --dts --clean",