@cullit/core 0.5.0 → 1.4.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
- 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';
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';
3
3
 
4
4
  interface GitCommit {
5
5
  hash: string;
@@ -71,17 +71,17 @@ interface PipelineResult {
71
71
  duration: number;
72
72
  }
73
73
 
74
- declare const VERSION = "0.5.0";
74
+ declare const VERSION = "1.4.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"];
78
- declare const OUTPUT_FORMATS: readonly ["markdown", "html", "json"];
79
- declare const PUBLISHER_TYPES: readonly ["stdout", "file", "slack", "discord", "github-release"];
78
+ declare const OUTPUT_FORMATS: readonly ["markdown", "html", "html-dark", "html-minimal", "html-edgy", "json"];
79
+ declare const PUBLISHER_TYPES: readonly ["stdout", "file", "slack", "discord", "github-release", "teams", "confluence", "notion", "gitlab-release", "changelog"];
80
80
  declare const ENRICHMENT_TYPES: readonly ["jira", "linear"];
81
81
  declare const CHANGE_CATEGORIES: readonly ["features", "fixes", "breaking", "improvements", "chores", "other"];
82
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"];
83
+ declare const TONES: readonly ["professional", "casual", "terse", "edgy", "hype", "snarky"];
84
+ declare const SOURCE_TYPES: readonly ["local", "jira", "linear", "gitlab", "bitbucket", "multi-repo"];
85
85
 
86
86
  type LogLevel = 'quiet' | 'normal' | 'verbose';
