@agentproto/catalog-sync 0.5.7 → 0.5.10

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/README.md CHANGED
@@ -49,6 +49,76 @@ pnpm --filter @agentproto/catalog-sync generate --check # diff vs committed; ex
49
49
  pnpm --filter @agentproto/catalog-sync generate --refresh # re-fetch pinned sources
50
50
  ```
51
51
 
52
+ ## `addedAt` convention
53
+
54
+ Some generators (currently `llm:openrouter` and `llm:requesty` — the two
55
+ producing `LLMPricing` entries) stamp an `addedAt?: string` field (ISO
56
+ `YYYY-MM-DD`) on each catalog entry: the date that model id was first seen
57
+ by a sync run.
58
+
59
+ - **Backfill rule.** When the provider source itself carries a creation
60
+ timestamp for the id (OpenRouter's and Requesty's `/v1/models` both carry
61
+ a Unix-seconds `created` field), a brand-new id is backfilled with that
62
+ date, converted to `YYYY-MM-DD`. When the source has no such field, the
63
+ id is stamped with the sync run's own date instead.
64
+ - **Never-mutated invariant.** Once an id has an `addedAt` stamp, it is
65
+ never overwritten by a later run — not even if the source's own `created`
66
+ value for that id later resolves to something different. A model id
67
+ disappearing and later reappearing keeps its ORIGINAL stamp rather than
68
+ looking newly added.
69
+ - **Mechanism: a per-generator ledger file**, committed at
70
+ `packages/catalog-sync/ledger/<generator-id>.json` (e.g.
71
+ `ledger/llm-openrouter.json`) — a flat `{ "model/id": "YYYY-MM-DD", ... }`
72
+ map, sorted keys, deterministic serialization. Each generator reads its
73
+ own ledger directly off disk (a plain `node:fs` read — `GeneratorContext`
74
+ intentionally has no general repo-read capability; the ledger is a
75
+ generator-internal concern, not part of the frozen `types.ts` contract),
76
+ merges in the current run's ids via `computeAddedAtLedger` (see
77
+ `src/added-at.ts`), and returns the updated ledger as a second entry in
78
+ its `GeneratedFiles` — so `runner.ts`'s existing diff/write logic (and
79
+ `--check`'s drift detection) covers the ledger file for free, same as any
80
+ other generated file.
81
+ - Ledger entries are **never dropped**, even for ids no longer present in a
82
+ run — the ledger records "first ever seen", not "currently present".
83
+
84
+ See `src/added-at.ts` for the implementation and `src/generators/llm-openrouter.ts` /
85
+ `src/generators/llm-requesty.ts` for how a generator wires it in.
86
+
87
+ ## `CATALOG-CHANGELOG.md`
88
+
89
+ `packages/model-catalog/CATALOG-CHANGELOG.md` is a human-readable "what
90
+ models changed" log — NOT the changesets-driven `CHANGELOG.md` next to it
91
+ (that one is package-version history, one entry per publish). It answers
92
+ "what did catalog-sync add or remove, and when" by reading top to bottom.
93
+
94
+ - **Written automatically** by `runGenerators` (`src/runner.ts`), not by
95
+ hand. After collecting every generator's output, the runner diffs each
96
+ changed `*.generated.ts` file's top-level id keys (before vs after —
97
+ see `src/changelog.ts`'s `extractRecordIds` / `diffModelIds`) and, when at
98
+ least one generator actually added or removed a model id, appends one
99
+ dated section:
100
+
101
+ ```
102
+ ## 2026-08-31
103
+
104
+ ### llm:requesty
105
+ - Added: alibaba/qwen3.8-flash, alibaba/qwen3.8-max, ...
106
+ - Removed: anthropic/claude-opus-4-1, ...
107
+ ```
108
+
109
+ - **Sections are appended at the END of the file (newest last)** — a plain,
110
+ non-destructive `+=`. This is a deliberate choice over "newest first";
111
+ it means the writer never has to parse and re-emit prior history, only
112
+ concatenate.
113
+ - **A run with no id-level drift emits nothing** — no empty section, no
114
+ touched file, no diff. Pricing-only changes (a model's price moved but no
115
+ id was added/removed) are NOT logged here; they're visible in the
116
+ generated file's own git history.
117
+ - addedAt ledger files (`ledger/*.json`) are deliberately excluded from this
118
+ diff — they never lose an id (see above), so they carry no "removed"
119
+ signal, and every "added" id they'd report is already covered by the
120
+ owning generator's own `*.generated.ts` diff.
121
+
52
122
  ## Generators
53
123
 
54
124
  ### `llm:openrouter`
@@ -66,6 +136,8 @@ Source: `https://openrouter.ai/api/v1/models` (pinned snapshot:
66
136
  (P3 spec: "plus cache fields if the source has them") — additive and
67
137
  `LLMPricing`-compatible.
68
138
  - Unpriced routes (`openrouter/auto`) are skipped.
139
+ - `addedAt` (ISO date, first-seen, never mutated) is stamped per id via the
140
+ `ledger/llm-openrouter.json` ledger — see "`addedAt` convention" above.
69
141
 
70
142
  ### `llm:requesty`
71
143
 
@@ -81,6 +153,8 @@ Source: `https://router.requesty.ai/v1/models` (pinned snapshot:
81
153
  `cacheWriteMultiplier` is never emitted.
82
154
  - Live refresh requires `REQUESTY_API_KEY` to be set; without it the generator
83
155
  reuses the committed snapshot.
156
+ - `addedAt` (ISO date, first-seen, never mutated) is stamped per id via the
157
+ `ledger/llm-requesty.json` ledger — see "`addedAt` convention" above.
84
158
 
85
159
  ### `llm:context-windows`
86
160
 
package/dist/bin.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { z } from 'zod';
3
- import { ElevenLabsVoicesSnapshotSchema, mapElevenLabsVoices, MinimaxVoicesSnapshotSchema, mapMinimaxVoices } from '@agentproto/model-catalog/providers';
4
3
  import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
5
4
  import { dirname, join, resolve } from 'path';
6
5
  import { fileURLToPath } from 'url';
6
+ import { ElevenLabsVoicesSnapshotSchema, mapElevenLabsVoices, MinimaxVoicesSnapshotSchema, mapMinimaxVoices } from '@agentproto/model-catalog/providers';
7
7
 
8
8
  /**
9
9
  * @agentproto/catalog-sync v0.1.0
@@ -16,6 +16,47 @@ import { fileURLToPath } from 'url';
16
16
  function defineGenerator(g) {
17
17
  return g;
18
18
  }
19
+ var __dirname$1 = dirname(fileURLToPath(import.meta.url));
20
+ function catalogSyncDir() {
21
+ return resolve(__dirname$1, "..");
22
+ }
23
+ function ledgerAbsPath(ledgerId) {
24
+ return join(catalogSyncDir(), "ledger", `${ledgerId}.json`);
25
+ }
26
+ function ledgerRelPath(ledgerId) {
27
+ return `packages/catalog-sync/ledger/${ledgerId}.json`;
28
+ }
29
+ function readLedger(ledgerId) {
30
+ const path = ledgerAbsPath(ledgerId);
31
+ if (!existsSync(path)) return {};
32
+ try {
33
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
34
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
35
+ return parsed;
36
+ } catch {
37
+ return {};
38
+ }
39
+ }
40
+ function serializeLedger(ledger) {
41
+ const sorted = {};
42
+ for (const id of Object.keys(ledger).sort()) sorted[id] = ledger[id];
43
+ return `${JSON.stringify(sorted, null, 2)}
44
+ `;
45
+ }
46
+ function computeAddedAtLedger(currentIds, previous, createdAt, today) {
47
+ const next = { ...previous };
48
+ for (const id of currentIds) {
49
+ if (next[id] !== void 0) continue;
50
+ next[id] = createdAt[id] ?? today;
51
+ }
52
+ return next;
53
+ }
54
+ function todayIso() {
55
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
56
+ }
57
+ function isoDateFromUnixSeconds(seconds) {
58
+ return new Date(seconds * 1e3).toISOString().slice(0, 10);
59
+ }
19
60
 
20
61
  // src/generators/llm-openrouter.ts
21
62
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
@@ -28,7 +69,8 @@ var PricingSchema = z.object({
28
69
  }).passthrough();
29
70
  var ModelSchema = z.object({
30
71
  id: z.string(),
31
- pricing: PricingSchema.optional()
72
+ pricing: PricingSchema.optional(),
73
+ created: z.number().optional()
32
74
  }).passthrough();
33
75
  var SnapshotSchema = z.object({
34
76
  data: z.array(ModelSchema)
@@ -61,6 +103,9 @@ function serializeEntry(e) {
61
103
  if (e.cacheWriteMultiplier !== void 0) {
62
104
  fields.push(`cacheWriteMultiplier: ${fmt(e.cacheWriteMultiplier)}`);
63
105
  }
106
+ if (e.addedAt !== void 0) {
107
+ fields.push(`addedAt: ${JSON.stringify(e.addedAt)}`);
108
+ }
64
109
  fields.push(`vendor: ${JSON.stringify(e.vendor)}`);
65
110
  fields.push(`provider: ${JSON.stringify(e.provider)}`);
66
111
  return `{
@@ -77,6 +122,9 @@ function serializeFile(entries) {
77
122
  "// Pricing carries provider USD (inputPer1M / outputPer1M) plus cache",
78
123
  "// multipliers (cacheReadMultiplier / cacheWriteMultiplier) derived from the",
79
124
  "// source's input_cache_read / input_cache_write per-token fields when present.",
125
+ "// addedAt is the ISO date this id was first seen by a sync run \u2014 backfilled",
126
+ "// from the source's own `created` timestamp, then NEVER mutated; see",
127
+ "// packages/catalog-sync/src/added-at.ts and the package README.",
80
128
  "",
81
129
  'import type { LLMPricing } from "./catalog.js"',
82
130
  "",
@@ -96,11 +144,13 @@ function serializeFile(entries) {
96
144
  lines.push("");
97
145
  return lines.join("\n");
98
146
  }
147
+ var LEDGER_ID = "llm-openrouter";
99
148
  async function generate(ctx) {
100
149
  const src = sources[0];
101
150
  if (!src) throw new Error("llm:openrouter: no source configured");
102
151
  const parsed = SnapshotSchema.parse(await ctx.fetchSource(src));
103
152
  const entries = {};
153
+ const createdAt = {};
104
154
  for (const model of parsed.data) {
105
155
  const pricing = model.pricing;
106
156
  if (!pricing) continue;
@@ -123,8 +173,19 @@ async function generate(ctx) {
123
173
  entry.cacheWriteMultiplier = round6(cacheWritePer1M / inputPer1M);
124
174
  }
125
175
  entries[model.id] = entry;
176
+ if (model.created !== void 0) createdAt[model.id] = isoDateFromUnixSeconds(model.created);
126
177
  }
127
- return { [OUTPUT_PATH]: serializeFile(entries) };
178
+ const ledger = computeAddedAtLedger(
179
+ Object.keys(entries),
180
+ readLedger(LEDGER_ID),
181
+ createdAt,
182
+ todayIso()
183
+ );
184
+ for (const [id, entry] of Object.entries(entries)) entry.addedAt = ledger[id];
185
+ return {
186
+ [OUTPUT_PATH]: serializeFile(entries),
187
+ [ledgerRelPath(LEDGER_ID)]: serializeLedger(ledger)
188
+ };
128
189
  }
129
190
  var sources = [{ id: "llm-openrouter", url: OPENROUTER_MODELS_URL }];
130
191
  var llmOpenRouterGenerator = defineGenerator({
@@ -140,7 +201,8 @@ var ModelSchema2 = z.object({
140
201
  input_price: z.number().optional(),
141
202
  output_price: z.number().optional(),
142
203
  cached_price: z.number().optional(),
143
- supports_caching: z.boolean().optional()
204
+ supports_caching: z.boolean().optional(),
205
+ created: z.number().optional()
144
206
  }).passthrough();
145
207
  var SnapshotSchema2 = z.object({
146
208
  data: z.array(ModelSchema2)
@@ -169,6 +231,9 @@ function serializeEntry2(e) {
169
231
  if (e.cacheReadMultiplier !== void 0) {
170
232
  fields.push(`cacheReadMultiplier: ${fmt2(e.cacheReadMultiplier)}`);
171
233
  }
234
+ if (e.addedAt !== void 0) {
235
+ fields.push(`addedAt: ${JSON.stringify(e.addedAt)}`);
236
+ }
172
237
  fields.push(`vendor: ${JSON.stringify(e.vendor)}`);
173
238
  fields.push(`provider: ${JSON.stringify(e.provider)}`);
174
239
  return `{
@@ -186,6 +251,9 @@ function serializeFile2(entries) {
186
251
  "// multiplier (cacheReadMultiplier) derived from the source's cached_price",
187
252
  "// per-token field when `supports_caching` is true. Requesty has no",
188
253
  "// cache-write price, so cacheWriteMultiplier is never emitted.",
254
+ "// addedAt is the ISO date this id was first seen by a sync run \u2014 backfilled",
255
+ "// from the source's own `created` timestamp, then NEVER mutated; see",
256
+ "// packages/catalog-sync/src/added-at.ts and the package README.",
189
257
  "",
190
258
  'import type { LLMPricing } from "./catalog.js"',
191
259
  "",
@@ -205,11 +273,13 @@ function serializeFile2(entries) {
205
273
  lines.push("");
206
274
  return lines.join("\n");
207
275
  }
276
+ var LEDGER_ID2 = "llm-requesty";
208
277
  async function generate2(ctx) {
209
278
  const src = sources2[0];
210
279
  if (!src) throw new Error("llm:requesty: no source configured");
211
280
  const parsed = SnapshotSchema2.parse(await ctx.fetchSource(src));
212
281
  const entries = {};
282
+ const createdAt = {};
213
283
  for (const model of parsed.data) {
214
284
  const inputPer1M = per1m2(model.input_price);
215
285
  const outputPer1M = per1m2(model.output_price);
@@ -228,8 +298,19 @@ async function generate2(ctx) {
228
298
  }
229
299
  }
230
300
  entries[model.id] = entry;
301
+ if (model.created !== void 0) createdAt[model.id] = isoDateFromUnixSeconds(model.created);
231
302
  }
232
- return { [OUTPUT_PATH2]: serializeFile2(entries) };
303
+ const ledger = computeAddedAtLedger(
304
+ Object.keys(entries),
305
+ readLedger(LEDGER_ID2),
306
+ createdAt,
307
+ todayIso()
308
+ );
309
+ for (const [id, entry] of Object.entries(entries)) entry.addedAt = ledger[id];
310
+ return {
311
+ [OUTPUT_PATH2]: serializeFile2(entries),
312
+ [ledgerRelPath(LEDGER_ID2)]: serializeLedger(ledger)
313
+ };
233
314
  }
234
315
  var sources2 = [
235
316
  {
@@ -373,6 +454,19 @@ var llmHuggingfaceGenerator = defineGenerator({
373
454
  sources: sources3,
374
455
  generate: generate3
375
456
  });
457
+
458
+ // src/generators/google-native-model-ids.mjs
459
+ var GOOGLE_NATIVE_MODEL_IDS = /* @__PURE__ */ new Set([
460
+ "gemini-2.5-pro",
461
+ "gemini-2.5-flash",
462
+ "gemini-2.5-flash-lite",
463
+ "gemini-3.5-flash",
464
+ "gemini-3.1-pro-preview",
465
+ "gemini-3-flash-preview",
466
+ "gemini-3.1-flash-lite"
467
+ ]);
468
+
469
+ // src/generators/llm-context-windows.ts
376
470
  var OUTPUT_PATH4 = "packages/model-catalog/src/llm/context-windows.generated.ts";
