@cullit/core 1.17.0 → 1.18.1

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/LICENSE CHANGED
@@ -1,21 +1,12 @@
1
- MIT License
1
+ Copyright (c) 2026 Cullit (Matt). All rights reserved.
2
2
 
3
- Copyright (c) 2026 Cullit (Matt)
3
+ This software and its source code are proprietary to Cullit.
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
5
+ You may view and fork this repository for evaluation purposes. You may not
6
+ use, copy, modify, merge, publish, distribute, sublicense, or sell copies
7
+ of this software without explicit written permission from the copyright holder.
11
8
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
9
+ Commercial use requires a valid Cullit license. See https://cullit.io/pricing
10
+ for available plans.
14
11
 
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
12
+ For full terms of use, see TERMS.md.
package/dist/index.d.ts CHANGED
@@ -71,7 +71,7 @@ interface PipelineResult {
71
71
  duration: number;
72
72
  }
73
73
 
74
- declare const VERSION = "1.17.0";
74
+ declare const VERSION = "1.18.1";
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", "none"];
@@ -312,6 +312,62 @@ declare function listPublishers(): string[];
312
312
  */
313
313
  declare function fetchWithTimeout(url: string, init: RequestInit, timeoutMs?: number): Promise<Response>;
314
314
 
315
+ /**
316
+ * Rate Limiter — Sliding-window rate limiter with pluggable backends.
317
+ *
318
+ * Usage:
319
+ * const limiter = createRateLimiter({ limit: 30, windowMs: 60_000 });
320
+ * const result = limiter.check('user-ip-or-key');
321
+ * if (!result.allowed) { // reject }
322
+ */
323
+ interface RateLimitResult {
324
+ allowed: boolean;
325
+ remaining: number;
326
+ /** Unix timestamp (seconds) when the window resets */
327
+ resetAt: number;
328
+ }
329
+ interface RateLimiter {
330
+ check(key: string): RateLimitResult;
331
+ /** Remove all tracked entries */
332
+ reset(): void;
333
+ }
334
+ interface RateLimiterOptions {
335
+ /** Max requests per window (default: 30) */
336
+ limit?: number;
337
+ /** Window duration in ms (default: 60_000) */
338
+ windowMs?: number;
339
+ /** Max tracked keys before eviction (default: 10_000) */
340
+ maxBuckets?: number;
341
+ }
342
+ declare function createRateLimiter(opts?: RateLimiterOptions): RateLimiter;
343
+
344
+ /**
345
+ * Structured error codes for the Cullit core pipeline.
346
+ * Used in CullitError instances so callers can branch on code rather than message parsing.
347
+ */
348
+ declare const CoreErrorCode: {
349
+ readonly GIT_REF_INVALID: "GIT_REF_INVALID";
350
+ readonly GIT_LOG_FAILED: "GIT_LOG_FAILED";
351
+ readonly MULTI_REPO_EMPTY: "MULTI_REPO_EMPTY";
352
+ readonly MULTI_REPO_MISSING_TARGET: "MULTI_REPO_MISSING_TARGET";
353
+ readonly MULTI_REPO_INVALID_URL: "MULTI_REPO_INVALID_URL";
354
+ readonly PIPELINE_NO_CHANGES: "PIPELINE_NO_CHANGES";
355
+ readonly PIPELINE_COLLECTOR_MISSING: "PIPELINE_COLLECTOR_MISSING";
356
+ readonly PIPELINE_GENERATOR_MISSING: "PIPELINE_GENERATOR_MISSING";
357
+ readonly LICENSE_INVALID: "LICENSE_INVALID";
358
+ readonly LICENSE_TIER_INSUFFICIENT: "LICENSE_TIER_INSUFFICIENT";
359
+ readonly FETCH_TIMEOUT: "FETCH_TIMEOUT";
360
+ readonly PUBLISHER_PATH_TRAVERSAL: "PUBLISHER_PATH_TRAVERSAL";
361
+ };
362
+ type CoreErrorCodeValue = typeof CoreErrorCode[keyof typeof CoreErrorCode];
363
+ /**
364
+ * Error class carrying a structured code alongside the human-readable message.
365
+ */
366
+ declare class CullitError extends Error {
367
+ readonly code: CoreErrorCodeValue;
368
+ constructor(code: CoreErrorCodeValue, message: string);
369
+ }
370
+
315
371
  /**
316
372
  * Main pipeline: Collect → Enrich → Generate → Format → Publish
317
373
  */
@@ -322,4 +378,4 @@ declare function runPipeline(from: string, to: string, config: CullConfig, optio
322
378
  templateProfile?: string;
323
379
  }): Promise<PipelineResult>;