87
87
  interface Logger {
@@ -123,6 +123,23 @@ declare function getRecentTags(cwd?: string, count?: number): string[];
123
123
  */
124
124
  declare function getLatestTag(cwd?: string): string | null;
125
125
 
126
+ /**
127
+ * Collects commits from multiple repositories and merges them into a single GitDiff.
128
+ * Supports both local paths and remote URLs (shallow-cloned to temp dirs).
129
+ *
130
+ * Commits are tagged with `[repo-name]` prefix in the message for traceability.
131
+ * Results are sorted by date (newest first) across all repos.
132
+ */
133
+ declare class MultiRepoCollector implements Collector {
134
+ private repos;
135
+ private tempDirs;
136
+ constructor(repos: RepoSource[]);
137
+ collect(from: string, to: string): Promise<GitDiff>;
138
+ private resolveRepoPath;
139
+ private inferName;
140
+ private cleanup;
141
+ }
142
+
126
143
  /**
127
144
  * Template-based release notes generator — no AI required.
128
145
  * Groups commits by conventional commit prefix and ticket type.
@@ -234,6 +251,19 @@ declare function isEnrichmentAllowed(license: LicenseStatus): boolean;
234
251
  * Build a human-readable upgrade message for a gated feature.
235
252
  */
236
253
  declare function upgradeMessage(feature: string): string;
254
+ interface UsageLimits {
255
+ generationsPerMonth: number;
256
+ maxProjects: number;
257
+ }
258
+ /**
259
+ * Get usage limits for a license tier.
260
+ */
261
+ declare function getTierLimits(tier: string): UsageLimits;
262
+ /**
263
+ * Report a generation event to the metering service.
264
+ * Non-blocking — failures are logged but never block the pipeline.
265
+ */
266
+ declare function reportUsage(project?: string): Promise<void>;
237
267
 
238
268
  /**
239
269
  * Plugin Registry — the seam between free (core) and pro features.
@@ -280,4 +310,4 @@ declare function runPipeline(from: string, to: string, config: CullConfig, optio
280
310
  logger?: Logger;
281
311
  }): Promise<PipelineResult>;
282
312
 
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 };
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 };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/constants.ts
2
- var VERSION = "0.5.0";
2
+ var VERSION = "1.4.0";
3
3
  var DEFAULT_CATEGORIES = ["features", "fixes", "breaking", "improvements", "chores"];
4
4
  var DEFAULT_MODELS = {
5
5
  anthropic: "claude-sonnet-4-20250514",
@@ -9,13 +9,13 @@ var DEFAULT_MODELS = {
9
9
  openclaw: "anthropic/claude-sonnet-4-6"
10
10
  };
11
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"];
12
+ var OUTPUT_FORMATS = ["markdown", "html", "html-dark", "html-minimal", "html-edgy", "json"];
13
+ var PUBLISHER_TYPES = ["stdout", "file", "slack", "discord", "github-release", "teams", "confluence", "notion", "gitlab-release", "changelog"];
14
14
  var ENRICHMENT_TYPES = ["jira", "linear"];
15
15
  var CHANGE_CATEGORIES = ["features", "fixes", "breaking", "improvements", "chores", "other"];
16
16
  var AUDIENCES = ["developer", "end-user", "executive"];
17
- var TONES = ["professional", "casual", "terse"];
18
- var SOURCE_TYPES = ["local", "jira", "linear"];
17
+ var TONES = ["professional", "casual", "terse", "edgy", "hype", "snarky"];
18
+ var SOURCE_TYPES = ["local", "jira", "linear", "gitlab", "bitbucket", "multi-repo"];
19
19
 
20
20
  // src/logger.ts
21
21
  function createLogger(level = "normal") {
@@ -36,7 +36,7 @@ function createLogger(level = "normal") {
36
36
  }
37
37
 
38
38
  // src/collectors/git.ts
39
- import { execSync } from "child_process";
39
+ import { execFileSync } from "child_process";
40
40
  function validateRef(ref) {
41
41
  if (!ref || ref.length > 256) {
42
42
  throw new Error(`Invalid git ref: too ${ref ? "long" : "short"}`);
@@ -66,8 +66,9 @@ var GitCollector = class {
66
66
  const format = "%H|%h|%an|%aI|%s|%b";
67
67
  const separator = "---CULLIT_COMMIT---";
68
68
  try {
69
- return execSync(
70
- `git log ${from}..${to} --format="${format}${separator}" --no-merges`,
69
+ return execFileSync(
70
+ "git",
71
+ ["log", `${from}..${to}`, `--format=${format}${separator}`, "--no-merges"],
71
72
  { cwd: this.cwd, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
72
73
  );
73
74
  } catch (error) {
@@ -130,8 +131,9 @@ ${body}` : message;
130
131
  }
131
132
  getFilesChanged(from, to) {
132
133
  try {
133
- const output = execSync(
134
- `git diff --shortstat ${from}..${to}`,
134
+ const output = execFileSync(
135
+ "git",
136
+ ["diff", "--shortstat", `${from}..${to}`],
135
137
  { cwd: this.cwd, encoding: "utf-8" }
136
138
  );
137
139
  const match = output.match(/(\d+) files? changed/);
@@ -143,8 +145,9 @@ ${body}` : message;
143
145
  };
144
146
  function getRecentTags(cwd = process.cwd(), count = 10) {
145
147
  try {
146
- const output = execSync(
147
- `git tag --sort=-v:refname`,
148
+ const output = execFileSync(
149
+ "git",
150
+ ["tag", "--sort=-v:refname"],
148
151
  { cwd, encoding: "utf-8" }
149
152
  );
150
153
  return output.trim().split("\n").filter(Boolean).slice(0, count);
@@ -154,7 +157,7 @@ function getRecentTags(cwd = process.cwd(), count = 10) {
154
157
  }
155
158
  function getLatestTag(cwd = process.cwd()) {
156
159
  try {
157
- return execSync("git describe --tags --abbrev=0", { cwd, encoding: "utf-8" }).trim();
160
+ return execFileSync("git", ["describe", "--tags", "--abbrev=0"], { cwd, encoding: "utf-8" }).trim();
158
161
  } catch {
159
162
  return null;
160
163
  }
@@ -164,8 +167,9 @@ function getCommitsSince(from, to, cwd = process.cwd()) {
164
167
  validateRef(to);
165
168
  const format = "%H|%h|%an|%aI|%s";
166
169
  const separator = "---CULLIT_COMMIT---";
167
- const log = execSync(
168
- `git log ${from}..${to} --format="${format}${separator}" --no-merges`,
170
+ const log = execFileSync(
171
+ "git",
172
+ ["log", `${from}..${to}`, `--format=${format}${separator}`, "--no-merges"],
169
173
  { cwd, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
170
174
  );
171
175
  if (!log.trim()) return [];
@@ -181,6 +185,80 @@ function getCommitsSince(from, to, cwd = process.cwd()) {
181
185
  });
182
186
  }
183
187
 
188
+ // src/collectors/multi-repo.ts
189
+ import { execFileSync as execFileSync2 } from "child_process";
190
+ import { mkdtempSync, rmSync } from "fs";
191
+ import { join } from "path";
192
+ import { tmpdir } from "os";
193
+ var MultiRepoCollector = class {
194
+ repos;
195
+ tempDirs = [];
196
+ constructor(repos) {
197
+ if (!repos.length) throw new Error("Multi-repo collector requires at least one repo");
198
+ this.repos = repos;
199
+ }
200
+ async collect(from, to) {
201
+ const allCommits = [];
202
+ let totalFilesChanged = 0;
203
+ try {
204
+ for (const repo of this.repos) {
205
+ const repoPath = await this.resolveRepoPath(repo);
206
+ const repoName = repo.name || this.inferName(repo);
207
+ const repoFrom = repo.from || from;
208
+ const repoTo = repo.to || to;
209
+ const collector = new GitCollector(repoPath);
210
+ const diff = await collector.collect(repoFrom, repoTo);
211
+ const taggedCommits = diff.commits.map((c) => ({
212
+ ...c,
213
+ message: `[${repoName}] ${c.message}`
214
+ }));
215
+ allCommits.push(...taggedCommits);
216
+ totalFilesChanged += diff.filesChanged || 0;
217
+ }
218
+ } finally {
219
+ this.cleanup();
220
+ }
221
+ allCommits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
222
+ return {
223
+ from,
224
+ to,
225
+ commits: allCommits,
226
+ filesChanged: totalFilesChanged
227
+ };
228
+ }
229
+ async resolveRepoPath(repo) {
230
+ if (repo.path) return repo.path;
231
+ if (!repo.url) {
232
+ throw new Error('Each repo must have either "url" or "path"');
233
+ }
234
+ if (!/^(https?:\/\/|git@|ssh:\/\/)/.test(repo.url)) {
235
+ throw new Error(`Invalid repo URL: ${repo.url}`);
236
+ }
237
+ const tempDir = mkdtempSync(join(tmpdir(), "cullit-repo-"));
238
+ this.tempDirs.push(tempDir);
239
+ execFileSync2(
240
+ "git",
241
+ ["clone", "--depth=500", "--single-branch", repo.url, tempDir],
242
+ { encoding: "utf-8", timeout: 6e4, stdio: "pipe" }
243
+ );
244
+ return tempDir;
245
+ }
246
+ inferName(repo) {
247
+ const source = repo.url || repo.path || "unknown";
248
+ const basename = source.replace(/\.git$/, "").split(/[/\\]/).pop();
249
+ return basename || "unknown";
250
+ }
251
+ cleanup() {
252
+ for (const dir of this.tempDirs) {
253
+ try {
254
+ rmSync(dir, { recursive: true, force: true });
255
+ } catch {
256
+ }
257
+ }
258
+ this.tempDirs = [];
259
+ }
260
+ };
261
+
184
262
  // src/generators/template.ts
185
263
  var TemplateGenerator = class {
186
264
  async generate(context, config) {
@@ -292,6 +370,18 @@ var TemplateGenerator = class {
292
370
  if (parts.length === 0) return `A quick update with ${commitCount} commits \u2014 nothing too wild.`;
293
371
  return `We've got ${parts.join(", ")} packed into ${commitCount} commits. Let's go!`;
294
372
  }
373
+ if (tone === "edgy") {
374
+ if (parts.length === 0) return `${commitCount} commits. No fluff. Just code that needed to exist.`;
375
+ return `Shipped: ${parts.join(", ")}. ${commitCount} commits. Zero apologies.`;
376
+ }
377
+ if (tone === "hype") {
378
+ if (parts.length === 0) return `\u{1F525} ${commitCount} commits just dropped and they're INCREDIBLE!`;
379
+ return `\u{1F680} HUGE release! ${parts.join(", ")} across ${commitCount} commits! This changes EVERYTHING!`;
380
+ }
381
+ if (tone === "snarky") {
382
+ if (parts.length === 0) return `${commitCount} commits. We were bored, okay?`;
383
+ return `Oh look, ${parts.join(", ")} from ${commitCount} commits. You're welcome.`;
384
+ }
295
385
  return parts.length > 0 ? `This release includes ${parts.join(", ")} across ${commitCount} commits.` : `This release includes ${commitCount} commits.`;
296
386
  }
297
387
  };
@@ -396,12 +486,64 @@ function groupByCategory(notes) {
396
486
  }
397
487
  return grouped;
398
488
  }
489
+ var THEME_DARK = `<style>
490
+ .cull-release{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f1117;color:#d4d4e0;padding:2rem;border-radius:12px;max-width:720px;line-height:1.7}
491
+ .cull-release h2{color:#f0f0f5;font-size:1.6rem;border-bottom:2px solid #5eead4;padding-bottom:.5rem;margin-bottom:1rem}
492
+ .cull-release h3{color:#5eead4;font-size:1.1rem;margin:1.5rem 0 .5rem}
493
+ .cull-release .summary{color:#7e819a;font-size:.95rem;margin-bottom:1.5rem}
494
+ .cull-release ul{list-style:none;padding:0}
495
+ .cull-release li{padding:.4rem 0;border-bottom:1px solid #1e2030;font-size:.9rem}
496
+ .cull-release li:last-child{border-bottom:none}
497
+ .cull-release code{background:#161822;color:#5eead4;padding:.15rem .4rem;border-radius:4px;font-size:.8rem}
498
+ .cull-release footer{margin-top:2rem;padding-top:1rem;border-top:1px solid #282a3a;color:#7e819a;font-size:.75rem}
499
+ .cull-release a{color:#5eead4;text-decoration:none}
500
+ </style>
501
+ `;
502
+ var THEME_MINIMAL = `<style>
503
+ .cull-release{font-family:'Georgia',serif;background:#fafafa;color:#222;padding:2.5rem;max-width:680px;line-height:1.8}
504
+ .cull-release h2{font-weight:400;font-size:1.4rem;letter-spacing:-.02em;border-bottom:1px solid #ddd;padding-bottom:.75rem;margin-bottom:1.25rem}
505
+ .cull-release h3{font-weight:600;font-size:.9rem;text-transform:uppercase;letter-spacing:.1em;color:#888;margin:1.5rem 0 .5rem}
506
+ .cull-release .summary{color:#555;font-size:.95rem;font-style:italic;margin-bottom:1.5rem}
507
+ .cull-release ul{list-style:none;padding:0}
508
+ .cull-release li{padding:.5rem 0;border-bottom:1px solid #eee;font-size:.9rem}
509
+ .cull-release li:last-child{border-bottom:none}
510
+ .cull-release code{background:#f0f0f0;color:#c7254e;padding:.1rem .35rem;border-radius:3px;font-size:.8rem}
511
+ .cull-release footer{margin-top:2rem;color:#aaa;font-size:.75rem}
512
+ .cull-release a{color:#222}
513
+ </style>
514
+ `;
515
+ var THEME_EDGY = `<style>
516
+ .cull-release{font-family:'Courier New',monospace;background:#000;color:#0f0;padding:2rem;border:1px solid #0f0;border-radius:0;max-width:720px;line-height:1.6;text-shadow:0 0 4px rgba(0,255,0,.3)}
517
+ .cull-release h2{color:#0f0;font-size:1.5rem;text-transform:uppercase;letter-spacing:.15em;border-bottom:2px solid #0f0;padding-bottom:.5rem;margin-bottom:1rem}
518
+ .cull-release h3{color:#0f0;font-size:1rem;text-transform:uppercase;letter-spacing:.1em;margin:1.5rem 0 .5rem;opacity:.8}
519
+ .cull-release h3::before{content:'> '}
520
+ .cull-release .summary{color:#0c0;font-size:.9rem;margin-bottom:1.5rem;opacity:.7}
521
+ .cull-release ul{list-style:none;padding:0}
522
+ .cull-release li{padding:.3rem 0;font-size:.85rem}
523
+ .cull-release li::before{content:'$ ';color:#0a0;opacity:.6}
524
+ .cull-release code{background:#001100;color:#5f5;padding:.15rem .4rem;border:1px solid #0a0;font-size:.8rem}
525
+ .cull-release footer{margin-top:2rem;padding-top:1rem;border-top:1px solid #0a0;color:#0a0;font-size:.7rem;opacity:.5}
526
+ .cull-release a{color:#0f0}
527
+ </style>
528
+ `;
529
+ function formatThemedHTML(theme) {
530
+ const themeCSS = {
531
+ dark: THEME_DARK,
532
+ minimal: THEME_MINIMAL,
533
+ edgy: THEME_EDGY
534
+ };
535
+ return (notes) => (themeCSS[theme] || "") + formatHTML(notes);
536
+ }
399
537
  registerFormatter("markdown", formatMarkdown);
400
538
  registerFormatter("html", formatHTML);
539
+ registerFormatter("html-dark", formatThemedHTML("dark"));
540
+ registerFormatter("html-minimal", formatThemedHTML("minimal"));
541
+ registerFormatter("html-edgy", formatThemedHTML("edgy"));
401
542
  registerFormatter("json", (notes) => JSON.stringify(notes, null, 2));
402
543
 
403
544
  // src/publishers/index.ts
404
545
  import { writeFileSync } from "fs";
546
+ import { resolve, relative, isAbsolute } from "path";
405
547
  var StdoutPublisher = class {
406
548
  async publish(notes, format, preformatted) {
407
549
  console.log(preformatted || formatNotes(notes, format));
@@ -410,6 +552,12 @@ var StdoutPublisher = class {
410
552
  var FilePublisher = class {
411
553
  constructor(path) {
412
554
  this.path = path;
555
+ const resolved = resolve(path);
556
+ const cwd = resolve(".");
557
+ const rel = relative(cwd, resolved);
558
+ if (rel.startsWith("..") || isAbsolute(rel)) {
559
+ throw new Error(`File output path must be within the project directory. Got: ${path}`);
560
+ }
413
561
  }
414
562
  async publish(notes, format, preformatted) {
415
563
  const output = preformatted || formatNotes(notes, format);
@@ -419,7 +567,7 @@ var FilePublisher = class {
419
567
  };
420
568
 
421
569
  // src/advisor.ts
422
- import { execSync as execSync2 } from "child_process";
570
+ import { execFileSync as execFileSync3 } from "child_process";
423
571
  var COMMIT_PATTERNS = [
424
572
  { pattern: /^breaking[(!:]|^BREAKING CHANGE/i, category: "breaking" },
425
573
  { pattern: /!:/, category: "breaking" },
@@ -456,8 +604,9 @@ function getCommitsSinceTag(tag, cwd) {
456
604
  }
457
605
  function getTagDate(tag, cwd) {
458
606
  try {
459
- const dateStr = execSync2(
460
- `git log -1 --format=%aI ${tag}`,
607
+ const dateStr = execFileSync3(
608
+ "git",
609
+ ["log", "-1", "--format=%aI", tag],
461
610
  { cwd, encoding: "utf-8" }
462
611
  ).trim();
463
612
  return new Date(dateStr);
@@ -565,6 +714,7 @@ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT) {
565
714
  var FREE_PROVIDERS = /* @__PURE__ */ new Set(["none"]);
566
715
  var FREE_PUBLISHERS = /* @__PURE__ */ new Set(["stdout", "file"]);
567
716
  var LICENSE_CACHE_TTL = 24 * 60 * 60 * 1e3;
717
+ var LICENSE_FAILURE_CACHE_TTL = 5 * 60 * 1e3;
568
718
  var cachedValidation = null;
569
719
  function resolveLicense() {
570
720
  const key = process.env.CULLIT_API_KEY?.trim();
@@ -612,10 +762,13 @@ async function validateLicense() {
612
762
  valid: false,
613
763
  message: "License validation failed. Check your API key at https://cullit.io/pricing"
614
764
  };
615
- cachedValidation = { status, key, expiresAt: Date.now() + LICENSE_CACHE_TTL };
765
+ cachedValidation = { status, key, expiresAt: Date.now() + LICENSE_FAILURE_CACHE_TTL };
616
766
  return status;
617
767
  } catch {
618
- return { tier: "pro", valid: true };
768
+ if (cachedValidation && cachedValidation.key === key) {
769
+ return cachedValidation.status;
770
+ }
771
+ return { tier: "pro", valid: true, message: "Offline validation \u2014 using cached license." };
619
772
  }
620
773
  }