377
471
  var AnthropicModelSchema = z.object({
378
472
  id: z.string(),
@@ -397,6 +491,17 @@ var MoonshotModelSchema = z.object({
397
491
  context_length: z.number().optional()
398
492
  }).passthrough();
399
493
  var MoonshotSnapshotSchema = z.object({ data: z.array(MoonshotModelSchema) });
494
+ var MistralModelSchema = z.object({
495
+ id: z.string(),
496
+ max_context_length: z.number().optional()
497
+ }).passthrough();
498
+ var MistralSnapshotSchema = z.object({ data: z.array(MistralModelSchema) });
499
+ var OpenRouterModelSchema = z.object({
500
+ id: z.string(),
501
+ context_length: z.number().nullable().optional(),
502
+ top_provider: z.object({ max_completion_tokens: z.number().nullable().optional() }).passthrough().optional()
503
+ }).passthrough();
504
+ var OpenRouterSnapshotSchema = z.object({ data: z.array(OpenRouterModelSchema) });
400
505
  function serializeEntry4(e) {
401
506
  const fields = [`contextWindow: ${e.contextWindow}`];
402
507
  if (e.maxOutput !== void 0) fields.push(`maxOutput: ${e.maxOutput}`);
@@ -410,7 +515,12 @@ function serializeFile4(entries) {
410
515
  "// AUTO-GENERATED by @agentproto/catalog-sync (llm:context-windows).",
411
516
  "// Do not edit by hand \u2014 re-run `pnpm --filter @agentproto/catalog-sync generate --refresh`.",
412
517
  "// Sources: api.anthropic.com/v1/models, api.groq.com/openai/v1/models,",
413
- "// api.x.ai/v1/models, api.moonshot.ai/v1/models",
518
+ "// api.x.ai/v1/models, api.moonshot.ai/v1/models,",
519
+ "// api.mistral.ai/v1/models",
520
+ "// google entries: SOURCED FROM OPENROUTER (openrouter.ai/api/v1/models),",
521
+ "// Google's native API is billing-blocked \u2014 ids remapped from",
522
+ "// google/<id> to the bare native id; see the generator's module",
523
+ "// doc comment for the exact remap + skip rules.",
414
524
  "// Context window (max input tokens) + max output tokens per live model id.",
415
525
  "// NOT pricing \u2014 see LLM_PRICING_CATALOG / OPENROUTER_ROUTES in catalog.ts.",
416
526
  "",
@@ -418,7 +528,7 @@ function serializeFile4(entries) {
418
528
  " contextWindow: number",
419
529
  " maxOutput?: number",
420
530
  " displayName?: string",
421
- ' provider: "anthropic" | "groq" | "xai" | "moonshot"',
531
+ ' provider: "anthropic" | "groq" | "xai" | "moonshot" | "mistral" | "google"',
422
532
  "}",
423
533
  "",
424
534
  "export const CONTEXT_WINDOWS: Record<string, ContextWindowEntry> = {"
@@ -444,6 +554,13 @@ async function generate4(ctx) {
444
554
  provider: "anthropic"
445
555
  };
446
556
  }
557
+ for (const [id, entry] of Object.entries(entries)) {
558
+ if (entry.provider !== "anthropic") continue;
559
+ const bare = id.replace(/-\d{8}$/, "");
560
+ if (bare === id) continue;
561
+ if (bare in entries) continue;
562
+ entries[bare] = { ...entry };
563
+ }
447
564
  }
448
565
  const groqSrc = sources4.find((s) => s.id === "llm-groq");
449
566
  if (groqSrc) {
@@ -473,6 +590,40 @@ async function generate4(ctx) {
473
590
  entries[m.id] = { contextWindow: m.context_length, provider: "moonshot" };
474
591
  }
475
592
  }
593
+ const mistralSrc = sources4.find((s) => s.id === "llm-mistral");
594
+ if (mistralSrc) {
595
+ const parsed = MistralSnapshotSchema.parse(await ctx.fetchSource(mistralSrc));
596
+ for (const m of parsed.data) {
597
+ if (!m.max_context_length) continue;
598
+ entries[m.id] = { contextWindow: m.max_context_length, provider: "mistral" };
599
+ }
600
+ }
601
+ const googleSrc = sources4.find((s) => s.id === "llm-openrouter");
602
+ if (googleSrc) {
603
+ const parsed = OpenRouterSnapshotSchema.parse(await ctx.fetchSource(googleSrc));
604
+ for (const m of parsed.data) {
605
+ if (!m.id.startsWith("google/")) continue;
606
+ const nativeId = m.id.slice("google/".length);
607
+ if (m.id.includes(":")) {
608
+ console.error(
609
+ `llm:context-windows: skipping "${m.id}" \u2014 ":"-suffixed batch route, not the same billing surface as the base model.`
610
+ );
611
+ continue;
612
+ }
613
+ if (!GOOGLE_NATIVE_MODEL_IDS.has(nativeId)) {
614
+ console.error(
615
+ `llm:context-windows: skipping "${m.id}" \u2014 no confirmed native Gemini id "${nativeId}" (see GOOGLE_NATIVE_MODEL_IDS); not guessing the remap.`
616
+ );
617
+ continue;
618
+ }
619
+ if (!m.context_length) continue;
620
+ entries[nativeId] = {
621
+ contextWindow: m.context_length,
622
+ ...m.top_provider?.max_completion_tokens ? { maxOutput: m.top_provider.max_completion_tokens } : {},
623
+ provider: "google"
624
+ };
625
+ }
626
+ }
476
627
  return { [OUTPUT_PATH4]: serializeFile4(entries) };
477
628
  }
478
629
  var sources4 = [
@@ -498,6 +649,18 @@ var sources4 = [
498
649
  id: "llm-moonshot",
499
650
  url: "https://api.moonshot.ai/v1/models",
500
651
  headers: { Authorization: "Bearer env:MOONSHOT_API_KEY" }
652
+ },
653
+ {
654
+ id: "llm-mistral",
655
+ url: "https://api.mistral.ai/v1/models",
656
+ headers: { Authorization: "Bearer env:MISTRAL_API_KEY" }
657
+ },
658
+ // Google via OpenRouter (no Google key needed — see module doc comment).
659
+ // Same source id/url as `llm-openrouter.ts`'s own generator, so both share
660
+ // the one pinned `snapshots/llm-openrouter.json` instead of fetching twice.
661
+ {
662
+ id: "llm-openrouter",
663
+ url: "https://openrouter.ai/api/v1/models"
501
664
  }
502
665
  ];
503
666
  var llmContextWindowsGenerator = defineGenerator({
@@ -879,22 +1042,70 @@ var imageReplicate = defineGenerator({
879
1042
  return { [outputPath]: lines.join("\n") };
880
1043
  }
881
1044
  });
882
- var __dirname$1 = dirname(fileURLToPath(import.meta.url));
883
- function catalogSyncDir() {
884
- return resolve(__dirname$1, "..");
1045
+
1046
+ // src/changelog.ts
1047
+ var RECORD_KEY_RE = /^ {2}"([^"]+)":\s*\{/gm;
1048
+ function extractRecordIds(source) {
1049
+ const ids = /* @__PURE__ */ new Set();
1050
+ for (const m of source.matchAll(RECORD_KEY_RE)) {
1051
+ if (m[1] !== void 0) ids.add(m[1]);
1052
+ }
1053
+ return ids;
1054
+ }
1055
+ function diffModelIds(before, after) {
1056
+ const beforeIds = before !== void 0 ? extractRecordIds(before) : /* @__PURE__ */ new Set();
1057
+ const afterIds = extractRecordIds(after);
1058
+ const added = [...afterIds].filter((id) => !beforeIds.has(id)).sort();
1059
+ const removed = [...beforeIds].filter((id) => !afterIds.has(id)).sort();
1060
+ return { added, removed };
1061
+ }
1062
+ function renderChangelogSection(date, entries) {
1063
+ const nonEmpty = entries.filter((e) => e.added.length > 0 || e.removed.length > 0);
1064
+ if (nonEmpty.length === 0) return "";
1065
+ const lines = [`## ${date}`, ""];
1066
+ for (const e of nonEmpty) {
1067
+ lines.push(`### ${e.generator}`);
1068
+ if (e.added.length > 0) lines.push(`- Added: ${e.added.join(", ")}`);
1069
+ if (e.removed.length > 0) lines.push(`- Removed: ${e.removed.join(", ")}`);
1070
+ lines.push("");
1071
+ }
1072
+ return lines.join("\n").replace(/\n+$/, "\n");
1073
+ }
1074
+ var CATALOG_CHANGELOG_HEADER = `# Catalog Changelog
1075
+
1076
+ Human-readable log of model additions/removals detected by
1077
+ \`@agentproto/catalog-sync\` runs \u2014 NOT \`CHANGELOG.md\` next to it (that one is
1078
+ changesets-driven package-version history). A dated section is appended
1079
+ automatically here whenever a sync run adds or removes model ids; sections
1080
+ are appended at the END of the file (newest last). Pricing-only drift (no id
1081
+ added/removed) is not logged here \u2014 see the generated files' own git history
1082
+ for that.
1083
+ `;
1084
+ function appendChangelog(existing, date, entries) {
1085
+ const section = renderChangelogSection(date, entries);
1086
+ if (section === "") return void 0;
1087
+ const base = existing !== void 0 && existing.length > 0 ? existing.replace(/\n+$/, "\n") : CATALOG_CHANGELOG_HEADER;
1088
+ return `${base}
1089
+ ${section}`;
1090
+ }
1091
+
1092
+ // src/runner.ts
1093
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
1094
+ function catalogSyncDir2() {
1095
+ return resolve(__dirname2, "..");
885
1096
  }
886
1097
  function snapshotPath(id) {
887
- return join(catalogSyncDir(), "snapshots", `${id}.json`);
1098
+ return join(catalogSyncDir2(), "snapshots", `${id}.json`);
888
1099
  }
889
1100
  function repoRoot() {
890
- let dir = catalogSyncDir();
1101
+ let dir = catalogSyncDir2();
891
1102
  for (let i = 0; i < 12; i++) {
892
1103
  if (existsSync(join(dir, "pnpm-workspace.yaml"))) return dir;
893
1104
  const parent = dirname(dir);
894
1105
  if (parent === dir) break;
895
1106
  dir = parent;
896
1107
  }
897
- return dirname(dirname(catalogSyncDir()));
1108
+ return dirname(dirname(catalogSyncDir2()));
898
1109
  }
899
1110
  function readIfExists(absPath) {
900
1111
  if (!existsSync(absPath)) return void 0;
@@ -916,8 +1127,13 @@ function resolveHeaders(headers) {
916
1127
  }
917
1128
  return { headers: out, missing };
918
1129
  }
1130
+ var CATALOG_CHANGELOG_PATH = "packages/model-catalog/CATALOG-CHANGELOG.md";
1131
+ function isChangelogEligible(relPath) {
1132
+ return relPath.endsWith(".generated.ts");
1133
+ }
919
1134
  async function runGenerators(gens, opts) {
920
1135
  const files2 = {};
1136
+ const pathToGenerator = {};
921
1137
  for (const gen of gens) {
922
1138
  const ctx = {
923
1139
  refresh: opts.refresh,
@@ -964,10 +1180,12 @@ async function runGenerators(gens, opts) {
964
1180
  const out = await gen.generate(ctx);
965
1181
  for (const [path, content] of Object.entries(out)) {
966
1182
  files2[path] = content;
1183
+ pathToGenerator[path] = gen.name;
967
1184
  }
968
1185
  }
969
1186
  const root = repoRoot();
970
1187
  const changed2 = [];
1188
+ const changelogEntries = [];
971
1189
  for (const [relPath, content] of Object.entries(files2)) {
972
1190
  const abs = join(root, relPath);
973
1191
  const existing = readIfExists(abs);
@@ -978,6 +1196,23 @@ async function runGenerators(gens, opts) {
978
1196
  writeFileSync(abs, content, "utf8");
979
1197
  }
980
1198
  }
1199
+ if (isChangelogEligible(relPath)) {
1200
+ const { added, removed } = diffModelIds(existing, content);
1201
+ if (added.length > 0 || removed.length > 0) {
1202
+ changelogEntries.push({ generator: pathToGenerator[relPath] ?? relPath, added, removed });
1203
+ }
1204
+ }
1205
+ }
1206
+ const changelogAbs = join(root, CATALOG_CHANGELOG_PATH);
1207
+ const changelogExisting = readIfExists(changelogAbs);
1208
+ const newChangelog = appendChangelog(changelogExisting, todayIso(), changelogEntries);
1209
+ if (newChangelog !== void 0) {
1210
+ files2[CATALOG_CHANGELOG_PATH] = newChangelog;
1211
+ changed2.push(CATALOG_CHANGELOG_PATH);
1212
+ if (opts.write) {
1213
+ mkdirSync(dirname(changelogAbs), { recursive: true });
1214
+ writeFileSync(changelogAbs, newChangelog, "utf8");
1215
+ }
981
1216
  }
982
1217
  return { files: files2, changed: changed2 };
983
1218
  }