324
380
 
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 };
381
+ export { AI_PROVIDERS, AUDIENCES, CHANGE_CATEGORIES, type ChangeCategory, type ChangeEntry, type Collector, type CollectorFactory, CoreErrorCode, type CoreErrorCodeValue, CullitError, 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 RateLimitResult, type RateLimiter, type RateLimiterOptions, type ReleaseAdvisory, type ReleaseNotes, SOURCE_TYPES, type SemverBump, StdoutPublisher, TONES, type TeamFeature, TemplateGenerator, type UsageLimits, VERSION, analyzeReleaseReadiness, createLogger, createRateLimiter, 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.17.0";
2
+ var VERSION = "1.18.1";
3
3
  var DEFAULT_CATEGORIES = ["features", "fixes", "breaking", "improvements", "chores"];
4
4
  var DEFAULT_MODELS = {
5
5
  anthropic: "claude-sonnet-4-20250514",
@@ -36,12 +36,44 @@ function createLogger(level = "normal") {
36
36
 
37
37
  // src/collectors/git.ts
38
38
  import { execFileSync } from "child_process";
39
+
40
+ // src/errors.ts
41
+ var CoreErrorCode = {
42
+ // Git / Source
43
+ GIT_REF_INVALID: "GIT_REF_INVALID",
44
+ GIT_LOG_FAILED: "GIT_LOG_FAILED",
45
+ // Multi-repo
46
+ MULTI_REPO_EMPTY: "MULTI_REPO_EMPTY",
47
+ MULTI_REPO_MISSING_TARGET: "MULTI_REPO_MISSING_TARGET",
48
+ MULTI_REPO_INVALID_URL: "MULTI_REPO_INVALID_URL",
49
+ // Pipeline
50
+ PIPELINE_NO_CHANGES: "PIPELINE_NO_CHANGES",
51
+ PIPELINE_COLLECTOR_MISSING: "PIPELINE_COLLECTOR_MISSING",
52
+ PIPELINE_GENERATOR_MISSING: "PIPELINE_GENERATOR_MISSING",
53
+ // License
54
+ LICENSE_INVALID: "LICENSE_INVALID",
55
+ LICENSE_TIER_INSUFFICIENT: "LICENSE_TIER_INSUFFICIENT",
56
+ // Fetch
57
+ FETCH_TIMEOUT: "FETCH_TIMEOUT",
58
+ // Publisher
59
+ PUBLISHER_PATH_TRAVERSAL: "PUBLISHER_PATH_TRAVERSAL"
60
+ };
61
+ var CullitError = class extends Error {
62
+ code;
63
+ constructor(code, message) {
64
+ super(message);
65
+ this.name = "CullitError";
66
+ this.code = code;
67
+ }
68
+ };
69
+
70
+ // src/collectors/git.ts
39
71
  function validateRef(ref) {
40
72
  if (!ref || ref.length > 256) {
41
- throw new Error(`Invalid git ref: too ${ref ? "long" : "short"}`);
73
+ throw new CullitError(CoreErrorCode.GIT_REF_INVALID, `Invalid git ref: too ${ref ? "long" : "short"}`);
42
74
  }
43
75
  if (!/^[a-zA-Z0-9._\-/~^]+$/.test(ref)) {
44
- throw new Error(`Invalid git ref "${ref}" \u2014 only alphanumeric, dots, dashes, underscores, slashes, tildes, and carets are allowed`);
76
+ throw new CullitError(CoreErrorCode.GIT_REF_INVALID, `Invalid git ref "${ref}" \u2014 only alphanumeric, dots, dashes, underscores, slashes, tildes, and carets are allowed`);
45
77
  }
46
78
  }
47
79
  var GitCollector = class {
@@ -74,7 +106,8 @@ var GitCollector = class {
74
106
  const errWithStderr = typeof error === "object" && error !== null && "stderr" in error ? error : void 0;
75
107
  const stderr = errWithStderr?.stderr?.toString?.() || "";
76
108
  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
- throw new Error(
109
+ throw new CullitError(
110
+ CoreErrorCode.GIT_LOG_FAILED,
78
111
  `Failed to read git log between ${from} and ${to}. ${hint}`
79
112
  );
80
113
  }
@@ -194,7 +227,7 @@ var MultiRepoCollector = class {
194
227
  repos;
195
228
  tempDirs = [];
196
229
  constructor(repos) {
197
- if (!repos.length) throw new Error("Multi-repo collector requires at least one repo");
230
+ if (!repos.length) throw new CullitError(CoreErrorCode.MULTI_REPO_EMPTY, "Multi-repo collector requires at least one repo");
198
231
  this.repos = repos;
199
232
  }
200
233
  async collect(from, to) {
@@ -229,10 +262,10 @@ var MultiRepoCollector = class {
229
262
  async resolveRepoPath(repo) {
230
263
  if (repo.path) return repo.path;
231
264
  if (!repo.url) {
232
- throw new Error('Each repo must have either "url" or "path"');
265
+ throw new CullitError(CoreErrorCode.MULTI_REPO_MISSING_TARGET, 'Each repo must have either "url" or "path"');
233
266
  }
234
267
  if (!/^(https?:\/\/|git@|ssh:\/\/)/.test(repo.url)) {
235
- throw new Error(`Invalid repo URL: ${repo.url}`);
268
+ throw new CullitError(CoreErrorCode.MULTI_REPO_INVALID_URL, `Invalid repo URL: ${repo.url}`);
236
269
  }
237
270
  const tempDir = mkdtempSync(join(tmpdir(), "cullit-repo-"));
238
271
  this.tempDirs.push(tempDir);
@@ -560,7 +593,7 @@ var FilePublisher = class {
560
593
  const cwd = resolve(".");
561
594
  const rel = relative(cwd, resolved);
562
595
  if (rel.startsWith("..") || isAbsolute(rel)) {
563
- throw new Error(`File output path must be within the project directory. Got: ${path}`);
596
+ throw new CullitError(CoreErrorCode.PUBLISHER_PATH_TRAVERSAL, `File output path must be within the project directory. Got: ${path}`);
564
597
  }
565
598
  }
566
599
  async publish(notes, format, preformatted) {
@@ -706,7 +739,7 @@ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT) {
706
739
  return await fetch(url, { ...init, signal: controller.signal });
707
740
  } catch (err) {
708
741
  if (err.name === "AbortError") {
709
- throw new Error(`Request to ${new URL(url).hostname} timed out after ${timeoutMs / 1e3}s`);
742
+ throw new CullitError(CoreErrorCode.FETCH_TIMEOUT, `Request to ${new URL(url).hostname} timed out after ${timeoutMs / 1e3}s`);
710
743
  }
711
744
  throw err;
712
745
  } finally {
@@ -746,6 +779,14 @@ async function validateLicense() {
746
779
  if (!validationUrl) {
747
780
  return { tier: "pro", valid: true };
748
781
  }
782
+ try {
783
+ const parsed = new URL(validationUrl);
784
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && parsed.hostname === "localhost")) {
785
+ return { tier: "pro", valid: true, message: "CULLIT_LICENSE_URL must use https." };
786
+ }
787
+ } catch {
788
+ return { tier: "pro", valid: true, message: "CULLIT_LICENSE_URL is not a valid URL." };
789
+ }
749
790
  try {
750
791
  const res = await fetchWithTimeout(validationUrl, {
751
792
  method: "POST",
@@ -902,6 +943,52 @@ function listPublishers() {
902
943
  return Array.from(publishers.keys());
903
944
  }
904
945
 
946
+ // src/rate-limiter.ts
947
+ var MemoryRateLimiter = class {
948
+ limit;
949
+ windowMs;
950
+ maxBuckets;
951
+ buckets = /* @__PURE__ */ new Map();
952
+ pruneTimer;
953
+ constructor(opts = {}) {
954
+ this.limit = opts.limit ?? 30;
955
+ this.windowMs = opts.windowMs ?? 6e4;
956
+ this.maxBuckets = opts.maxBuckets ?? 1e4;
957
+ this.pruneTimer = setInterval(() => {
958
+ const now = Date.now();
959
+ for (const [key, times] of this.buckets) {
960
+ const active = times.filter((t) => now - t < this.windowMs);
961
+ if (active.length === 0) this.buckets.delete(key);
962
+ else this.buckets.set(key, active);
963
+ }
964
+ }, 12e4);
965
+ this.pruneTimer.unref();
966
+ }
967
+ check(key) {
968
+ const now = Date.now();
969
+ const timestamps = this.buckets.get(key) || [];
970
+ const recent = timestamps.filter((t) => now - t < this.windowMs);
971
+ const remaining = Math.max(0, this.limit - recent.length);
972
+ const resetAt = recent.length > 0 ? Math.ceil((recent[0] + this.windowMs) / 1e3) : Math.ceil((now + this.windowMs) / 1e3);
973
+ if (recent.length >= this.limit) {
974
+ return { allowed: false, remaining: 0, resetAt };
975
+ }
976
+ if (!this.buckets.has(key) && this.buckets.size >= this.maxBuckets) {
977
+ const oldest = this.buckets.keys().next().value;
978
+ if (oldest) this.buckets.delete(oldest);
979
+ }
980
+ recent.push(now);
981
+ this.buckets.set(key, recent);
982
+ return { allowed: true, remaining: remaining - 1, resetAt };
983
+ }
984
+ reset() {
985
+ this.buckets.clear();
986
+ }
987
+ };
988
+ function createRateLimiter(opts) {
989
+ return new MemoryRateLimiter(opts);
990
+ }
991
+
905
992
  // src/index.ts
906
993
  registerCollector("local", (config) => new GitCollector(config.source?.repoPath));
907
994
  registerCollector("multi-repo", (config) => {
@@ -979,16 +1066,17 @@ async function runPipeline(from, to, config, options = {}) {
979
1066
  const license = await validateLicense();
980
1067
  if (!license.valid) {
981
1068
  if (!isProviderAllowed(config.ai.provider, license)) {
982
- throw new Error(license.message || "Invalid CULLIT_API_KEY");
1069
+ throw new CullitError(CoreErrorCode.LICENSE_INVALID, license.message || "Invalid CULLIT_API_KEY");
983
1070
  }
984
1071
  log.warn(`\u26A0 ${license.message || "Invalid CULLIT_API_KEY \u2014 running in free mode."}`);
985
1072
  }
986
1073
  if (!isProviderAllowed(config.ai.provider, license)) {
987
- throw new Error(upgradeMessage(`AI provider "${config.ai.provider}"`));
1074
+ throw new CullitError(CoreErrorCode.LICENSE_TIER_INSUFFICIENT, upgradeMessage(`AI provider "${config.ai.provider}"`));
988
1075
  }
989
1076
  const collectorFactory = getCollector(config.source.type);
990
1077
  if (!collectorFactory) {
991
- throw new Error(
1078
+ throw new CullitError(
1079
+ CoreErrorCode.PIPELINE_COLLECTOR_MISSING,
992
1080
  `Source type "${config.source.type}" is not available. ` + (config.source.type !== "local" ? "Install @cullit/licensed (private distribution) to use this source." : "Valid sources: local")
993
1081
  );
994
1082
  }
@@ -1000,7 +1088,7 @@ async function runPipeline(from, to, config, options = {}) {
1000
1088
  log.info(`\xBB Found ${diff.commits.length} ${itemLabel}${diff.filesChanged ? `, ${diff.filesChanged} files changed` : ""}`);
1001
1089
  if (diff.commits.length === 0) {
1002
1090
  const source = config.source.type === "jira" ? "Jira" : config.source.type === "linear" ? "Linear" : `${from} and ${to}`;
1003
- throw new Error(`No ${itemLabel} found from ${source}`);
1091
+ throw new CullitError(CoreErrorCode.PIPELINE_NO_CHANGES, `No ${itemLabel} found from ${source}`);
1004
1092
  }
1005
1093
  const tickets = [];
1006
1094
  const enrichmentSources = config.source.enrichment || [];
@@ -1033,7 +1121,8 @@ async function runPipeline(from, to, config, options = {}) {
1033
1121
  log.info(`\xBB Generating with ${providerName} (${modelName})...`);
1034
1122
  const generatorFactory = getGenerator(config.ai.provider);
1035
1123
  if (!generatorFactory) {
1036
- throw new Error(
1124
+ throw new CullitError(
1125
+ CoreErrorCode.PIPELINE_GENERATOR_MISSING,
1037
1126
  `AI provider "${config.ai.provider}" is not available. ` + (config.ai.provider !== "none" ? "Install @cullit/licensed (private distribution) to use AI providers." : "")
1038
1127
  );
1039
1128
  }
@@ -1112,6 +1201,8 @@ export {
1112
1201
  AI_PROVIDERS,
1113
1202
  AUDIENCES,
1114
1203
  CHANGE_CATEGORIES,
1204
+ CoreErrorCode,
1205
+ CullitError,
1115
1206
  DEFAULT_CATEGORIES,
1116
1207
  DEFAULT_MODELS,
1117
1208
  ENRICHMENT_TYPES,
@@ -1127,6 +1218,7 @@ export {
1127
1218
  VERSION,
1128
1219
  analyzeReleaseReadiness,
1129
1220
  createLogger,
1221
+ createRateLimiter,
1130
1222
  escapeHtml,
1131
1223
  fetchWithTimeout,
1132
1224
  formatNotes,
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@cullit/core",
3
- "version": "1.17.0",
3
+ "version": "1.18.1",
4
4
  "type": "module",
5
5
  "description": "Core engine for Cullit — AI-powered release note generation.",
6
- "license": "MIT",
6
+ "license": "SEE LICENSE IN LICENSE",
7
7
  "author": "Cullit <matt@cullit.io>",
8
8
  "repository": {
9
9
  "type": "git",
@@ -38,7 +38,7 @@
38
38
  "access": "public"
39
39
  },
40
40
  "dependencies": {
41
- "@cullit/config": "1.17.0"
41
+ "@cullit/config": "1.18.1"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsup src/index.ts --format esm --dts --clean",