@contentrain/mcp 3.5.0 → 3.6.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/README.md CHANGED
@@ -199,7 +199,7 @@ MCP supports three backends behind the same `RepoProvider` contract:
199
199
  - **GitHubProvider** — Octokit over the Git Data + Repos APIs. No clone, no worktree. `@octokit/rest` ships as an optional peer dependency.
200
200
  - **GitLabProvider** — gitbeaker over the GitLab REST API. No clone, no worktree. `@gitbeaker/rest` ships as an optional peer dependency. Supports gitlab.com and self-hosted CE / EE.
201
201
 
202
- Each remote provider implements the same surface: reader (readFile / listDirectory / fileExists), writer (applyPlan — one atomic commit), branch ops (list / create / delete / diff / merge / isMerged / getDefaultBranch). `mergeBranch` goes straight through on GitHub; on GitLab it opens an MR and immediately accepts it so the final `MergeResult` shape matches either way.
202
+ Each remote provider implements the same surface: reader (readFile / listDirectory / fileExists), writer (applyPlan — one atomic commit), branch ops (list / create / delete / diff / merge / isMerged / getDefaultBranch). `mergeBranch` goes straight through on GitHub; on GitLab it opens an MR and immediately accepts it so the final `MergeResult` shape matches either way. The GitHub provider also takes a full commit SHA as `applyPlan`'s `base` (and `createBranch`'s `fromRef`) for compare-and-set writes: a missing branch forks from that commit, and a branch that has moved away from it refuses the write with `PROVIDER_CONFLICT`.
203
203
 
204
204
  ### GitLab — installation & usage
205
205
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/core/ops/apply-to-worktree.ts","../../../src/core/ops/content-delete.ts","../../../src/core/ops/content-save.ts","../../../src/core/ops/model-delete.ts","../../../src/core/ops/model-save.ts","../../../src/core/ops/paths.ts","../../../src/core/bind-ref.ts","../../../src/core/ops/vocabulary-save.ts"],"mappings":";;;;;;;;;;;AAkBA;;;;;;;;;iBAAsB,sBAAA,CAAuB,QAAA,UAAkB,OAAA,WAAkB,UAAA,KAAe,OAAA;;;UCX/E,kBAAA;EACf,KAAA,EAAO,eAAA;EACP,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EDMqG;;;;ECDrG,aAAA;AAAA;AAAA,KAGU,iBAAA,GAAoB,MAAA;;;;AAbhC;;;;;;;;;;iBAuDsB,iBAAA,CACpB,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,kBAAA,GACN,OAAA,CAAQ,iBAAA;;;UCpDD,SAAA;EACR,KAAA,EAAO,eAAA;EACP,OAAA,EAAS,YAAA;EACT,MAAA,EAAQ,iBAAA;EACR,UAAA,GAAa,UAAA;EFCwF;;;;;;EEMrG,YAAA;AAAA;;;ADjBF;;;;;;;iBC6BsB,eAAA,CAAgB,MAAA,EAAQ,UAAA,EAAY,KAAA,EAAO,SAAA,GAAY,OAAA,CAAQ,eAAA;;;UChCpE,gBAAA;EACf,KAAA,EAAO,eAAA;AAAA;AAAA,KAGG,eAAA,GAAkB,MAAA;;;;;;;;;;;;;AFD9B;;iBEiBsB,eAAA,CAAgB,MAAA,EAAQ,UAAA,EAAY,KAAA,EAAO,gBAAA,GAAmB,OAAA,CAAQ,eAAA;;;UClB3E,cAAA;EACf,KAAA,EAAO,eAAA;AAAA;AAAA,UAGQ,eAAA;EACf,MAAA;EACA,EAAA;AAAA;AAAA,KAGU,aAAA,GAAgB,MAAA,CAAO,eAAA;;;;;;;;;AHRnC;iBGmBsB,aAAA,CAAc,MAAA,EAAQ,UAAA,EAAY,KAAA,EAAO,cAAA,GAAiB,OAAA,CAAQ,aAAA;;;;;;;;AJRxF;;;;;;;;;;;;iBKEgB,cAAA,CACd,KAAA,EAAO,IAAA,CAAK,eAAA;AAAA,iBAUE,eAAA,CACd,KAAA,EAAO,IAAA,CAAK,eAAA,2EACZ,MAAA;AAAA,iBAac,gBAAA,CACd,KAAA,EAAO,IAAA,CAAK,eAAA,kEACZ,MAAA,UACA,IAAA;;;;;;;;;;;iBAuBc,YAAA,CACd,KAAA,EAAO,IAAA,CAAK,eAAA,2BACZ,MAAA,UACA,aAAA,UACA,IAAA;;;;;;;;AL1DF;;;;;iBMNgB,OAAA,CAAQ,MAAA,EAAQ,UAAA,EAAY,GAAA,WAAc,UAAA;;;UCFzC,mBAAA;;;;APQjB;;EOFE,KAAA,EAAO,MAAA,SAAe,MAAA;AAAA;AAAA,UAGP,qBAAA;EPDgE;EOG/E,KAAA;AAAA;AAAA,UAGe,oBAAA;EACf,KAAA;EACA,OAAA;EACA,KAAA;AAAA;AAAA,UAGe,sBAAA;EACf,OAAA;EACA,OAAA;EACA,KAAA;AAAA;AAAA,KAGU,kBAAA,GAAqB,MAAA,CAAO,oBAAA;AAAA,KAC5B,oBAAA,GAAuB,MAAA,CAAO,sBAAA;;;;;;;ANjB1C;;iBM2EsB,kBAAA,CACpB,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,mBAAA,GACN,OAAA,CAAQ,kBAAA;;iBA2DW,oBAAA,CACpB,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,qBAAA,GACN,OAAA,CAAQ,oBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/core/ops/apply-to-worktree.ts","../../../src/core/ops/content-delete.ts","../../../src/core/ops/content-save.ts","../../../src/core/ops/model-delete.ts","../../../src/core/ops/model-save.ts","../../../src/core/ops/paths.ts","../../../src/core/bind-ref.ts","../../../src/core/ops/vocabulary-save.ts"],"mappings":";;;;;;;;;;;AAkBA;;;;;;;;;iBAAsB,sBAAA,CAAuB,QAAA,UAAkB,OAAA,WAAkB,UAAA,KAAe,OAAA;;;UCX/E,kBAAA;EACf,KAAA,EAAO,eAAA;EACP,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EDMqG;;;;ECDrG,aAAA;AAAA;AAAA,KAGU,iBAAA,GAAoB,MAAA;;;;AAbhC;;;;;;;;;;iBAuDsB,iBAAA,CACpB,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,kBAAA,GACN,OAAA,CAAQ,iBAAA;;;UCrDD,SAAA;EACR,KAAA,EAAO,eAAA;EACP,OAAA,EAAS,YAAA;EACT,MAAA,EAAQ,iBAAA;EACR,UAAA,GAAa,UAAA;EFEwF;;;;;;EEKrG,YAAA;AAAA;;;ADhBF;;;;;;;iBC4BsB,eAAA,CAAgB,MAAA,EAAQ,UAAA,EAAY,KAAA,EAAO,SAAA,GAAY,OAAA,CAAQ,eAAA;;;UC/BpE,gBAAA;EACf,KAAA,EAAO,eAAA;AAAA;AAAA,KAGG,eAAA,GAAkB,MAAA;;;;;;;;;;;;;AFD9B;;iBEiBsB,eAAA,CAAgB,MAAA,EAAQ,UAAA,EAAY,KAAA,EAAO,gBAAA,GAAmB,OAAA,CAAQ,eAAA;;;UClB3E,cAAA;EACf,KAAA,EAAO,eAAA;AAAA;AAAA,UAGQ,eAAA;EACf,MAAA;EACA,EAAA;AAAA;AAAA,KAGU,aAAA,GAAgB,MAAA,CAAO,eAAA;;;;;;;;;AHRnC;iBGmBsB,aAAA,CAAc,MAAA,EAAQ,UAAA,EAAY,KAAA,EAAO,cAAA,GAAiB,OAAA,CAAQ,aAAA;;;;;;;;AJRxF;;;;;;;;;;;;iBKEgB,cAAA,CACd,KAAA,EAAO,IAAA,CAAK,eAAA;AAAA,iBAUE,eAAA,CACd,KAAA,EAAO,IAAA,CAAK,eAAA,2EACZ,MAAA;AAAA,iBAac,gBAAA,CACd,KAAA,EAAO,IAAA,CAAK,eAAA,kEACZ,MAAA,UACA,IAAA;;;;;;;;;;;iBAuBc,YAAA,CACd,KAAA,EAAO,IAAA,CAAK,eAAA,2BACZ,MAAA,UACA,aAAA,UACA,IAAA;;;;;;;;AL1DF;;;;;iBMNgB,OAAA,CAAQ,MAAA,EAAQ,UAAA,EAAY,GAAA,WAAc,UAAA;;;UCFzC,mBAAA;;;;APQjB;;EOFE,KAAA,EAAO,MAAA,SAAe,MAAA;AAAA;AAAA,UAGP,qBAAA;EPDgE;EOG/E,KAAA;AAAA;AAAA,UAGe,oBAAA;EACf,KAAA;EACA,OAAA;EACA,KAAA;AAAA;AAAA,UAGe,sBAAA;EACf,OAAA;EACA,OAAA;EACA,KAAA;AAAA;AAAA,KAGU,kBAAA,GAAqB,MAAA,CAAO,oBAAA;AAAA,KAC5B,oBAAA,GAAuB,MAAA,CAAO,sBAAA;;;;;;;ANjB1C;;iBM2EsB,kBAAA,CACpB,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,mBAAA,GACN,OAAA,CAAQ,kBAAA;;iBA2DW,oBAAA,CACpB,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,qBAAA,GACN,OAAA,CAAQ,oBAAA"}
@@ -1,7 +1,7 @@
1
1
  import { n as applyChangesToWorktree, t as planReconcile } from "../../reconcile-B55dZqhg.mjs";
2
2
  import "../../context-BHfO4WBv.mjs";
3
3
  import { A as contentDirPath, M as documentFilePath, N as metaFilePath, j as contentFilePath } from "../../model-manager-CfwJWvz7.mjs";
4
- import { a as planModelDelete, i as planModelSave, n as planVocabularySave, o as planContentSave, r as bindRef, s as planContentDelete, t as planVocabularyDelete } from "../../ops-B2P11b8v.mjs";
4
+ import { a as planModelDelete, i as planModelSave, n as planVocabularySave, o as planContentSave, r as bindRef, s as planContentDelete, t as planVocabularyDelete } from "../../ops-PfMNR0nx.mjs";
5
5
  import "../../fs-DJv5G8uL.mjs";
6
6
  import "../../meta-manager-Cd6AfuGS.mjs";
7
7
  import "../../config-B8a4JBT1.mjs";
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import "./contracts-DfL0BfrD.mjs";
3
3
  import "./reconcile-B55dZqhg.mjs";
4
4
  import "./context-BHfO4WBv.mjs";
5
5
  import "./model-manager-CfwJWvz7.mjs";
6
- import "./ops-B2P11b8v.mjs";
6
+ import "./ops-PfMNR0nx.mjs";
7
7
  import "./fs-DJv5G8uL.mjs";
8
8
  import "./meta-manager-Cd6AfuGS.mjs";
9
9
  import "./config-B8a4JBT1.mjs";
@@ -15,7 +15,7 @@ import "./transaction-BR2jg4uj.mjs";
15
15
  import "./local-DgjDad1x.mjs";
16
16
  import "./detect-CI32ynpq.mjs";
17
17
  import "./annotations-CXHPnS-M.mjs";
18
- import { n as createServer } from "./server-CaVOG4yI.mjs";
18
+ import { n as createServer } from "./server-BRTztDnc.mjs";
19
19
  import "./errors-BmLE-JSC.mjs";
20
20
  import "./reconcile-CwS4LUbo.mjs";
21
21
  import "./scan-config-C5zzygeH.mjs";
@@ -1,7 +1,7 @@
1
1
  import { a as serializeMarkdownFrontmatter$1, i as parseMarkdownFrontmatter$1, o as canonicalStringify$1 } from "./context-BHfO4WBv.mjs";
2
2
  import { M as documentFilePath, N as metaFilePath, j as contentFilePath } from "./model-manager-CfwJWvz7.mjs";
3
3
  import { r as mergeEntryMeta } from "./meta-manager-Cd6AfuGS.mjs";
4
- import { LOCALE_PATTERN, MODEL_FIELD_ORDER, SLUG_PATTERN, canonicalStringify, generateEntryId, validateEntryId, validateLocale, validateSlug } from "@contentrain/types";
4
+ import { LOCALE_PATTERN, MODEL_FIELD_ORDER, SLUG_PATTERN, canonicalStringify, generateEntryId, rewriteEntryMedia, rewriteMarkdownMedia, validateEntryId, validateLocale, validateSlug } from "@contentrain/types";
5
5
  //#region src/core/ops/content-delete.ts
6
6
  function contentDir(model) {
7
7
  return model.content_path ?? `.contentrain/content/${model.domain}/${model.id}`;
@@ -189,89 +189,6 @@ async function pushAllMetaForModel(reader, modelId, changes) {
189
189
  });
190
190
  }
191
191
  //#endregion