621
774
  function isProviderAllowed(provider, license) {
@@ -634,6 +787,35 @@ function upgradeMessage(feature) {
634
787
  Get your API key at https://cullit.io/pricing
635
788
  Then set CULLIT_API_KEY in your environment.`;
636
789
  }
790
+ var TIER_LIMITS = {
791
+ free: { generationsPerMonth: 3, maxProjects: 1 },
792
+ pro: { generationsPerMonth: 500, maxProjects: 5 },
793
+ team: { generationsPerMonth: 2e3, maxProjects: 25 },
794
+ enterprise: { generationsPerMonth: Infinity, maxProjects: Infinity }
795
+ };
796
+ function getTierLimits(tier) {
797
+ return TIER_LIMITS[tier] || TIER_LIMITS.free;
798
+ }
799
+ async function reportUsage(project = "default") {
800
+ const key = process.env.CULLIT_API_KEY?.trim();
801
+ const meterUrl = process.env.CULLIT_METER_URL?.trim();
802
+ if (!meterUrl || !key) return;
803
+ try {
804
+ await fetchWithTimeout(meterUrl, {
805
+ method: "POST",
806
+ headers: {
807
+ "Content-Type": "application/json",
808
+ "Authorization": `Bearer ${key}`
809
+ },
810
+ body: JSON.stringify({
811
+ event: "generation",
812
+ project,
813
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
814
+ })
815
+ }, 5e3);
816
+ } catch {
817
+ }
818
+ }
637
819
 
638
820
  // src/registry.ts
639
821
  var collectors = /* @__PURE__ */ new Map();
@@ -690,7 +872,11 @@ function listPublishers() {
690
872
  }
691
873
 
692
874
  // src/index.ts
693
- registerCollector("local", () => new GitCollector());
875
+ registerCollector("local", (config) => new GitCollector(config.source?.repoPath));
876
+ registerCollector("multi-repo", (config) => {
877
+ if (!config.repos?.length) throw new Error('Multi-repo source requires "repos" array in config');
878
+ return new MultiRepoCollector(config.repos);
879
+ });
694
880
  registerGenerator("none", () => new TemplateGenerator());
695
881
  registerPublisher("stdout", (_target) => new StdoutPublisher());
696
882
  registerPublisher("file", (target) => new FilePublisher(target.path));
@@ -805,6 +991,7 @@ export {
805
991
  ENRICHMENT_TYPES,
806
992
  FilePublisher,
807
993
  GitCollector,
994
+ MultiRepoCollector,
808
995
  OUTPUT_FORMATS,
809
996
  PUBLISHER_TYPES,
810
997
  SOURCE_TYPES,
@@ -823,6 +1010,7 @@ export {
823
1010
  getLatestTag,
824
1011
  getPublisher,
825
1012
  getRecentTags,
1013
+ getTierLimits,
826
1014
  hasCollector,
827
1015
  hasEnricher,
828
1016
  hasGenerator,
@@ -840,6 +1028,7 @@ export {
840
1028
  registerFormatter,
841
1029
  registerGenerator,
842
1030
  registerPublisher,
1031
+ reportUsage,
843
1032
  resolveLicense,
844
1033
  runPipeline,
845
1034
  upgradeMessage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cullit/core",
3
- "version": "0.5.0",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "description": "Core engine for Cullit — AI-powered release note generation.",
6
6
  "license": "MIT",
@@ -31,10 +31,11 @@
31
31
  "node": ">=18"
32
32
  },
33
33
  "dependencies": {
34
- "@cullit/config": "0.5.0"
34
+ "@cullit/config": "1.4.0"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsup src/index.ts --format esm --dts --clean",
38
- "dev": "tsup src/index.ts --format esm --dts --watch"
38
+ "dev": "tsup src/index.ts --format esm --dts --watch",
39
+ "test": "vitest run"
39
40
  }
40
41
  }