192
- //#region src/core/media/media-rewrite.ts
193
- /**
194
- * Media path → delivery URL rewriting for MCP content writes.
195
- *
196
- * Ported from Studio's `server/utils/media-rewrite.ts`, with one deliberate
197
- * difference: Studio derives the delivery base from a `projectId` plus its
198
- * runtime config, whereas MCP receives the fully-qualified per-project base
199
- * directly via `RepoProvider.mediaBaseUrl`
200
- * (`{siteUrl}/api/cdn/v1/{projectId}` — the project segment is already
201
- * included). So here `toDeliveryUrl` is a single `{base}/{path}` join.
202
- *
203
- * Why this exists: Studio stores uploaded media in CDN/R2 and references it
204
- * from content by a relative storage path (`media/...`). That path renders
205
- * nowhere on its own — it must become an absolute, public delivery URL before
206
- * it reaches a browser. When an external agent writes through MCP Cloud, the
207
- * write bypasses Studio's content-engine and goes straight through
208
- * `planContentSave`, so this is where the normalization has to happen: the
209
- * git-committed value is then already a ready-to-use URL for any consumer
210
- * (`@contentrain/query`, raw markdown, a plain landing page) with no SDK.
211
- *
212
- * Only image/video/file fields (resolved via the model schema, including those
213
- * nested in object/array fields) and `media/...` src/href targets in
214
- * markdown/HTML are rewritten. External URLs (`http(s)://`, `//`, `data:`) and
215
- * already-absolute delivery URLs pass through untouched, so every entry point
216
- * is idempotent and safe to call repeatedly. In local mode (no base) media
217
- * stays a relative path — the OSS file model.
218
- */
219
- const MEDIA_FIELD_TYPES = new Set([
220
- "image",
221
- "video",
222
- "file"
223
- ]);
224
- /**
225
- * Whether a stored field value is a relative media-storage path (`media/...`)
226
- * rather than an already-absolute URL or external link. Only these are
227
- * rewritten to delivery URLs — `http(s)://`, `//`, and `data:` are left as-is.
228
- */
229
- function isStoredMediaPath(value) {
230
- return typeof value === "string" && value.startsWith("media/");
231
- }
232
- /** Join a relative media path onto the per-project delivery base. */
233
- function toDeliveryUrl(base, path) {
234
- return `${base.replace(/\/+$/, "")}/${path}`;
235
- }
236
- /**
237
- * Rewrite a stored media path to its absolute delivery URL. Non-media values
238
- * (external URLs, empty, non-strings) pass through untouched, so this is safe
239
- * to call on any field value.
240
- */
241
- function rewriteMediaUrl(base, value) {
242
- return isStoredMediaPath(value) ? toDeliveryUrl(base, value) : value;
243
- }
244
- /**
245
- * Rewrite media paths within a single field value, guided by its FieldDef so
246
- * only media fields — including those nested inside object/array fields — are
247
- * touched. Non-media values pass through.
248
- */
249
- function rewriteFieldMedia(value, field, base) {
250
- if (value == null) return value;
251
- if (MEDIA_FIELD_TYPES.has(field.type)) return rewriteMediaUrl(base, value);
252
- if (field.type === "object" && field.fields && typeof value === "object" && !Array.isArray(value)) return rewriteEntryMedia(value, field.fields, base);
253
- if (field.type === "array" && Array.isArray(value)) {
254
- const itemDef = typeof field.items === "string" ? { type: field.items } : field.items ?? null;
255
- if (!itemDef) return value;
256
- return value.map((v) => rewriteFieldMedia(v, itemDef, base));
257
- }
258
- return value;
259
- }
260
- /**
261
- * Rewrite media paths across one entry/frontmatter object using a field map.
262
- * Returns a shallow copy (the input is never mutated); a no-op when the object
263
- * has no media fields.
264
- */
265
- function rewriteEntryMedia(entry, fields, base) {
266
- const out = { ...entry };
267
- for (const [fieldId, field] of Object.entries(fields)) if (fieldId in out) out[fieldId] = rewriteFieldMedia(out[fieldId], field, base);
268
- return out;
269
- }
270
- /** Rewrite `media/...` markdown image/link targets and inline src/href attrs. */
271
- function rewriteMarkdownMedia(body, base) {
272
- return body.replace(/(\]\()(media\/[^)\s"]+)/g, (_m, open, p) => `${open}${toDeliveryUrl(base, p)}`).replace(/(\s(?:src|href)=)(["'])(media\/[^"']+)\2/gi, (_m, attr, quote, p) => `${attr}${quote}${toDeliveryUrl(base, p)}${quote}`);
273
- }
274
- //#endregion
275
192
  //#region src/core/ops/content-save.ts
276
193
  /** Keys named individually in a replacement advisory before it collapses to a count. */
277
194
  const MAX_REPORTED_KEYS = 5;
@@ -701,4 +618,4 @@ async function planVocabularyDelete(reader, input) {
701
618
  //#endregion
702
619
  export { planModelDelete as a, planModelSave as i, planVocabularySave as n, planContentSave as o, bindRef as r, planContentDelete as s, planVocabularyDelete as t };
703
620
 
704
- //# sourceMappingURL=ops-B2P11b8v.mjs.map
621
+ //# sourceMappingURL=ops-PfMNR0nx.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ops-PfMNR0nx.mjs","names":["canonicalStringify","parseMarkdownFrontmatter","serializeMarkdownFrontmatter","canonicalStringify","canonicalStringify"],"sources":["../src/core/ops/content-delete.ts","../src/core/ops/content-save.ts","../src/core/ops/model-delete.ts","../src/core/ops/model-save.ts","../src/core/bind-ref.ts","../src/core/ops/vocabulary-save.ts"],"sourcesContent":["import type { EntryMeta, ModelDefinition } from '@contentrain/types'\nimport { validateSlug } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport { canonicalStringify } from '../serialization/index.js'\nimport { contentFilePath, documentFilePath, metaFilePath } from './paths.js'\nimport type { OpPlan } from './types.js'\n\nexport interface ContentDeleteInput {\n model: ModelDefinition\n id?: string\n slug?: string\n locale?: string\n keys?: string[]\n /**\n * `config.locales.default` — where a non-i18n model's single meta record\n * lives. Required to resolve meta paths; see `metaFilePath`.\n */\n defaultLocale: string\n}\n\nexport type ContentDeletePlan = OpPlan<string[]>\n\nfunction contentDir(model: Pick<ModelDefinition, 'domain' | 'id' | 'content_path'>): string {\n return model.content_path ?? `.contentrain/content/${model.domain}/${model.id}`\n}\n\nasync function tryReadJson<T>(reader: RepoReader, path: string): Promise<T | null> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return null\n }\n}\n\nasync function discoverCollectionLocales(\n reader: RepoReader,\n model: ModelDefinition,\n): Promise<string[]> {\n const dir = contentDir(model)\n const files = await reader.listDirectory(dir)\n const strategy = model.locale_strategy ?? 'file'\n if (strategy === 'directory') {\n return files\n }\n return files\n .filter(f => f.endsWith('.json'))\n .map(f => f.replace('.json', '').replace(`${model.id}.`, ''))\n}\n\n/**\n * Build the FileChange[] required to delete content. Pure — touches no disk.\n *\n * Semantics preserved from legacy `deleteContent`:\n * - Collection: remove the entry ID from each locale's content file and its\n * meta file. Meta file is deleted outright if the removal empties it.\n * - Singleton: delete the locale's content + meta file (locale-scoped when\n * i18n, non-i18n removes whatever meta lives under `.contentrain/meta/{id}/`).\n * - Dictionary: two modes — key-scoped removal updates the file in place,\n * or locale-scoped removes the content + meta pair.\n * - Document: strategy-aware delete (file/suffix/directory/none), plus\n * meta removal under `.contentrain/meta/{id}/{slug}/`.\n */\nexport async function planContentDelete(\n reader: RepoReader,\n input: ContentDeleteInput,\n): Promise<ContentDeletePlan> {\n const { model } = input\n\n // A non-i18n model stores content locale-agnostically in data.json and keeps\n // exactly one meta record, pinned to the default locale. A locale-scoped\n // delete is therefore meaningless — and actively destructive: the locale\n // silently maps onto data.json and the default-locale meta, so passing a\n // non-default locale deletes the shared content and the wrong meta while the\n // named locale's stray meta is never touched. Reject it; stray per-locale\n // meta is cleaned up by `contentrain_validate fix:true`.\n if (!model.i18n && input.locale) {\n throw new Error(\n `Model \"${model.id}\" has i18n disabled — its content is locale-agnostic (stored in data.json), `\n + 'so a locale-scoped delete is invalid. Omit \"locale\" to delete the entry, '\n + 'or run contentrain_validate fix:true to remove stray per-locale meta files.',\n )\n }\n\n const changes: FileChange[] = []\n const removed: string[] = []\n\n switch (model.kind) {\n case 'collection': {\n if (!input.id) throw new Error('Collection delete requires an entry ID')\n\n const locales = input.locale\n ? [input.locale]\n : await discoverCollectionLocales(reader, model)\n\n for (const loc of locales) {\n const cPath = contentFilePath(model, loc)\n const data = await tryReadJson<Record<string, unknown>>(reader, cPath)\n if (!data || !(input.id in data)) continue\n delete data[input.id]\n const sorted: Record<string, unknown> = {}\n for (const k of Object.keys(data).toSorted()) sorted[k] = data[k]\n changes.push({ path: cPath, content: canonicalStringify(sorted) })\n removed.push(`content/${model.domain}/${model.id}/${loc}.json#${input.id}`)\n }\n\n for (const loc of locales) {\n // For a non-i18n model `loc` is the pseudo-locale \"data\" (discovered from\n // data.json); metaFilePath maps it onto the default locale regardless.\n const mPath = metaFilePath(model, loc, input.defaultLocale)\n const meta = await tryReadJson<Record<string, EntryMeta>>(reader, mPath)\n if (!meta || !(input.id in meta)) continue\n delete meta[input.id]\n if (Object.keys(meta).length > 0) {\n changes.push({ path: mPath, content: canonicalStringify(meta) })\n } else {\n changes.push({ path: mPath, content: null })\n }\n }\n break\n }\n\n case 'singleton': {\n if (model.i18n && !input.locale) {\n throw new Error('Singleton delete requires a locale when i18n is enabled')\n }\n const locale = input.locale ?? 'data'\n const cPath = contentFilePath(model, locale)\n changes.push({ path: cPath, content: null })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n\n if (model.i18n) {\n changes.push({ path: metaFilePath(model, locale, input.defaultLocale), content: null })\n } else {\n // Removing data.json leaves a non-i18n model with no content at all, so\n // every meta file under it goes — including strays left by older writes.\n await pushAllMetaForModel(reader, model.id, changes)\n }\n break\n }\n\n case 'dictionary': {\n if (model.i18n && !input.locale) {\n throw new Error('Dictionary delete requires a locale when i18n is enabled')\n }\n const locale = input.locale ?? 'data'\n const cPath = contentFilePath(model, locale)\n\n if (input.keys?.length) {\n const existing = (await tryReadJson<Record<string, string>>(reader, cPath)) ?? {}\n const notFound: string[] = []\n for (const key of input.keys) {\n if (key in existing) delete existing[key]\n else notFound.push(key)\n }\n if (notFound.length > 0) {\n throw new Error(`Dictionary \"${model.id}\" (${locale}): keys not found — [${notFound.join(', ')}]`)\n }\n changes.push({ path: cPath, content: canonicalStringify(existing) })\n removed.push(...input.keys.map(k => `${model.id}/${locale}:${k}`))\n } else {\n changes.push({ path: cPath, content: null })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n if (model.i18n) {\n changes.push({ path: metaFilePath(model, locale, input.defaultLocale), content: null })\n } else {\n await pushAllMetaForModel(reader, model.id, changes)\n }\n }\n break\n }\n\n case 'document': {\n if (!input.slug) throw new Error('Document delete requires a slug')\n const slugErr = validateSlug(input.slug)\n if (slugErr) throw new Error(slugErr)\n\n const dir = contentDir(model)\n const strategy = model.locale_strategy ?? 'file'\n\n if (!model.i18n) {\n changes.push({ path: `${dir}/${input.slug}.md`, content: null })\n } else if (strategy === 'file') {\n const slugDir = `${dir}/${input.slug}`\n const files = await reader.listDirectory(slugDir)\n for (const f of files) changes.push({ path: `${slugDir}/${f}`, content: null })\n } else if (input.locale) {\n changes.push({ path: documentFilePath(model, input.locale, input.slug), content: null })\n } else if (strategy === 'suffix') {\n const files = await reader.listDirectory(dir)\n for (const f of files) {\n if (f.startsWith(`${input.slug}.`) && f.endsWith('.md')) {\n changes.push({ path: `${dir}/${f}`, content: null })\n }\n }\n } else if (strategy === 'directory') {\n const subdirs = await reader.listDirectory(dir)\n for (const d of subdirs) {\n const filePath = `${dir}/${d}/${input.slug}.md`\n if (await reader.fileExists(filePath)) {\n changes.push({ path: filePath, content: null })\n }\n }\n } else if (strategy === 'none') {\n const files = await reader.listDirectory(dir)\n if (files.includes(`${input.slug}.md`)) {\n changes.push({ path: `${dir}/${input.slug}.md`, content: null })\n }\n }\n\n removed.push(`${model.content_path ?? `content/${model.domain}/${model.id}`}/${input.slug}`)\n\n const metaSlugDir = `.contentrain/meta/${model.id}/${input.slug}`\n if (input.locale) {\n changes.push({ path: `${metaSlugDir}/${input.locale}.json`, content: null })\n } else {\n const metaFiles = await reader.listDirectory(metaSlugDir)\n for (const f of metaFiles) changes.push({ path: `${metaSlugDir}/${f}`, content: null })\n }\n break\n }\n }\n\n changes.sort((a, b) => a.path.localeCompare(b.path))\n return { changes, result: removed, advisories: [] }\n}\n\nasync function pushAllMetaForModel(reader: RepoReader, modelId: string, changes: FileChange[]): Promise<void> {\n const metaDir = `.contentrain/meta/${modelId}`\n const files = await reader.listDirectory(metaDir)\n for (const f of files) changes.push({ path: `${metaDir}/${f}`, content: null })\n}\n","import type { ContentrainConfig, EntryMeta, ModelDefinition, Vocabulary } from '@contentrain/types'\nimport { generateEntryId, rewriteEntryMedia, rewriteMarkdownMedia, validateEntryId, validateLocale, validateSlug } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport type { ContentEntry } from '../content-manager.js'\nimport { canonicalStringify, parseMarkdownFrontmatter, serializeMarkdownFrontmatter } from '../serialization/index.js'\nimport type { ContentSaveEntryResult, ContentSavePlan } from './types.js'\nimport { contentFilePath, documentFilePath, metaFilePath } from './paths.js'\nimport { mergeEntryMeta } from '../meta-manager.js'\n\n/** Keys named individually in a replacement advisory before it collapses to a count. */\nconst MAX_REPORTED_KEYS = 5\n\ninterface PlanInput {\n model: ModelDefinition\n entries: ContentEntry[]\n config: ContentrainConfig\n vocabulary?: Vocabulary | null\n /**\n * Per-project public media delivery base (`RepoProvider.mediaBaseUrl`). When\n * set (hosted/cloud mode), relative `media/...` references in media fields\n * and markdown bodies are normalized to absolute delivery URLs before the\n * value is committed. Undefined in local mode — paths are kept verbatim.\n */\n mediaBaseUrl?: string\n}\n\n/**\n * Build the FileChange[] required to save a batch of content entries. The\n * plan is deterministic, pure, and does not touch disk — it reads existing\n * state through `reader` and returns changes for a single atomic commit.\n *\n * Grouping: multiple entries targeting the same (model, locale) share a\n * single content file and a single meta file. Reads are cached in-memory\n * so repeated reads of the same path during a plan are one IO.\n */\nexport async function planContentSave(reader: RepoReader, input: PlanInput): Promise<ContentSavePlan> {\n const { model, entries, config, vocabulary, mediaBaseUrl } = input\n const result: ContentSaveEntryResult[] = []\n const advisories: string[] = []\n\n // Hosted/cloud mode: normalize relative `media/...` references to absolute\n // delivery URLs so the committed value renders anywhere with no SDK. A no-op\n // in local mode (no base) or for field-less models (e.g. dictionaries).\n const normalizeMedia = (data: Record<string, unknown>): Record<string, unknown> =>\n mediaBaseUrl && model.fields ? rewriteEntryMedia(data, model.fields, mediaBaseUrl) : data\n\n // In-memory accumulators keyed by content-root-relative path.\n const contentByPath = new Map<string, unknown>()\n const metaByPath = new Map<string, unknown>()\n const markdownChanges = new Map<string, string>()\n\n async function readJsonOrEmpty<T>(path: string): Promise<T> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return {} as T\n }\n }\n\n /** Absent file and absent entry both mean \"no prior meta\" — hence undefined, not `{}`. */\n async function readJsonOrUndefined<T>(path: string): Promise<T | undefined> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return undefined\n }\n }\n\n /** Prior meta for a whole-file (non-collection) meta path, honouring the accumulator. */\n async function priorMeta(mPath: string): Promise<EntryMeta | undefined> {\n return (metaByPath.get(mPath) as EntryMeta | undefined)\n ?? await readJsonOrUndefined<EntryMeta>(mPath)\n }\n\n const defaultLocale = config.locales.default\n\n for (const entry of entries) {\n const locale = entry.locale ?? defaultLocale\n\n const localeErr = validateLocale(locale, config)\n if (localeErr) throw new Error(localeErr)\n if (entry.id) {\n const err = validateEntryId(entry.id)\n if (err) throw new Error(err)\n }\n if (entry.slug) {\n const err = validateSlug(entry.slug)\n if (err) throw new Error(err)\n }\n\n switch (model.kind) {\n case 'singleton': {\n const cPath = contentFilePath(model, locale)\n const mPath = metaFilePath(model, locale, defaultLocale)\n contentByPath.set(cPath, normalizeMedia(entry.data))\n metaByPath.set(mPath, mergeEntryMeta(await priorMeta(mPath), entry))\n result.push({ action: 'updated', locale })\n break\n }\n\n case 'collection': {\n const isNew = !entry.id\n const id = entry.id ?? generateEntryId()\n const cPath = contentFilePath(model, locale)\n const mPath = metaFilePath(model, locale, defaultLocale)\n\n const existing = (contentByPath.get(cPath) as Record<string, unknown> | undefined)\n ?? await readJsonOrEmpty<Record<string, unknown>>(cPath)\n\n const action: 'created' | 'updated' = isNew || !(id in existing) ? 'created' : 'updated'\n existing[id] = normalizeMedia(entry.data)\n\n const sorted: Record<string, unknown> = {}\n for (const key of Object.keys(existing).toSorted()) {\n sorted[key] = existing[key]\n }\n contentByPath.set(cPath, sorted)\n\n const existingMeta = (metaByPath.get(mPath) as Record<string, EntryMeta> | undefined)\n ?? await readJsonOrEmpty<Record<string, EntryMeta>>(mPath)\n existingMeta[id] = mergeEntryMeta(existingMeta[id], entry)\n metaByPath.set(mPath, existingMeta)\n\n result.push({ action, id, locale })\n break\n }\n\n case 'dictionary': {\n const cPath = contentFilePath(model, locale)\n const mPath = metaFilePath(model, locale, defaultLocale)\n\n const existing = (contentByPath.get(cPath) as Record<string, string> | undefined)\n ?? await readJsonOrEmpty<Record<string, string>>(cPath)\n\n const newData = entry.data as Record<string, string>\n const entryAdvisories: string[] = []\n\n // Overwriting a key used to be refused outright, which made the most\n // ordinary dictionary operation — correcting a translation — impossible:\n // the workaround was delete, merge, save, merge. Four operations and two\n // branches to fix one string. The refusal also advised \"include all keys\n // in a single save call\", which cannot work, because the check compares\n // values per key rather than counting them.\n //\n // Choosing a translation is a content decision, and MCP does not make\n // those. It reports them — the same way the duplicate-value case two\n // blocks below already does — and the branch diff shows the rest.\n const replaced = Object.keys(newData).filter(\n k => k in existing && existing[k] !== newData[k],\n )\n if (replaced.length > 0) {\n const shown = replaced.slice(0, MAX_REPORTED_KEYS)\n const detail = shown.map(k => `\"${k}\": \"${existing[k]}\" → \"${newData[k]}\"`).join(', ')\n const more = replaced.length > shown.length ? `, and ${replaced.length - shown.length} more` : ''\n entryAdvisories.push(\n `Dictionary \"${model.id}\" (${locale}): replaced ${replaced.length} existing value(s) — ${detail}${more}.`,\n )\n }\n\n const reverseMap = new Map<string, string>()\n for (const [k, v] of Object.entries(existing)) reverseMap.set(v, k)\n for (const [newKey, newValue] of Object.entries(newData)) {\n if (newKey in existing) continue\n const existingKey = reverseMap.get(newValue)\n if (existingKey && existingKey !== newKey) {\n entryAdvisories.push(\n `Value \"${newValue}\" already exists as key \"${existingKey}\". Consider reusing instead of creating \"${newKey}\".`,\n )\n }\n }\n\n if (vocabulary && Object.keys(vocabulary.terms).length > 0) {\n outer:\n for (const [newKey, newValue] of Object.entries(newData)) {\n if (newKey in existing) continue\n for (const translations of Object.values(vocabulary.terms)) {\n if (Object.values(translations).includes(newValue)) {\n entryAdvisories.push(\n `Value \"${newValue}\" matches a vocabulary term. Use the canonical form for consistency.`,\n )\n continue outer\n }\n }\n }\n }\n\n advisories.push(...entryAdvisories)\n\n contentByPath.set(cPath, { ...existing, ...newData })\n metaByPath.set(mPath, mergeEntryMeta(await priorMeta(mPath), entry))\n\n result.push({\n action: 'updated',\n locale,\n ...(entryAdvisories.length > 0 ? { advisories: entryAdvisories } : {}),\n })\n break\n }\n\n case 'document': {\n const entryAdvisories: string[] = []\n const slug = entry.slug ?? (entry.data['slug'] as string | undefined)\n if (!slug) throw new Error('Document entries require a slug')\n const slugErr = validateSlug(slug)\n if (slugErr) throw new Error(slugErr)\n\n const incomingFm = { ...entry.data }\n const bodySent = 'body' in incomingFm\n const incomingBody = (incomingFm['body'] as string | undefined) ?? ''\n delete incomingFm['body']\n if (!incomingFm['slug']) incomingFm['slug'] = slug\n\n const dPath = documentFilePath(model, locale, slug)\n const mPath = metaFilePath(model, locale, defaultLocale, slug)\n\n // Accumulator before disk, as the collection branch does: two entries\n // touching one document in a single call must compose, not race.\n let existingRaw: string | null = markdownChanges.get(dPath) ?? null\n if (existingRaw === null) {\n try { existingRaw = await reader.readFile(dPath) }\n catch { /* not yet */ }\n }\n // Same rule the collection branch uses: anything already present —\n // on disk or produced earlier in this plan — makes this an update.\n const action: 'created' | 'updated' = existingRaw ? 'updated' : 'created'\n\n // Merge with what is on disk, the way collections and singletons above\n // already do. Documents were the only kind that replaced instead of\n // merging, so a save that touched one frontmatter field wrote a file\n // containing only that field — and an entirely empty body. Nothing\n // reported it: the response said `valid: true`, because validation runs\n // over the plan's own output, which was internally consistent and\n // wrong. Under auto-merge that reaches the default branch directly.\n const existing = existingRaw ? parseMarkdownFrontmatter(existingRaw) : null\n const fmData = { ...existing?.frontmatter, ...incomingFm }\n\n // Absent `body` means \"I am not editing the body\" — keep what is there.\n // A `body` that is present, even empty, is an instruction, and is\n // honoured; clearing real content that way is announced rather than\n // silent, because it is indistinguishable from a templating mistake.\n const bodyContent = bodySent ? incomingBody : (existing?.body ?? '')\n if (bodySent && !incomingBody.trim() && existing?.body.trim()) {\n const notice = `Document \"${slug}\" (${locale}): body cleared — an explicit empty \"body\" replaced ${existing.body.length} characters. Omit \"body\" to leave it untouched.`\n entryAdvisories.push(notice)\n advisories.push(notice)\n }\n\n // Frontmatter media fields go through the schema-guided rewrite; the\n // markdown body has its `media/...` image/link targets rewritten too.\n const normalizedBody = mediaBaseUrl ? rewriteMarkdownMedia(bodyContent, mediaBaseUrl) : bodyContent\n markdownChanges.set(dPath, serializeMarkdownFrontmatter(normalizeMedia(fmData), normalizedBody))\n metaByPath.set(mPath, mergeEntryMeta(await priorMeta(mPath), entry))\n\n result.push({\n action,\n slug,\n locale,\n ...(entryAdvisories.length > 0 ? { advisories: entryAdvisories } : {}),\n })\n break\n }\n }\n }\n\n const changes: FileChange[] = []\n for (const [path, data] of contentByPath) {\n changes.push({ path, content: canonicalStringify(data) })\n }\n for (const [path, meta] of metaByPath) {\n changes.push({ path, content: canonicalStringify(meta) })\n }\n for (const [path, content] of markdownChanges) {\n changes.push({ path, content })\n }\n changes.sort((a, b) => a.path.localeCompare(b.path))\n\n return { changes, result, advisories }\n}\n","import type { ModelDefinition } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport type { OpPlan } from './types.js'\n\nexport interface ModelDeleteInput {\n model: ModelDefinition\n}\n\nexport type ModelDeletePlan = OpPlan<string[]>\n\n/**\n * Build the FileChange[] required to delete a model and its associated\n * content/meta files.\n *\n * The schema is at most two directory levels deep (e.g. document meta at\n * `.contentrain/meta/{id}/{slug}/{locale}.json`). The helper below walks\n * that much, which is enough for every model kind + locale strategy and\n * keeps us free of a general-purpose recursive lister for now.\n *\n * `result` is the legacy three-entry summary:\n * - `models/{id}.json`\n * - `content/{domain}/{id}/` (only if any content files existed)\n * - `meta/{id}/` (only if any meta files existed)\n */\nexport async function planModelDelete(reader: RepoReader, input: ModelDeleteInput): Promise<ModelDeletePlan> {\n const { model } = input\n const changes: FileChange[] = []\n const removed: string[] = []\n\n changes.push({ path: `.contentrain/models/${model.id}.json`, content: null })\n removed.push(`models/${model.id}.json`)\n\n const contentDir = model.content_path ?? `.contentrain/content/${model.domain}/${model.id}`\n const contentFiles = await enumerateTwoLevels(reader, contentDir)\n for (const path of contentFiles) {\n changes.push({ path, content: null })\n }\n if (contentFiles.length > 0) {\n removed.push(model.content_path ?? `content/${model.domain}/${model.id}/`)\n }\n\n const metaDir = `.contentrain/meta/${model.id}`\n const metaFiles = await enumerateTwoLevels(reader, metaDir)\n for (const path of metaFiles) {\n changes.push({ path, content: null })\n }\n if (metaFiles.length > 0) {\n removed.push(`meta/${model.id}/`)\n }\n\n changes.sort((a, b) => a.path.localeCompare(b.path))\n return { changes, result: removed, advisories: [] }\n}\n\n/** List files up to two directory levels deep. Filters to `.json` and `.md`. */\nasync function enumerateTwoLevels(reader: RepoReader, dir: string): Promise<string[]> {\n const results: string[] = []\n const entries = await reader.listDirectory(dir)\n for (const entry of entries) {\n const path = `${dir}/${entry}`\n if (entry.endsWith('.json') || entry.endsWith('.md')) {\n results.push(path)\n continue\n }\n const subEntries = await reader.listDirectory(path)\n for (const sub of subEntries) {\n if (sub.endsWith('.json') || sub.endsWith('.md')) {\n results.push(`${path}/${sub}`)\n }\n }\n }\n return results\n}\n","import type { ModelDefinition } from '@contentrain/types'\nimport { MODEL_FIELD_ORDER } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport { canonicalStringify } from '../serialization/index.js'\nimport type { OpPlan } from './types.js'\n\nexport interface ModelSaveInput {\n model: ModelDefinition\n}\n\nexport interface ModelSaveResult {\n action: 'created' | 'updated'\n id: string\n}\n\nexport type ModelSavePlan = OpPlan<ModelSaveResult>\n\n/**\n * Build the FileChange[] required to create or update a model definition.\n *\n * The only disk change is the model JSON file itself. Content and meta\n * directories are created on-demand when their first write lands — git does\n * not track empty directories, and MCP's write helpers `mkdir -p` before\n * every file write, so there is no operational reason to emit an empty-dir\n * ensure step here.\n */\nexport async function planModelSave(reader: RepoReader, input: ModelSaveInput): Promise<ModelSavePlan> {\n const { model } = input\n const modelPath = `.contentrain/models/${model.id}.json`\n\n let action: 'created' | 'updated' = 'created'\n try {\n await reader.readFile(modelPath)\n action = 'updated'\n } catch { /* not yet exists */ }\n\n const changes: FileChange[] = [\n { path: modelPath, content: canonicalStringify(model, MODEL_FIELD_ORDER) },\n ]\n\n return {\n changes,\n result: { action, id: model.id },\n advisories: [],\n }\n}\n","import type { RepoReader } from './contracts/index.js'\n\n/**\n * Pin a `RepoReader` to one ref. The reconcile planner takes three of these\n * (merge-base / contentrain / base branch) and stays entirely ignorant of\n * refs — executors and tools decide what to bind, the planner just reads.\n *\n * For readers that honour the per-call `ref?` parameter (GitHub, GitLab)\n * this is the whole story; a reader that ignores it (LocalReader reads the\n * working tree) needs a ref-aware implementation instead — see\n * `git/ref-reader.ts`.\n */\nexport function bindRef(reader: RepoReader, ref: string): RepoReader {\n return {\n readFile: path => reader.readFile(path, ref),\n listDirectory: path => reader.listDirectory(path, ref),\n fileExists: path => reader.fileExists(path, ref),\n }\n}\n","import type { Vocabulary } from '@contentrain/types'\nimport { canonicalStringify, LOCALE_PATTERN, SLUG_PATTERN } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport type { OpPlan } from './types.js'\n\nconst VOCABULARY_PATH = '.contentrain/vocabulary.json'\n\n/** Terms named individually in an advisory before it collapses to a count. */\nconst MAX_REPORTED_TERMS = 5\n\nexport interface VocabularySaveInput {\n /**\n * Terms to add or update, keyed by term slug, each holding its translations\n * keyed by locale. Merged with what is already there — a term absent from\n * this map is untouched.\n */\n terms: Record<string, Record<string, string>>\n}\n\nexport interface VocabularyDeleteInput {\n /** Term slugs to remove. A slug that is not present is reported, not an error. */\n terms: string[]\n}\n\nexport interface VocabularySaveResult {\n added: string[]\n updated: string[]\n total: number\n}\n\nexport interface VocabularyDeleteResult {\n deleted: string[]\n missing: string[]\n total: number\n}\n\nexport type VocabularySavePlan = OpPlan<VocabularySaveResult>\nexport type VocabularyDeletePlan = OpPlan<VocabularyDeleteResult>\n\nasync function readVocabulary(reader: RepoReader): Promise<Vocabulary> {\n try {\n const parsed = JSON.parse(await reader.readFile(VOCABULARY_PATH)) as Partial<Vocabulary>\n return { version: parsed.version ?? 1, terms: parsed.terms ?? {} }\n } catch {\n return { version: 1, terms: {} }\n }\n}\n\n/**\n * Validate the shape callers get wrong.\n *\n * The nesting is `term -> locale -> translation`, and it reads identically the\n * other way round, so a caller that groups by locale at the top level produces\n * a structurally valid file that matches nothing. That is not a typo anyone\n * notices — the vocabulary simply stops having any effect. Checking that outer\n * keys are term slugs and inner keys are locale codes catches the inversion at\n * the point of writing.\n */\nfunction validateTerms(terms: Record<string, Record<string, string>>): string[] {\n const errors: string[] = []\n\n for (const [term, translations] of Object.entries(terms)) {\n if (!SLUG_PATTERN.test(term)) {\n errors.push(`Term \"${term}\": must be kebab-case. The outer key is the term, not a locale.`)\n }\n if (typeof translations !== 'object' || translations === null || Array.isArray(translations)) {\n errors.push(`Term \"${term}\": expected an object of locale → translation.`)\n continue\n }\n if (Object.keys(translations).length === 0) {\n errors.push(`Term \"${term}\": has no translations.`)\n }\n for (const [locale, value] of Object.entries(translations)) {\n if (!LOCALE_PATTERN.test(locale)) {\n errors.push(\n `Term \"${term}\": \"${locale}\" is not a locale code. Terms nest as { \"${term}\": { \"en\": \"…\" } } — one term, its translations inside.`,\n )\n }\n if (typeof value !== 'string') {\n errors.push(`Term \"${term}\" (${locale}): translation must be a string, got ${typeof value}.`)\n }\n }\n }\n\n return errors\n}\n\n/**\n * Build the FileChange[] to add or update vocabulary terms.\n *\n * Vocabulary had no write tool at all: the rules forbid editing `.contentrain/`\n * by hand, and there was no supported way to obey that. This closes it through\n * the same plan/apply path every other write uses, so it lands on a `cr/*`\n * branch and follows the project's workflow like anything else.\n */\nexport async function planVocabularySave(\n reader: RepoReader,\n input: VocabularySaveInput,\n): Promise<VocabularySavePlan> {\n const errors = validateTerms(input.terms)\n if (errors.length > 0) {\n throw new Error(`Vocabulary validation failed:\\n- ${errors.join('\\n- ')}`)\n }\n\n const existing = await readVocabulary(reader)\n const added: string[] = []\n const updated: string[] = []\n const advisories: string[] = []\n\n const merged: Record<string, Record<string, string>> = { ...existing.terms }\n for (const [term, translations] of Object.entries(input.terms)) {\n const prior = existing.terms[term]\n if (!prior) {\n added.push(term)\n merged[term] = { ...translations }\n continue\n }\n updated.push(term)\n // Merge per locale: adding a Turkish translation must not drop the English.\n merged[term] = { ...prior, ...translations }\n\n const changed = Object.entries(translations).filter(([l, v]) => l in prior && prior[l] !== v)\n if (changed.length > 0) {\n const shown = changed.slice(0, MAX_REPORTED_TERMS)\n const detail = shown.map(([l, v]) => `${l}: \"${prior[l]}\" → \"${v}\"`).join(', ')\n const more = changed.length > shown.length ? `, and ${changed.length - shown.length} more` : ''\n advisories.push(`Term \"${term}\": replaced ${changed.length} translation(s) — ${detail}${more}.`)\n }\n }\n\n // A canonical term whose value duplicates another term's is the whole thing\n // this file exists to prevent, so it is worth saying out loud.\n const byValue = new Map<string, string>()\n for (const [term, translations] of Object.entries(merged)) {\n for (const value of Object.values(translations)) {\n const owner = byValue.get(value)\n if (owner && owner !== term) {\n advisories.push(`Terms \"${owner}\" and \"${term}\" share the translation \"${value}\".`)\n } else {\n byValue.set(value, term)\n }\n }\n }\n\n const next: Vocabulary = { version: existing.version, terms: merged }\n const changes: FileChange[] = [\n { path: VOCABULARY_PATH, content: canonicalStringify(next) },\n ]\n\n return {\n changes,\n result: { added, updated, total: Object.keys(merged).length },\n advisories,\n }\n}\n\n/** Build the FileChange[] to remove vocabulary terms. */\nexport async function planVocabularyDelete(\n reader: RepoReader,\n input: VocabularyDeleteInput,\n): Promise<VocabularyDeletePlan> {\n const existing = await readVocabulary(reader)\n\n const deleted: string[] = []\n const missing: string[] = []\n const remaining = { ...existing.terms }\n\n for (const term of input.terms) {\n if (term in remaining) {\n delete remaining[term]\n deleted.push(term)\n } else {\n missing.push(term)\n }\n }\n\n const advisories = missing.length > 0\n ? [`Not in the vocabulary, so nothing to delete: ${missing.join(', ')}.`]\n : []\n\n // Nothing changed — emit no FileChange, so the caller can skip the commit\n // rather than create an empty branch.\n const changes: FileChange[] = deleted.length > 0\n ? [{ path: VOCABULARY_PATH, content: canonicalStringify({ version: existing.version, terms: remaining } satisfies Vocabulary) }]\n : []\n\n return {\n changes,\n result: { deleted, missing, total: Object.keys(remaining).length },\n advisories,\n }\n}\n"],"mappings":"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"}
@@ -88,14 +88,27 @@ const GITHUB_CAPABILITIES = {
88
88
  mergeCommit: true
89
89
  };
90
90
  //#endregion
91
+ //#region src/providers/shared/refs.ts
92
+ /**
93
+ * A full commit SHA: 40 hex digits. Where a provider accepts either a branch
94
+ * name or a commit as a base, this is how it tells them apart. Only the full
95
+ * form counts: an abbreviated SHA is read as a branch name. Git would allow a
96
+ * branch named with 40 hex digits; such a name is read as a SHA here.
97
+ */
98
+ function isCommitSha(ref) {
99
+ return /^[0-9a-f]{40}$/i.test(ref);
100
+ }
101
+ //#endregion
91
102
  //#region src/providers/github/apply-plan.ts
92
103
  /**
93
104
  * Apply a plan to a GitHub repository as a single atomic commit via the
94
105
  * Git Data API. High-level flow:
95
106
  *
96
107
  * 1. Resolve the base commit SHA — either the current HEAD of the target
97
- * branch, or the HEAD of `input.base` (or the repo's default branch)
98
- * when the target branch does not yet exist.
108
+ * branch, or the HEAD of `input.base` (default: the `contentrain`
109
+ * branch) when the target branch does not yet exist. A `base` that is a
110
+ * full commit SHA is used as it is (compare-and-set, see
111
+ * `resolveBaseSha`).
99
112
  * 2. Read the base tree SHA from that commit.
100
113
  * 3. Map `input.changes` to tree entries — `content` inline for each write,
101
114
  * `sha: null` for each deletion. No per-file blob round trip: GitHub
@@ -177,18 +190,31 @@ function buildTreeEntry(repo, change) {
177
190
  };
178
191
  }
179
192
  async function resolveBaseSha(client, repo, branch, base) {
193
+ let head;
180
194
  try {
195
+ head = (await client.rest.git.getRef({
196
+ owner: repo.owner,
197
+ repo: repo.name,
198
+ ref: `heads/${branch}`
199
+ })).data.object.sha;
200
+ } catch (error) {
201
+ if (!isNotFoundError(error)) throw error;
202
+ }
203
+ if (base !== void 0 && isCommitSha(base)) {
204
+ if (head === void 0) return {
205
+ baseSha: base,
206
+ branchExists: false
207
+ };
208
+ if (head.toLowerCase() !== base.toLowerCase()) throw staleBase(branch, base, head);
181
209
  return {
182
- baseSha: (await client.rest.git.getRef({
183
- owner: repo.owner,
184
- repo: repo.name,
185
- ref: `heads/${branch}`
186
- })).data.object.sha,
210
+ baseSha: head,
187
211
  branchExists: true
188
212
  };
189
- } catch (error) {
190
- if (!isNotFoundError(error)) throw error;
191
213
  }
214
+ if (head !== void 0) return {
215
+ baseSha: head,
216
+ branchExists: true
217
+ };
192
218
  const baseRefName = base ?? CONTENTRAIN_BRANCH;
193
219
  return {
194
220
  baseSha: (await client.rest.git.getRef({
@@ -199,6 +225,10 @@ async function resolveBaseSha(client, repo, branch, base) {
199
225
  branchExists: false
200
226
  };
201
227
  }
228
+ /** A 409, so `mapProviderError` reports it as PROVIDER_CONFLICT: re-read, rebuild, retry once. */
229
+ function staleBase(branch, base, head) {
230
+ return Object.assign(/* @__PURE__ */ new Error(`Branch ${branch} is at ${head}, not at the base ${base} the write was built on`), { status: 409 });
231
+ }
202
232
  //#endregion
203
233
  //#region src/providers/github/merge-commit.ts
204
234
  /**
@@ -325,17 +355,18 @@ async function listBranches(client, repo, prefix) {
325
355
  }
326
356
  return branches;
327
357
  }
358
+ /** `fromRef` is a branch name or a full commit SHA; a SHA is used as it is, with no ref lookup. */
328
359
  async function createBranch(client, repo, name, fromRef) {
329
- const base = await client.rest.git.getRef({
360
+ const sha = isCommitSha(fromRef) ? fromRef : (await client.rest.git.getRef({
330
361
  owner: repo.owner,
331
362
  repo: repo.name,
332
363
  ref: `heads/${fromRef}`
333
- });
364
+ })).data.object.sha;
334
365
  await client.rest.git.createRef({
335
366
  owner: repo.owner,
336
367
  repo: repo.name,
337
368
  ref: `refs/heads/${name}`,
338
- sha: base.data.object.sha
369
+ sha
339
370
  });
340
371
  }
341
372
  async function deleteBranch(client, repo, name) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["listBranchesOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp","getDefaultBranchOp","getMergeBaseOp","createMergeCommitOp"],"sources":["../../../src/providers/github/app-auth.ts","../../../src/providers/github/capabilities.ts","../../../src/providers/github/apply-plan.ts","../../../src/providers/github/merge-commit.ts","../../../src/providers/github/branch-ops.ts","../../../src/providers/github/reader.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/types.ts"],"sourcesContent":["import { createPrivateKey, createSign } from 'node:crypto'\n\n/**\n * GitHub App authentication helpers.\n *\n * Two entry points:\n *\n * - {@link signAppJwt} — mint a short-lived (10 min) GitHub App JWT from\n * `appId` + `privateKey`. Used to authenticate app-level endpoints\n * (listing installations, creating installation tokens).\n *\n * - {@link exchangeInstallationToken} — exchange an app JWT for a\n * per-installation access token that can be passed straight to\n * `Octokit({ auth: token })`. Installation tokens last ~1 hour and\n * must be refreshed.\n *\n * Both helpers are pure — they never import `@octokit/rest` or\n * `@octokit/auth-app`. Callers that want auto-refresh behaviour should\n * either wire `@octokit/auth-app` as the `authStrategy` when constructing\n * their own Octokit (see the embedding guide), or call\n * `exchangeInstallationToken` on their own schedule.\n */\n\nexport interface AppAuthConfig {\n /** GitHub App ID (numeric, from the app's settings page). */\n appId: number\n /** PEM-encoded private key — the contents of the `.pem` the app issued. */\n privateKey: string\n /** Installation the token should be scoped to. */\n installationId: number\n}\n\nexport interface InstallationTokenResult {\n /** Opaque bearer token — pass to `new Octokit({ auth: token })`. */\n token: string\n /** ISO 8601 expiry. Installation tokens expire after ~1 hour. */\n expiresAt: string\n}\n\n/**\n * Sign a GitHub App JWT.\n *\n * GitHub's spec: RS256 (RSASSA-PKCS1-v1_5), 10-minute max lifetime,\n * `iat` 60 seconds in the past to cover small clock skew, `iss`\n * set to the numeric app ID.\n */\nfunction base64UrlEncode(obj: unknown): string {\n return Buffer.from(JSON.stringify(obj), 'utf8').toString('base64url')\n}\n\nexport function signAppJwt(config: Pick<AppAuthConfig, 'appId' | 'privateKey'>): string {\n const now = Math.floor(Date.now() / 1000)\n const header = { alg: 'RS256', typ: 'JWT' }\n const payload = {\n iat: now - 60,\n exp: now + 9 * 60,\n iss: String(config.appId),\n }\n\n const toSign = `${base64UrlEncode(header)}.${base64UrlEncode(payload)}`\n const keyObject = createPrivateKey({ key: config.privateKey, format: 'pem' })\n const signer = createSign('RSA-SHA256')\n signer.update(toSign)\n signer.end()\n const signature = signer.sign(keyObject).toString('base64url')\n\n return `${toSign}.${signature}`\n}\n\n/**\n * Exchange an App JWT for an installation-scoped access token by calling\n * GitHub's `POST /app/installations/{id}/access_tokens` endpoint.\n *\n * Uses `fetch` (Node ≥22 has it native) so the helper stays dependency-\n * free. Throws on non-2xx responses with the GitHub-returned message.\n */\nexport async function exchangeInstallationToken(\n config: AppAuthConfig,\n opts: { baseUrl?: string, fetchImpl?: typeof globalThis.fetch } = {},\n): Promise<InstallationTokenResult> {\n const jwt = signAppJwt(config)\n const baseUrl = opts.baseUrl ?? 'https://api.github.com'\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch\n\n const url = `${baseUrl}/app/installations/${config.installationId}/access_tokens`\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${jwt}`,\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n },\n })\n\n if (!response.ok) {\n const body = await response.text().catch(() => '')\n throw new Error(\n `GitHub installation-token exchange failed: ${response.status} ${response.statusText}${body ? ` — ${body}` : ''}`,\n )\n }\n\n const data = await response.json() as { token: string, expires_at: string }\n return { token: data.token, expiresAt: data.expires_at }\n}\n","import type { ProviderCapabilities } from '../../core/contracts/index.js'\n\n/**\n * Capability set for GitHubProvider.\n *\n * GitHub over the Git Data API has no working tree, so local worktree\n * features, source-file access and AST scans are unavailable. Push /\n * PR operations are free because every commit goes straight to the\n * remote. Branch protection detection uses the Repos API.\n *\n * Tools that require `astScan`, `sourceRead` or `sourceWrite` must\n * gracefully reject (with `capability_required`) when running against\n * a GitHubProvider — this is the mechanism behind phase 6's normalize\n * capability-gate.\n */\nexport const GITHUB_CAPABILITIES: ProviderCapabilities = {\n localWorktree: false,\n sourceRead: false,\n sourceWrite: false,\n pushRemote: true,\n branchProtection: true,\n pullRequestFallback: true,\n astScan: false,\n // Git Data API commits accept multiple parents, so a reconcile can land\n // as a true two-parent merge commit. GitLab's Commits API cannot express\n // that — its provider leaves the flag absent and callers fall back to a\n // merge-request flow.\n mergeCommit: true,\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { ApplyPlanInput, Commit, FileChange } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n// A GitHub Git-tree entry. Writes carry `content` inline (GitHub creates the\n// blob as part of createTree); deletions carry `sha: null`. The two are\n// mutually exclusive — an entry may set `content` OR `sha`, never both.\ntype TreeEntry =\n | { path: string, mode: '100644', type: 'blob', content: string }\n | { path: string, mode: '100644', type: 'blob', sha: null }\n\n/**\n * Apply a plan to a GitHub repository as a single atomic commit via the\n * Git Data API. High-level flow:\n *\n * 1. Resolve the base commit SHA — either the current HEAD of the target\n * branch, or the HEAD of `input.base` (or the repo's default branch)\n * when the target branch does not yet exist.\n * 2. Read the base tree SHA from that commit.\n * 3. Map `input.changes` to tree entries — `content` inline for each write,\n * `sha: null` for each deletion. No per-file blob round trip: GitHub\n * creates the blobs as part of `createTree`, which keeps the write to a\n * fixed 3 mutations (tree + commit + ref) regardless of file count and\n * stays under the mutation-rate secondary limit. Mirrors the GitLab\n * provider, which already inlines content in its commit actions.\n * 4. Create a new tree layered on top of the base tree with the collected\n * entries.\n * 5. Create the commit (tree, parents, author).\n * 6. Update an existing branch ref or create a new one pointing at the\n * commit.\n *\n * No working tree, no transaction — the commit is durable as soon as the\n * final ref update returns.\n */\nexport async function applyPlanToGitHub(\n client: GitHubClient,\n repo: RepoRef,\n input: ApplyPlanInput,\n): Promise<Commit> {\n const { baseSha, branchExists } = await resolveBaseSha(client, repo, input.branch, input.base)\n\n const baseCommit = await client.rest.git.getCommit({\n owner: repo.owner,\n repo: repo.name,\n commit_sha: baseSha,\n })\n const baseTreeSha = baseCommit.data.tree.sha\n\n const treeEntries = input.changes.map(change => buildTreeEntry(repo, change))\n\n const tree = await client.rest.git.createTree({\n owner: repo.owner,\n repo: repo.name,\n base_tree: baseTreeSha,\n tree: treeEntries,\n })\n\n const timestamp = new Date().toISOString()\n const commit = await client.rest.git.createCommit({\n owner: repo.owner,\n repo: repo.name,\n message: input.message,\n tree: tree.data.sha,\n parents: [baseSha],\n author: {\n name: input.author.name,\n email: input.author.email,\n date: timestamp,\n },\n })\n\n if (branchExists) {\n await client.rest.git.updateRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n sha: commit.data.sha,\n })\n } else {\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${input.branch}`,\n sha: commit.data.sha,\n })\n }\n\n return {\n sha: commit.data.sha,\n message: commit.data.message,\n author: {\n name: commit.data.author?.name ?? input.author.name,\n email: commit.data.author?.email ?? input.author.email,\n },\n timestamp: commit.data.author?.date ?? timestamp,\n }\n}\n\nfunction buildTreeEntry(repo: RepoRef, change: FileChange): TreeEntry {\n const path = resolveRepoPath(repo.contentRoot, change.path)\n if (change.content === null) {\n return { path, mode: '100644', type: 'blob', sha: null }\n }\n // Inline UTF-8 content — the write path never produces binary/base64, so\n // there is no blob-encoding branch to preserve. GitHub creates the blob\n // when the tree is created.\n return { path, mode: '100644', type: 'blob', content: change.content }\n}\n\nasync function resolveBaseSha(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string | undefined,\n): Promise<{ baseSha: string, branchExists: boolean }> {\n try {\n const ref = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${branch}`,\n })\n return { baseSha: ref.data.object.sha, branchExists: true }\n } catch (error) {\n if (!isNotFoundError(error)) throw error\n }\n\n // Invariant: feature branches always fork from the Contentrain\n // content-tracking branch. Callers that genuinely want to bypass this\n // must pass `base` explicitly. The repository's default branch\n // (main / master / trunk) is NOT the fallback — that would create a\n // split-brain where remote writes derive from a different ref than\n // local writes, which the LocalProvider transaction path forbids.\n const baseRefName = base ?? CONTENTRAIN_BRANCH\n\n const baseRef = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${baseRefName}`,\n })\n return { baseSha: baseRef.data.object.sha, branchExists: false }\n}\n","import type { Commit, CommitAuthor, FileChange } from '../../core/contracts/index.js'\nimport { resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\ntype TreeEntry =\n | { path: string, mode: '100644', type: 'blob', content: string }\n | { path: string, mode: '100644', type: 'blob', sha: null }\n\n/**\n * Merge-base of two refs via the compare API — one call answers both the\n * health question (\"how far apart are they?\") and the reconcile input\n * (\"where do the three trees meet?\").\n */\nexport async function getMergeBase(\n client: GitHubClient,\n repo: RepoRef,\n refA: string,\n refB: string,\n): Promise<string | null> {\n try {\n const compare = await client.rest.repos.compareCommitsWithBasehead({\n owner: repo.owner,\n repo: repo.name,\n basehead: `${refA}...${refB}`,\n })\n return compare.data.merge_base_commit?.sha ?? null\n } catch {\n return null\n }\n}\n\n/**\n * Two-parent merge commit via the Git Data API: tree layered on OURS'\n * tree, `parents: [ours, theirs]`, ref advanced. After it, `theirs` is an\n * ancestor of the branch and a plain fast-forward advance works again.\n *\n * IMPORTANT — the caller composes the COMPLETE tree delta versus ours.\n * The planner only owns content paths; a path that changed on theirs only\n * (source code from the base branch) must be included by the caller, or\n * the merge commit's tree would silently revert it. The compare API gives\n * both file lists; Studio's orchestrator owns that composition.\n */\nexport async function createMergeCommit(\n client: GitHubClient,\n repo: RepoRef,\n input: {\n branch: string\n ours: string\n theirs: string\n changes: FileChange[]\n message: string\n author: CommitAuthor\n },\n): Promise<Commit> {\n const ref = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n })\n if (ref.data.object.sha !== input.ours) {\n throw Object.assign(new Error(\n `createMergeCommit: \"${input.branch}\" is at ${ref.data.object.sha.slice(0, 8)}, not the expected ${input.ours.slice(0, 8)} — the branch moved since planning.`,\n ), {\n code: 'RECONCILE_STALE_OURS',\n agent_hint: 'Re-run the reconcile plan against the current tip.',\n })\n }\n\n const oursCommit = await client.rest.git.getCommit({\n owner: repo.owner,\n repo: repo.name,\n commit_sha: input.ours,\n })\n\n const treeEntries: TreeEntry[] = input.changes.map((change) => {\n const path = resolveRepoPath(repo.contentRoot, change.path)\n return change.content === null\n ? { path, mode: '100644', type: 'blob', sha: null }\n : { path, mode: '100644', type: 'blob', content: change.content }\n })\n\n const tree = treeEntries.length > 0\n ? await client.rest.git.createTree({\n owner: repo.owner,\n repo: repo.name,\n base_tree: oursCommit.data.tree.sha,\n tree: treeEntries,\n })\n : { data: { sha: oursCommit.data.tree.sha } }\n\n const timestamp = new Date().toISOString()\n const commit = await client.rest.git.createCommit({\n owner: repo.owner,\n repo: repo.name,\n message: input.message,\n tree: tree.data.sha,\n parents: [input.ours, input.theirs],\n author: {\n name: input.author.name,\n email: input.author.email,\n date: timestamp,\n },\n })\n\n await client.rest.git.updateRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n sha: commit.data.sha,\n })\n\n return {\n sha: commit.data.sha,\n message: commit.data.message,\n author: {\n name: commit.data.author?.name ?? input.author.name,\n email: commit.data.author?.email ?? input.author.email,\n },\n timestamp: commit.data.author?.date ?? timestamp,\n }\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * Branch/merge/diff helpers backed by the Repos and Git Data APIs.\n *\n * Pure functions so they can be composed into `GitHubProvider` or used\n * standalone. All throw on non-404 errors; 404s collapse to empty-ish\n * results where that matches the `RepoProvider` contract (e.g. a\n * missing branch prefix yields `[]` rather than raising).\n */\n\nexport async function getDefaultBranch(client: GitHubClient, repo: RepoRef): Promise<string> {\n const response = await client.rest.repos.get({ owner: repo.owner, repo: repo.name })\n return response.data.default_branch\n}\n\nexport async function listBranches(\n client: GitHubClient,\n repo: RepoRef,\n prefix?: string,\n): Promise<Branch[]> {\n const branches: Branch[] = []\n const iterator = client.paginate.iterator(client.rest.repos.listBranches, {\n owner: repo.owner,\n repo: repo.name,\n per_page: 100,\n })\n for await (const page of iterator) {\n for (const b of page.data) {\n if (prefix && !b.name.startsWith(prefix)) continue\n branches.push({ name: b.name, sha: b.commit.sha, protected: b.protected })\n }\n }\n return branches\n}\n\nexport async function createBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n fromRef: string,\n): Promise<void> {\n const base = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${fromRef}`,\n })\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${name}`,\n sha: base.data.object.sha,\n })\n}\n\nexport async function deleteBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n): Promise<void> {\n await client.rest.git.deleteRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${name}`,\n })\n}\n\nexport async function getBranchDiff(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base,\n head: branch,\n })\n const files = response.data.files ?? []\n return files.map(f => ({\n path: f.filename,\n status: normaliseStatus(f.status),\n before: null,\n after: null,\n }))\n}\n\nexport async function mergeBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult> {\n try {\n const response = await client.rest.repos.merge({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n const remote = await cleanupSourceBranch(client, repo, branch, into, opts)\n return { merged: true, sha: response.data.sha, pullRequestUrl: null, ...(remote ? { remote } : {}) }\n } catch (error) {\n if (isNotModified(error)) {\n const remote = await cleanupSourceBranch(client, repo, branch, into, opts)\n return { merged: true, sha: null, pullRequestUrl: null, ...(remote ? { remote } : {}) }\n }\n throw error\n }\n}\n\n/**\n * Post-merge deletion of the source branch — **opt-in**. `mergeBranch` is a\n * general merge primitive: like `git merge` and GitHub's merge API it leaves\n * the source branch alone by default. A caller that wants the merged branch\n * removed (e.g. cr/* review-branch cleanup) opts in with\n * `removeSourceBranch: true`.\n *\n * Even when opted in, a long-lived branch is NEVER deleted: not the merge\n * target (`into`), not the `contentrain` content branch, and not the repo's\n * default branch. This mirrors the LocalProvider's `deleteRemoteBranch`\n * guard and defends against head/base confusion and `contentrain→main` /\n * `main→contentrain` flows. If the default branch can't be resolved, the\n * delete is skipped (fail safe). Never throws — the merge already succeeded.\n */\nasync function cleanupSourceBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult['remote'] | undefined> {\n if (opts?.removeSourceBranch !== true) return undefined\n\n // Free guards first — no API call for the obvious protected refs.\n if (branch === into || branch === CONTENTRAIN_BRANCH) {\n return { deleted: false, skipped: 'protected' }\n }\n try {\n if (branch === await getDefaultBranch(client, repo)) {\n return { deleted: false, skipped: 'protected' }\n }\n } catch {\n // Cannot verify the default branch — refuse to delete rather than risk it.\n return { deleted: false, skipped: 'protected' }\n }\n\n try {\n await deleteBranch(client, repo, branch)\n return { deleted: true }\n } catch (error) {\n const status = (error as { status?: number }).status\n // 404/422: ref already gone (e.g. a concurrent cleanup) — expected no-op.\n if (status === 404 || status === 422) {\n return { deleted: false, skipped: 'not-found' }\n }\n return {\n deleted: false,\n warning: `Could not delete \"${branch}\": ${error instanceof Error ? error.message : String(error)}`,\n }\n }\n}\n\nexport async function isMerged(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n): Promise<boolean> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n return response.data.ahead_by === 0\n}\n\nfunction normaliseStatus(status: string): FileDiff['status'] {\n if (status === 'added') return 'added'\n if (status === 'removed') return 'removed'\n return 'modified'\n}\n\nfunction isNotModified(error: unknown): boolean {\n return typeof error === 'object' && error !== null && (error as { status?: number }).status === 204\n}\n","import type { RepoReader } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubReader — `RepoReader` backed by the GitHub Repos + Git Data APIs.\n *\n * Reads pass through `repos.getContent`; directories return a list of\n * names, files return decoded UTF-8 text. Files larger than ~1 MB are\n * fetched by blob SHA through `git.getBlob` because `getContent` omits\n * the body in that case.\n *\n * `ref` is forwarded verbatim and may be a branch name, tag name or\n * commit SHA. When omitted, GitHub resolves to the repository's default\n * branch — which is usually wrong for Contentrain flows, so callers\n * should always pass the explicit `contentrain` tracking branch.\n *\n * Pass `{ memoize: true }` to dedupe reads within one operation: a repeated\n * `(path, ref)` returns the in-flight/cached promise instead of a fresh\n * `getContent`. This is OPT-IN and only safe for a SHORT-LIVED, read-only\n * reader — a long-lived reader that outlives a write would serve stale\n * results, so the provider's own reader never enables it. Failed reads are\n * evicted so a transient error is retried, not cached forever.\n */\nexport class GitHubReader implements RepoReader {\n private readonly fileMemo = new Map<string, Promise<string>>()\n private readonly listMemo = new Map<string, Promise<string[]>>()\n private readonly existsMemo = new Map<string, Promise<boolean>>()\n\n constructor(\n private readonly client: GitHubClient,\n private readonly repo: RepoRef,\n private readonly opts?: { memoize?: boolean },\n ) {}\n\n /**\n * Run `fetch` through the given memo when memoization is enabled, keyed by\n * `(ref, repoPath)`. A rejected promise is evicted so the next call retries.\n */\n private memoized<T>(memo: Map<string, Promise<T>>, repoPath: string, ref: string | undefined, fetch: () => Promise<T>): Promise<T> {\n if (!this.opts?.memoize) return fetch()\n const key = `${ref ?? ''}:${repoPath}`\n const cached = memo.get(key)\n if (cached) return cached\n const promise = fetch()\n memo.set(key, promise)\n promise.catch(() => memo.delete(key))\n return promise\n }\n\n async readFile(path: string, ref?: string): Promise<string> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.fileMemo, repoPath, ref, () => this.readFileUncached(path, repoPath, ref))\n }\n\n private async readFileUncached(path: string, repoPath: string, ref?: string): Promise<string> {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (Array.isArray(data)) {\n throw new Error(`GitHubReader: path \"${path}\" is a directory, not a file`)\n }\n if (data.type !== 'file') {\n throw new Error(`GitHubReader: path \"${path}\" is a ${data.type}, not a file`)\n }\n\n // GitHub omits content for files > 1 MB — fall back to blob fetch.\n if (data.content === '' && data.size > 0) {\n const blob = await this.client.rest.git.getBlob({\n owner: this.repo.owner,\n repo: this.repo.name,\n file_sha: data.sha,\n })\n if (blob.data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected blob encoding \"${blob.data.encoding}\" for ${path}`)\n }\n return Buffer.from(blob.data.content, 'base64').toString('utf-8')\n }\n\n if (data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected encoding \"${data.encoding}\" for ${path}`)\n }\n return Buffer.from(data.content, 'base64').toString('utf-8')\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.listMemo, repoPath, ref, async () => {\n try {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (!Array.isArray(data)) return []\n return data.map(entry => entry.name)\n } catch (error) {\n if (isNotFoundError(error)) return []\n throw error\n }\n })\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.existsMemo, repoPath, ref, async () => {\n try {\n await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n return true\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n })\n }\n}\n","import type {\n ApplyPlanInput,\n Branch,\n Commit,\n CommitAuthor,\n FileChange,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n} from '../../core/contracts/index.js'\nimport { applyPlanToGitHub } from './apply-plan.js'\nimport { createMergeCommit as createMergeCommitOp, getMergeBase as getMergeBaseOp } from './merge-commit.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { GITHUB_CAPABILITIES } from './capabilities.js'\nimport type { GitHubClient } from './client.js'\nimport { GitHubReader } from './reader.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubProvider — `RepoProvider` backed by the Octokit-driven GitHub\n * REST + Git Data APIs.\n *\n * The provider is transport-agnostic; it only talks to an `Octokit`\n * instance passed into the constructor. The `createGitHubProvider`\n * helper in `factory.ts` wraps the dynamic import so consumers never\n * have to touch Octokit directly.\n */\nexport class GitHubProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = GITHUB_CAPABILITIES\n private readonly reader: GitHubReader\n\n constructor(\n private readonly client: GitHubClient,\n public readonly repo: RepoRef,\n ) {\n this.reader = new GitHubReader(client, repo)\n }\n\n readFile(path: string, ref?: string): Promise<string> {\n return this.reader.readFile(path, ref)\n }\n listDirectory(path: string, ref?: string): Promise<string[]> {\n return this.reader.listDirectory(path, ref)\n }\n fileExists(path: string, ref?: string): Promise<boolean> {\n return this.reader.fileExists(path, ref)\n }\n\n applyPlan(input: ApplyPlanInput): Promise<Commit> {\n return applyPlanToGitHub(this.client, this.repo, input)\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.client, this.repo, prefix)\n }\n createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? 'main'\n return createBranchOp(this.client, this.repo, name, resolved)\n }\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.client, this.repo, name)\n }\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? 'main'\n return getBranchDiffOp(this.client, this.repo, branch, resolved)\n }\n mergeBranch(branch: string, into: string, opts?: { removeSourceBranch?: boolean }): Promise<MergeResult> {\n return mergeBranchOp(this.client, this.repo, branch, into, opts)\n }\n isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? 'main'\n return isMergedOp(this.client, this.repo, branch, resolved)\n }\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.client, this.repo)\n }\n\n getMergeBase(refA: string, refB: string): Promise<string | null> {\n return getMergeBaseOp(this.client, this.repo, refA, refB)\n }\n\n createMergeCommit(input: {\n branch: string\n ours: string\n theirs: string\n changes: FileChange[]\n message: string\n author: CommitAuthor\n }): Promise<Commit> {\n return createMergeCommitOp(this.client, this.repo, input)\n }\n}\n","import type { GitHubClient } from './client.js'\nimport { exchangeInstallationToken } from './app-auth.js'\nimport { GitHubProvider } from './provider.js'\nimport type { GitHubAuth, RepoRef } from './types.js'\n\n/**\n * Create an Octokit-backed `GitHubClient` from an auth configuration.\n *\n * The `@octokit/rest` module is imported dynamically so it stays a pure\n * optional peer dependency — self-hosted MCP on stdio can run without\n * it. If the module is not installed, the import throws with a helpful\n * hint pointing the operator at the peer dependency.\n *\n * Two auth modes:\n *\n * - `pat` — personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners.\n * - `app` — GitHub App installation auth. The factory mints a short-\n * lived JWT, exchanges it for an installation token via\n * `exchangeInstallationToken`, and instantiates Octokit with the\n * resulting bearer. The returned token expires in ~1 hour; callers\n * that need auto-refresh should instead inject their own Octokit\n * built with `@octokit/auth-app`'s auth strategy and construct\n * `GitHubProvider` directly. See the embedding guide for trade-offs.\n */\nexport async function createGitHubClient(auth: GitHubAuth): Promise<GitHubClient> {\n let OctokitCtor: typeof import('@octokit/rest').Octokit\n try {\n ({ Octokit: OctokitCtor } = await import('@octokit/rest'))\n } catch (error) {\n throw new Error(\n '@octokit/rest is required for the GitHubProvider but could not be loaded. '\n + 'Install it as a peer dependency: pnpm add @octokit/rest.',\n { cause: error },\n )\n }\n\n if (auth.type === 'pat') {\n return new OctokitCtor({ auth: auth.token })\n }\n\n if (auth.type === 'app') {\n const { token } = await exchangeInstallationToken({\n appId: auth.appId,\n privateKey: auth.privateKey,\n installationId: auth.installationId,\n })\n return new OctokitCtor({ auth: token })\n }\n\n const unknown = auth as { type: string }\n throw new Error(`GitHub auth type \"${unknown.type}\" is not supported. Use \"pat\" or \"app\".`)\n}\n\n/**\n * Factory for the full provider — instantiates an Octokit client and\n * wraps it in a `GitHubProvider`. Consumers who already hold an Octokit\n * instance (HTTP server injecting shared clients, tests, etc.) should\n * instantiate `GitHubProvider` directly instead.\n */\nexport async function createGitHubProvider(opts: { auth: GitHubAuth, repo: RepoRef }): Promise<GitHubProvider> {\n const client = await createGitHubClient(opts.auth)\n return new GitHubProvider(client, opts.repo)\n}\n","import type { CommitAuthor } from '../../core/contracts/index.js'\n\n/**\n * A reference to a GitHub repository.\n *\n * `contentRoot` is the repo-relative directory prefix where Contentrain\n * content lives. For a flat content repo it stays `''`; for a monorepo\n * where Contentrain is embedded (e.g. `apps/web/.contentrain/`) it holds\n * that prefix. All reader/writer paths are joined against it.\n */\nexport interface RepoRef {\n owner: string\n name: string\n contentRoot?: string\n}\n\n/**\n * Authentication options for the GitHub provider.\n *\n * - `pat` — a personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners. Phase 5.1 ships with this mode only.\n * - `app` — GitHub App installation auth (JWT + installation token\n * exchange). Planned for Phase 5.2; see `.internal/refactor/\n * 02-studio-handoff.md` S6 for how Studio's hosted MCP plugs in.\n */\nexport type GitHubAuth =\n | { type: 'pat', token: string }\n | {\n type: 'app'\n appId: number\n privateKey: string\n installationId: number\n }\n\n/** Default author used when a call does not provide one. */\nexport const DEFAULT_GITHUB_AUTHOR: CommitAuthor = {\n name: 'Contentrain',\n email: 'ai@contentrain.io',\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["listBranchesOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp","getDefaultBranchOp","getMergeBaseOp","createMergeCommitOp"],"sources":["../../../src/providers/github/app-auth.ts","../../../src/providers/github/capabilities.ts","../../../src/providers/shared/refs.ts","../../../src/providers/github/apply-plan.ts","../../../src/providers/github/merge-commit.ts","../../../src/providers/github/branch-ops.ts","../../../src/providers/github/reader.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/types.ts"],"sourcesContent":["import { createPrivateKey, createSign } from 'node:crypto'\n\n/**\n * GitHub App authentication helpers.\n *\n * Two entry points:\n *\n * - {@link signAppJwt} — mint a short-lived (10 min) GitHub App JWT from\n * `appId` + `privateKey`. Used to authenticate app-level endpoints\n * (listing installations, creating installation tokens).\n *\n * - {@link exchangeInstallationToken} — exchange an app JWT for a\n * per-installation access token that can be passed straight to\n * `Octokit({ auth: token })`. Installation tokens last ~1 hour and\n * must be refreshed.\n *\n * Both helpers are pure — they never import `@octokit/rest` or\n * `@octokit/auth-app`. Callers that want auto-refresh behaviour should\n * either wire `@octokit/auth-app` as the `authStrategy` when constructing\n * their own Octokit (see the embedding guide), or call\n * `exchangeInstallationToken` on their own schedule.\n */\n\nexport interface AppAuthConfig {\n /** GitHub App ID (numeric, from the app's settings page). */\n appId: number\n /** PEM-encoded private key — the contents of the `.pem` the app issued. */\n privateKey: string\n /** Installation the token should be scoped to. */\n installationId: number\n}\n\nexport interface InstallationTokenResult {\n /** Opaque bearer token — pass to `new Octokit({ auth: token })`. */\n token: string\n /** ISO 8601 expiry. Installation tokens expire after ~1 hour. */\n expiresAt: string\n}\n\n/**\n * Sign a GitHub App JWT.\n *\n * GitHub's spec: RS256 (RSASSA-PKCS1-v1_5), 10-minute max lifetime,\n * `iat` 60 seconds in the past to cover small clock skew, `iss`\n * set to the numeric app ID.\n */\nfunction base64UrlEncode(obj: unknown): string {\n return Buffer.from(JSON.stringify(obj), 'utf8').toString('base64url')\n}\n\nexport function signAppJwt(config: Pick<AppAuthConfig, 'appId' | 'privateKey'>): string {\n const now = Math.floor(Date.now() / 1000)\n const header = { alg: 'RS256', typ: 'JWT' }\n const payload = {\n iat: now - 60,\n exp: now + 9 * 60,\n iss: String(config.appId),\n }\n\n const toSign = `${base64UrlEncode(header)}.${base64UrlEncode(payload)}`\n const keyObject = createPrivateKey({ key: config.privateKey, format: 'pem' })\n const signer = createSign('RSA-SHA256')\n signer.update(toSign)\n signer.end()\n const signature = signer.sign(keyObject).toString('base64url')\n\n return `${toSign}.${signature}`\n}\n\n/**\n * Exchange an App JWT for an installation-scoped access token by calling\n * GitHub's `POST /app/installations/{id}/access_tokens` endpoint.\n *\n * Uses `fetch` (Node ≥22 has it native) so the helper stays dependency-\n * free. Throws on non-2xx responses with the GitHub-returned message.\n */\nexport async function exchangeInstallationToken(\n config: AppAuthConfig,\n opts: { baseUrl?: string, fetchImpl?: typeof globalThis.fetch } = {},\n): Promise<InstallationTokenResult> {\n const jwt = signAppJwt(config)\n const baseUrl = opts.baseUrl ?? 'https://api.github.com'\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch\n\n const url = `${baseUrl}/app/installations/${config.installationId}/access_tokens`\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${jwt}`,\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n },\n })\n\n if (!response.ok) {\n const body = await response.text().catch(() => '')\n throw new Error(\n `GitHub installation-token exchange failed: ${response.status} ${response.statusText}${body ? ` — ${body}` : ''}`,\n )\n }\n\n const data = await response.json() as { token: string, expires_at: string }\n return { token: data.token, expiresAt: data.expires_at }\n}\n","import type { ProviderCapabilities } from '../../core/contracts/index.js'\n\n/**\n * Capability set for GitHubProvider.\n *\n * GitHub over the Git Data API has no working tree, so local worktree\n * features, source-file access and AST scans are unavailable. Push /\n * PR operations are free because every commit goes straight to the\n * remote. Branch protection detection uses the Repos API.\n *\n * Tools that require `astScan`, `sourceRead` or `sourceWrite` must\n * gracefully reject (with `capability_required`) when running against\n * a GitHubProvider — this is the mechanism behind phase 6's normalize\n * capability-gate.\n */\nexport const GITHUB_CAPABILITIES: ProviderCapabilities = {\n localWorktree: false,\n sourceRead: false,\n sourceWrite: false,\n pushRemote: true,\n branchProtection: true,\n pullRequestFallback: true,\n astScan: false,\n // Git Data API commits accept multiple parents, so a reconcile can land\n // as a true two-parent merge commit. GitLab's Commits API cannot express\n // that — its provider leaves the flag absent and callers fall back to a\n // merge-request flow.\n mergeCommit: true,\n}\n","/**\n * A full commit SHA: 40 hex digits. Where a provider accepts either a branch\n * name or a commit as a base, this is how it tells them apart. Only the full\n * form counts: an abbreviated SHA is read as a branch name. Git would allow a\n * branch named with 40 hex digits; such a name is read as a SHA here.\n */\nexport function isCommitSha(ref: string): boolean {\n return /^[0-9a-f]{40}$/i.test(ref)\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { ApplyPlanInput, Commit, FileChange } from '../../core/contracts/index.js'\nimport { isCommitSha, isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n// A GitHub Git-tree entry. Writes carry `content` inline (GitHub creates the\n// blob as part of createTree); deletions carry `sha: null`. The two are\n// mutually exclusive — an entry may set `content` OR `sha`, never both.\ntype TreeEntry =\n | { path: string, mode: '100644', type: 'blob', content: string }\n | { path: string, mode: '100644', type: 'blob', sha: null }\n\n/**\n * Apply a plan to a GitHub repository as a single atomic commit via the\n * Git Data API. High-level flow:\n *\n * 1. Resolve the base commit SHA — either the current HEAD of the target\n * branch, or the HEAD of `input.base` (default: the `contentrain`\n * branch) when the target branch does not yet exist. A `base` that is a\n * full commit SHA is used as it is (compare-and-set, see\n * `resolveBaseSha`).\n * 2. Read the base tree SHA from that commit.\n * 3. Map `input.changes` to tree entries — `content` inline for each write,\n * `sha: null` for each deletion. No per-file blob round trip: GitHub\n * creates the blobs as part of `createTree`, which keeps the write to a\n * fixed 3 mutations (tree + commit + ref) regardless of file count and\n * stays under the mutation-rate secondary limit. Mirrors the GitLab\n * provider, which already inlines content in its commit actions.\n * 4. Create a new tree layered on top of the base tree with the collected\n * entries.\n * 5. Create the commit (tree, parents, author).\n * 6. Update an existing branch ref or create a new one pointing at the\n * commit.\n *\n * No working tree, no transaction — the commit is durable as soon as the\n * final ref update returns.\n */\nexport async function applyPlanToGitHub(\n client: GitHubClient,\n repo: RepoRef,\n input: ApplyPlanInput,\n): Promise<Commit> {\n const { baseSha, branchExists } = await resolveBaseSha(client, repo, input.branch, input.base)\n\n const baseCommit = await client.rest.git.getCommit({\n owner: repo.owner,\n repo: repo.name,\n commit_sha: baseSha,\n })\n const baseTreeSha = baseCommit.data.tree.sha\n\n const treeEntries = input.changes.map(change => buildTreeEntry(repo, change))\n\n const tree = await client.rest.git.createTree({\n owner: repo.owner,\n repo: repo.name,\n base_tree: baseTreeSha,\n tree: treeEntries,\n })\n\n const timestamp = new Date().toISOString()\n const commit = await client.rest.git.createCommit({\n owner: repo.owner,\n repo: repo.name,\n message: input.message,\n tree: tree.data.sha,\n parents: [baseSha],\n author: {\n name: input.author.name,\n email: input.author.email,\n date: timestamp,\n },\n })\n\n if (branchExists) {\n await client.rest.git.updateRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n sha: commit.data.sha,\n })\n } else {\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${input.branch}`,\n sha: commit.data.sha,\n })\n }\n\n return {\n sha: commit.data.sha,\n message: commit.data.message,\n author: {\n name: commit.data.author?.name ?? input.author.name,\n email: commit.data.author?.email ?? input.author.email,\n },\n timestamp: commit.data.author?.date ?? timestamp,\n }\n}\n\nfunction buildTreeEntry(repo: RepoRef, change: FileChange): TreeEntry {\n const path = resolveRepoPath(repo.contentRoot, change.path)\n if (change.content === null) {\n return { path, mode: '100644', type: 'blob', sha: null }\n }\n // Inline UTF-8 content — the write path never produces binary/base64, so\n // there is no blob-encoding branch to preserve. GitHub creates the blob\n // when the tree is created.\n return { path, mode: '100644', type: 'blob', content: change.content }\n}\n\nasync function resolveBaseSha(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string | undefined,\n): Promise<{ baseSha: string, branchExists: boolean }> {\n let head: string | undefined\n try {\n const ref = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${branch}`,\n })\n head = ref.data.object.sha\n } catch (error) {\n if (!isNotFoundError(error)) throw error\n }\n\n // A commit SHA as base is a compare-and-set: the caller read the content\n // at that commit and the write must build on exactly it. A missing branch\n // forks from the SHA with no ref lookup. An existing branch must still\n // point at it; if it moved, the write is refused rather than silently\n // layered on a state the caller never read. The race between this check\n // and the ref update is closed by updateRef itself, which only\n // fast-forwards: a branch that moves in between rejects the commit.\n if (base !== undefined && isCommitSha(base)) {\n if (head === undefined) return { baseSha: base, branchExists: false }\n if (head.toLowerCase() !== base.toLowerCase()) throw staleBase(branch, base, head)\n return { baseSha: head, branchExists: true }\n }\n\n if (head !== undefined) return { baseSha: head, branchExists: true }\n\n // Invariant: feature branches always fork from the Contentrain\n // content-tracking branch. Callers that genuinely want to bypass this\n // must pass `base` explicitly. The repository's default branch\n // (main / master / trunk) is NOT the fallback — that would create a\n // split-brain where remote writes derive from a different ref than\n // local writes, which the LocalProvider transaction path forbids.\n const baseRefName = base ?? CONTENTRAIN_BRANCH\n\n const baseRef = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${baseRefName}`,\n })\n return { baseSha: baseRef.data.object.sha, branchExists: false }\n}\n\n/** A 409, so `mapProviderError` reports it as PROVIDER_CONFLICT: re-read, rebuild, retry once. */\nfunction staleBase(branch: string, base: string, head: string): Error {\n return Object.assign(\n new Error(`Branch ${branch} is at ${head}, not at the base ${base} the write was built on`),\n { status: 409 },\n )\n}\n","import type { Commit, CommitAuthor, FileChange } from '../../core/contracts/index.js'\nimport { resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\ntype TreeEntry =\n | { path: string, mode: '100644', type: 'blob', content: string }\n | { path: string, mode: '100644', type: 'blob', sha: null }\n\n/**\n * Merge-base of two refs via the compare API — one call answers both the\n * health question (\"how far apart are they?\") and the reconcile input\n * (\"where do the three trees meet?\").\n */\nexport async function getMergeBase(\n client: GitHubClient,\n repo: RepoRef,\n refA: string,\n refB: string,\n): Promise<string | null> {\n try {\n const compare = await client.rest.repos.compareCommitsWithBasehead({\n owner: repo.owner,\n repo: repo.name,\n basehead: `${refA}...${refB}`,\n })\n return compare.data.merge_base_commit?.sha ?? null\n } catch {\n return null\n }\n}\n\n/**\n * Two-parent merge commit via the Git Data API: tree layered on OURS'\n * tree, `parents: [ours, theirs]`, ref advanced. After it, `theirs` is an\n * ancestor of the branch and a plain fast-forward advance works again.\n *\n * IMPORTANT — the caller composes the COMPLETE tree delta versus ours.\n * The planner only owns content paths; a path that changed on theirs only\n * (source code from the base branch) must be included by the caller, or\n * the merge commit's tree would silently revert it. The compare API gives\n * both file lists; Studio's orchestrator owns that composition.\n */\nexport async function createMergeCommit(\n client: GitHubClient,\n repo: RepoRef,\n input: {\n branch: string\n ours: string\n theirs: string\n changes: FileChange[]\n message: string\n author: CommitAuthor\n },\n): Promise<Commit> {\n const ref = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n })\n if (ref.data.object.sha !== input.ours) {\n throw Object.assign(new Error(\n `createMergeCommit: \"${input.branch}\" is at ${ref.data.object.sha.slice(0, 8)}, not the expected ${input.ours.slice(0, 8)} — the branch moved since planning.`,\n ), {\n code: 'RECONCILE_STALE_OURS',\n agent_hint: 'Re-run the reconcile plan against the current tip.',\n })\n }\n\n const oursCommit = await client.rest.git.getCommit({\n owner: repo.owner,\n repo: repo.name,\n commit_sha: input.ours,\n })\n\n const treeEntries: TreeEntry[] = input.changes.map((change) => {\n const path = resolveRepoPath(repo.contentRoot, change.path)\n return change.content === null\n ? { path, mode: '100644', type: 'blob', sha: null }\n : { path, mode: '100644', type: 'blob', content: change.content }\n })\n\n const tree = treeEntries.length > 0\n ? await client.rest.git.createTree({\n owner: repo.owner,\n repo: repo.name,\n base_tree: oursCommit.data.tree.sha,\n tree: treeEntries,\n })\n : { data: { sha: oursCommit.data.tree.sha } }\n\n const timestamp = new Date().toISOString()\n const commit = await client.rest.git.createCommit({\n owner: repo.owner,\n repo: repo.name,\n message: input.message,\n tree: tree.data.sha,\n parents: [input.ours, input.theirs],\n author: {\n name: input.author.name,\n email: input.author.email,\n date: timestamp,\n },\n })\n\n await client.rest.git.updateRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n sha: commit.data.sha,\n })\n\n return {\n sha: commit.data.sha,\n message: commit.data.message,\n author: {\n name: commit.data.author?.name ?? input.author.name,\n email: commit.data.author?.email ?? input.author.email,\n },\n timestamp: commit.data.author?.date ?? timestamp,\n }\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport { isCommitSha } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * Branch/merge/diff helpers backed by the Repos and Git Data APIs.\n *\n * Pure functions so they can be composed into `GitHubProvider` or used\n * standalone. All throw on non-404 errors; 404s collapse to empty-ish\n * results where that matches the `RepoProvider` contract (e.g. a\n * missing branch prefix yields `[]` rather than raising).\n */\n\nexport async function getDefaultBranch(client: GitHubClient, repo: RepoRef): Promise<string> {\n const response = await client.rest.repos.get({ owner: repo.owner, repo: repo.name })\n return response.data.default_branch\n}\n\nexport async function listBranches(\n client: GitHubClient,\n repo: RepoRef,\n prefix?: string,\n): Promise<Branch[]> {\n const branches: Branch[] = []\n const iterator = client.paginate.iterator(client.rest.repos.listBranches, {\n owner: repo.owner,\n repo: repo.name,\n per_page: 100,\n })\n for await (const page of iterator) {\n for (const b of page.data) {\n if (prefix && !b.name.startsWith(prefix)) continue\n branches.push({ name: b.name, sha: b.commit.sha, protected: b.protected })\n }\n }\n return branches\n}\n\n/** `fromRef` is a branch name or a full commit SHA; a SHA is used as it is, with no ref lookup. */\nexport async function createBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n fromRef: string,\n): Promise<void> {\n const sha = isCommitSha(fromRef)\n ? fromRef\n : (await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${fromRef}`,\n })).data.object.sha\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${name}`,\n sha,\n })\n}\n\nexport async function deleteBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n): Promise<void> {\n await client.rest.git.deleteRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${name}`,\n })\n}\n\nexport async function getBranchDiff(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base,\n head: branch,\n })\n const files = response.data.files ?? []\n return files.map(f => ({\n path: f.filename,\n status: normaliseStatus(f.status),\n before: null,\n after: null,\n }))\n}\n\nexport async function mergeBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult> {\n try {\n const response = await client.rest.repos.merge({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n const remote = await cleanupSourceBranch(client, repo, branch, into, opts)\n return { merged: true, sha: response.data.sha, pullRequestUrl: null, ...(remote ? { remote } : {}) }\n } catch (error) {\n if (isNotModified(error)) {\n const remote = await cleanupSourceBranch(client, repo, branch, into, opts)\n return { merged: true, sha: null, pullRequestUrl: null, ...(remote ? { remote } : {}) }\n }\n throw error\n }\n}\n\n/**\n * Post-merge deletion of the source branch — **opt-in**. `mergeBranch` is a\n * general merge primitive: like `git merge` and GitHub's merge API it leaves\n * the source branch alone by default. A caller that wants the merged branch\n * removed (e.g. cr/* review-branch cleanup) opts in with\n * `removeSourceBranch: true`.\n *\n * Even when opted in, a long-lived branch is NEVER deleted: not the merge\n * target (`into`), not the `contentrain` content branch, and not the repo's\n * default branch. This mirrors the LocalProvider's `deleteRemoteBranch`\n * guard and defends against head/base confusion and `contentrain→main` /\n * `main→contentrain` flows. If the default branch can't be resolved, the\n * delete is skipped (fail safe). Never throws — the merge already succeeded.\n */\nasync function cleanupSourceBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult['remote'] | undefined> {\n if (opts?.removeSourceBranch !== true) return undefined\n\n // Free guards first — no API call for the obvious protected refs.\n if (branch === into || branch === CONTENTRAIN_BRANCH) {\n return { deleted: false, skipped: 'protected' }\n }\n try {\n if (branch === await getDefaultBranch(client, repo)) {\n return { deleted: false, skipped: 'protected' }\n }\n } catch {\n // Cannot verify the default branch — refuse to delete rather than risk it.\n return { deleted: false, skipped: 'protected' }\n }\n\n try {\n await deleteBranch(client, repo, branch)\n return { deleted: true }\n } catch (error) {\n const status = (error as { status?: number }).status\n // 404/422: ref already gone (e.g. a concurrent cleanup) — expected no-op.\n if (status === 404 || status === 422) {\n return { deleted: false, skipped: 'not-found' }\n }\n return {\n deleted: false,\n warning: `Could not delete \"${branch}\": ${error instanceof Error ? error.message : String(error)}`,\n }\n }\n}\n\nexport async function isMerged(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n): Promise<boolean> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n return response.data.ahead_by === 0\n}\n\nfunction normaliseStatus(status: string): FileDiff['status'] {\n if (status === 'added') return 'added'\n if (status === 'removed') return 'removed'\n return 'modified'\n}\n\nfunction isNotModified(error: unknown): boolean {\n return typeof error === 'object' && error !== null && (error as { status?: number }).status === 204\n}\n","import type { RepoReader } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubReader — `RepoReader` backed by the GitHub Repos + Git Data APIs.\n *\n * Reads pass through `repos.getContent`; directories return a list of\n * names, files return decoded UTF-8 text. Files larger than ~1 MB are\n * fetched by blob SHA through `git.getBlob` because `getContent` omits\n * the body in that case.\n *\n * `ref` is forwarded verbatim and may be a branch name, tag name or\n * commit SHA. When omitted, GitHub resolves to the repository's default\n * branch — which is usually wrong for Contentrain flows, so callers\n * should always pass the explicit `contentrain` tracking branch.\n *\n * Pass `{ memoize: true }` to dedupe reads within one operation: a repeated\n * `(path, ref)` returns the in-flight/cached promise instead of a fresh\n * `getContent`. This is OPT-IN and only safe for a SHORT-LIVED, read-only\n * reader — a long-lived reader that outlives a write would serve stale\n * results, so the provider's own reader never enables it. Failed reads are\n * evicted so a transient error is retried, not cached forever.\n */\nexport class GitHubReader implements RepoReader {\n private readonly fileMemo = new Map<string, Promise<string>>()\n private readonly listMemo = new Map<string, Promise<string[]>>()\n private readonly existsMemo = new Map<string, Promise<boolean>>()\n\n constructor(\n private readonly client: GitHubClient,\n private readonly repo: RepoRef,\n private readonly opts?: { memoize?: boolean },\n ) {}\n\n /**\n * Run `fetch` through the given memo when memoization is enabled, keyed by\n * `(ref, repoPath)`. A rejected promise is evicted so the next call retries.\n */\n private memoized<T>(memo: Map<string, Promise<T>>, repoPath: string, ref: string | undefined, fetch: () => Promise<T>): Promise<T> {\n if (!this.opts?.memoize) return fetch()\n const key = `${ref ?? ''}:${repoPath}`\n const cached = memo.get(key)\n if (cached) return cached\n const promise = fetch()\n memo.set(key, promise)\n promise.catch(() => memo.delete(key))\n return promise\n }\n\n async readFile(path: string, ref?: string): Promise<string> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.fileMemo, repoPath, ref, () => this.readFileUncached(path, repoPath, ref))\n }\n\n private async readFileUncached(path: string, repoPath: string, ref?: string): Promise<string> {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (Array.isArray(data)) {\n throw new Error(`GitHubReader: path \"${path}\" is a directory, not a file`)\n }\n if (data.type !== 'file') {\n throw new Error(`GitHubReader: path \"${path}\" is a ${data.type}, not a file`)\n }\n\n // GitHub omits content for files > 1 MB — fall back to blob fetch.\n if (data.content === '' && data.size > 0) {\n const blob = await this.client.rest.git.getBlob({\n owner: this.repo.owner,\n repo: this.repo.name,\n file_sha: data.sha,\n })\n if (blob.data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected blob encoding \"${blob.data.encoding}\" for ${path}`)\n }\n return Buffer.from(blob.data.content, 'base64').toString('utf-8')\n }\n\n if (data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected encoding \"${data.encoding}\" for ${path}`)\n }\n return Buffer.from(data.content, 'base64').toString('utf-8')\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.listMemo, repoPath, ref, async () => {\n try {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (!Array.isArray(data)) return []\n return data.map(entry => entry.name)\n } catch (error) {\n if (isNotFoundError(error)) return []\n throw error\n }\n })\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.existsMemo, repoPath, ref, async () => {\n try {\n await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n return true\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n })\n }\n}\n","import type {\n ApplyPlanInput,\n Branch,\n Commit,\n CommitAuthor,\n FileChange,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n} from '../../core/contracts/index.js'\nimport { applyPlanToGitHub } from './apply-plan.js'\nimport { createMergeCommit as createMergeCommitOp, getMergeBase as getMergeBaseOp } from './merge-commit.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { GITHUB_CAPABILITIES } from './capabilities.js'\nimport type { GitHubClient } from './client.js'\nimport { GitHubReader } from './reader.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubProvider — `RepoProvider` backed by the Octokit-driven GitHub\n * REST + Git Data APIs.\n *\n * The provider is transport-agnostic; it only talks to an `Octokit`\n * instance passed into the constructor. The `createGitHubProvider`\n * helper in `factory.ts` wraps the dynamic import so consumers never\n * have to touch Octokit directly.\n */\nexport class GitHubProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = GITHUB_CAPABILITIES\n private readonly reader: GitHubReader\n\n constructor(\n private readonly client: GitHubClient,\n public readonly repo: RepoRef,\n ) {\n this.reader = new GitHubReader(client, repo)\n }\n\n readFile(path: string, ref?: string): Promise<string> {\n return this.reader.readFile(path, ref)\n }\n listDirectory(path: string, ref?: string): Promise<string[]> {\n return this.reader.listDirectory(path, ref)\n }\n fileExists(path: string, ref?: string): Promise<boolean> {\n return this.reader.fileExists(path, ref)\n }\n\n applyPlan(input: ApplyPlanInput): Promise<Commit> {\n return applyPlanToGitHub(this.client, this.repo, input)\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.client, this.repo, prefix)\n }\n createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? 'main'\n return createBranchOp(this.client, this.repo, name, resolved)\n }\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.client, this.repo, name)\n }\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? 'main'\n return getBranchDiffOp(this.client, this.repo, branch, resolved)\n }\n mergeBranch(branch: string, into: string, opts?: { removeSourceBranch?: boolean }): Promise<MergeResult> {\n return mergeBranchOp(this.client, this.repo, branch, into, opts)\n }\n isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? 'main'\n return isMergedOp(this.client, this.repo, branch, resolved)\n }\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.client, this.repo)\n }\n\n getMergeBase(refA: string, refB: string): Promise<string | null> {\n return getMergeBaseOp(this.client, this.repo, refA, refB)\n }\n\n createMergeCommit(input: {\n branch: string\n ours: string\n theirs: string\n changes: FileChange[]\n message: string\n author: CommitAuthor\n }): Promise<Commit> {\n return createMergeCommitOp(this.client, this.repo, input)\n }\n}\n","import type { GitHubClient } from './client.js'\nimport { exchangeInstallationToken } from './app-auth.js'\nimport { GitHubProvider } from './provider.js'\nimport type { GitHubAuth, RepoRef } from './types.js'\n\n/**\n * Create an Octokit-backed `GitHubClient` from an auth configuration.\n *\n * The `@octokit/rest` module is imported dynamically so it stays a pure\n * optional peer dependency — self-hosted MCP on stdio can run without\n * it. If the module is not installed, the import throws with a helpful\n * hint pointing the operator at the peer dependency.\n *\n * Two auth modes:\n *\n * - `pat` — personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners.\n * - `app` — GitHub App installation auth. The factory mints a short-\n * lived JWT, exchanges it for an installation token via\n * `exchangeInstallationToken`, and instantiates Octokit with the\n * resulting bearer. The returned token expires in ~1 hour; callers\n * that need auto-refresh should instead inject their own Octokit\n * built with `@octokit/auth-app`'s auth strategy and construct\n * `GitHubProvider` directly. See the embedding guide for trade-offs.\n */\nexport async function createGitHubClient(auth: GitHubAuth): Promise<GitHubClient> {\n let OctokitCtor: typeof import('@octokit/rest').Octokit\n try {\n ({ Octokit: OctokitCtor } = await import('@octokit/rest'))\n } catch (error) {\n throw new Error(\n '@octokit/rest is required for the GitHubProvider but could not be loaded. '\n + 'Install it as a peer dependency: pnpm add @octokit/rest.',\n { cause: error },\n )\n }\n\n if (auth.type === 'pat') {\n return new OctokitCtor({ auth: auth.token })\n }\n\n if (auth.type === 'app') {\n const { token } = await exchangeInstallationToken({\n appId: auth.appId,\n privateKey: auth.privateKey,\n installationId: auth.installationId,\n })\n return new OctokitCtor({ auth: token })\n }\n\n const unknown = auth as { type: string }\n throw new Error(`GitHub auth type \"${unknown.type}\" is not supported. Use \"pat\" or \"app\".`)\n}\n\n/**\n * Factory for the full provider — instantiates an Octokit client and\n * wraps it in a `GitHubProvider`. Consumers who already hold an Octokit\n * instance (HTTP server injecting shared clients, tests, etc.) should\n * instantiate `GitHubProvider` directly instead.\n */\nexport async function createGitHubProvider(opts: { auth: GitHubAuth, repo: RepoRef }): Promise<GitHubProvider> {\n const client = await createGitHubClient(opts.auth)\n return new GitHubProvider(client, opts.repo)\n}\n","import type { CommitAuthor } from '../../core/contracts/index.js'\n\n/**\n * A reference to a GitHub repository.\n *\n * `contentRoot` is the repo-relative directory prefix where Contentrain\n * content lives. For a flat content repo it stays `''`; for a monorepo\n * where Contentrain is embedded (e.g. `apps/web/.contentrain/`) it holds\n * that prefix. All reader/writer paths are joined against it.\n */\nexport interface RepoRef {\n owner: string\n name: string\n contentRoot?: string\n}\n\n/**\n * Authentication options for the GitHub provider.\n *\n * - `pat` — a personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners. Phase 5.1 ships with this mode only.\n * - `app` — GitHub App installation auth (JWT + installation token\n * exchange). Planned for Phase 5.2; see `.internal/refactor/\n * 02-studio-handoff.md` S6 for how Studio's hosted MCP plugs in.\n */\nexport type GitHubAuth =\n | { type: 'pat', token: string }\n | {\n type: 'app'\n appId: number\n privateKey: string\n installationId: number\n }\n\n/** Default author used when a call does not provide one. */\nexport const DEFAULT_GITHUB_AUTHOR: CommitAuthor = {\n name: 'Contentrain',\n email: 'ai@contentrain.io',\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import "../../contracts-DfL0BfrD.mjs";
2
2
  import "../../reconcile-B55dZqhg.mjs";
3
3
  import "../../context-BHfO4WBv.mjs";
4
4
  import { k as LocalReader } from "../../model-manager-CfwJWvz7.mjs";
5
- import "../../ops-B2P11b8v.mjs";
5
+ import "../../ops-PfMNR0nx.mjs";
6
6
  import "../../fs-DJv5G8uL.mjs";
7
7
  import "../../meta-manager-Cd6AfuGS.mjs";
8
8
  import "../../config-B8a4JBT1.mjs";