@contentrain/mcp 1.10.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{apply-manager-UXUTYFZn.mjs → apply-manager-BpKGvXmn.mjs} +7 -7
- package/dist/apply-manager-BpKGvXmn.mjs.map +1 -0
- package/dist/{branch-lifecycle-Dd84lx37.mjs → branch-lifecycle-BAfgSQBv.mjs} +2 -2
- package/dist/{branch-lifecycle-Dd84lx37.mjs.map → branch-lifecycle-BAfgSQBv.mjs.map} +1 -1
- package/dist/{config-DlFcTxFG.mjs → config-oxxgznz7.mjs} +1 -1
- package/dist/{config-DlFcTxFG.mjs.map → config-oxxgznz7.mjs.map} +1 -1
- package/dist/{content-manager-DcmNwvmq.d.mts → content-manager-BgxF3dHu.d.mts} +3 -1
- package/dist/content-manager-BgxF3dHu.d.mts.map +1 -0
- package/dist/{context-CYN__o3Q.mjs → context-Cppz4R65.mjs} +3 -3
- package/dist/{context-CYN__o3Q.mjs.map → context-Cppz4R65.mjs.map} +1 -1
- package/dist/core/apply-manager.mjs +7 -7
- package/dist/core/config.mjs +1 -1
- package/dist/core/content-manager.d.mts +1 -1
- package/dist/core/content-manager.mjs +2 -2
- package/dist/core/context.mjs +4 -4
- package/dist/core/doctor.d.mts.map +1 -1
- package/dist/core/doctor.mjs +5 -5
- package/dist/core/meta-manager.d.mts +29 -1
- package/dist/core/meta-manager.d.mts.map +1 -1
- package/dist/core/meta-manager.mjs +2 -2
- package/dist/core/model-manager.d.mts +13 -4
- package/dist/core/model-manager.d.mts.map +1 -1
- package/dist/core/model-manager.mjs +2 -2
- package/dist/core/ops/index.d.mts +17 -2
- package/dist/core/ops/index.d.mts.map +1 -1
- package/dist/core/ops/index.mjs +4 -2
- package/dist/core/validator/index.d.mts.map +1 -1
- package/dist/core/validator/index.mjs +4 -4
- package/dist/{doctor-BwSJW1sS.mjs → doctor-D3usxlom.mjs} +37 -11
- package/dist/doctor-D3usxlom.mjs.map +1 -0
- package/dist/git/branch-lifecycle.mjs +2 -2
- package/dist/git/transaction.mjs +6 -6
- package/dist/index.mjs +12 -12
- package/dist/{local-BIE23GuI.mjs → local-CUOVOu5U.mjs} +6 -6
- package/dist/{local-BIE23GuI.mjs.map → local-CUOVOu5U.mjs.map} +1 -1
- package/dist/meta-manager-CJUiTgP2.mjs +98 -0
- package/dist/meta-manager-CJUiTgP2.mjs.map +1 -0
- package/dist/{model-manager-BhLsUgaB.mjs → model-manager-Rcj5Wzct.mjs} +138 -32
- package/dist/model-manager-Rcj5Wzct.mjs.map +1 -0
- package/dist/{ops-Dh2T3u53.mjs → ops-B422KP_S.mjs} +26 -23
- package/dist/ops-B422KP_S.mjs.map +1 -0
- package/dist/{paths-CYl5vCBD.mjs → paths-CmVw5Cw2.mjs} +15 -4
- package/dist/{paths-CYl5vCBD.mjs.map → paths-CmVw5Cw2.mjs.map} +1 -1
- package/dist/providers/local/index.mjs +8 -8
- package/dist/server/http/index.mjs +12 -12
- package/dist/{server-BkDuFvVt.mjs → server-DT8MN5d1.mjs} +163 -60
- package/dist/server-DT8MN5d1.mjs.map +1 -0
- package/dist/server.mjs +12 -12
- package/dist/{transaction-DhVoGigz.mjs → transaction-C1P-WnVo.mjs} +4 -4
- package/dist/{transaction-DhVoGigz.mjs.map → transaction-C1P-WnVo.mjs.map} +1 -1
- package/dist/{validator-8vaPxSba.mjs → validator-DLmW9MVF.mjs} +83 -76
- package/dist/validator-DLmW9MVF.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/apply-manager-UXUTYFZn.mjs.map +0 -1
- package/dist/content-manager-DcmNwvmq.d.mts.map +0 -1
- package/dist/doctor-BwSJW1sS.mjs.map +0 -1
- package/dist/meta-manager-C900rEyG.mjs +0 -53
- package/dist/meta-manager-C900rEyG.mjs.map +0 -1
- package/dist/model-manager-BhLsUgaB.mjs.map +0 -1
- package/dist/ops-Dh2T3u53.mjs.map +0 -1
- package/dist/server-BkDuFvVt.mjs.map +0 -1
- package/dist/validator-8vaPxSba.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ops-B422KP_S.mjs","names":["canonicalStringify","serializeMarkdownFrontmatter","canonicalStringify","canonicalStringify"],"sources":["../src/core/ops/apply-to-worktree.ts","../src/core/ops/content-delete.ts","../src/core/media/media-rewrite.ts","../src/core/ops/content-save.ts","../src/core/ops/model-delete.ts","../src/core/ops/model-save.ts"],"sourcesContent":["import { mkdir, rm, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport type { FileChange } from '../contracts/index.js'\n\n/**\n * Phase 2 shim — applies a `FileChange[]` to a local worktree directory.\n *\n * Phase 3 replaces this with `LocalProvider.applyPlan` (same logic, owned by\n * the provider) while a `GitHubProvider.applyPlan` produces the equivalent\n * commit through the Git Data API. Until then, tool handlers call this shim\n * inside the existing `createTransaction` flow so Phase 2 gets plan/apply\n * semantics without rewriting the transaction machinery.\n *\n * - `content: string` → `writeFile` (creates parent dirs).\n * - `content: null` → `rm`; missing files are tolerated.\n * - Changes are applied in their given order; callers should sort for\n * determinism.\n */\nexport async function applyChangesToWorktree(worktree: string, changes: readonly FileChange[]): Promise<void> {\n for (const change of changes) {\n const absolute = join(worktree, change.path)\n if (change.content === null) {\n await rm(absolute, { force: true }).catch(() => { /* tolerate missing */ })\n continue\n }\n await mkdir(dirname(absolute), { recursive: true })\n await writeFile(absolute, change.content, 'utf-8')\n }\n}\n","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 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 { FieldDef, FieldType } from '@contentrain/types'\n\n/**\n * Media path → delivery URL rewriting for MCP content writes.\n *\n * Ported from Studio's `server/utils/media-rewrite.ts`, with one deliberate\n * difference: Studio derives the delivery base from a `projectId` plus its\n * runtime config, whereas MCP receives the fully-qualified per-project base\n * directly via `RepoProvider.mediaBaseUrl`\n * (`{siteUrl}/api/cdn/v1/{projectId}` — the project segment is already\n * included). So here `toDeliveryUrl` is a single `{base}/{path}` join.\n *\n * Why this exists: Studio stores uploaded media in CDN/R2 and references it\n * from content by a relative storage path (`media/...`). That path renders\n * nowhere on its own — it must become an absolute, public delivery URL before\n * it reaches a browser. When an external agent writes through MCP Cloud, the\n * write bypasses Studio's content-engine and goes straight through\n * `planContentSave`, so this is where the normalization has to happen: the\n * git-committed value is then already a ready-to-use URL for any consumer\n * (`@contentrain/query`, raw markdown, a plain landing page) with no SDK.\n *\n * Only image/video/file fields (resolved via the model schema, including those\n * nested in object/array fields) and `media/...` src/href targets in\n * markdown/HTML are rewritten. External URLs (`http(s)://`, `//`, `data:`) and\n * already-absolute delivery URLs pass through untouched, so every entry point\n * is idempotent and safe to call repeatedly. In local mode (no base) media\n * stays a relative path — the OSS file model.\n */\n\nconst MEDIA_FIELD_TYPES = new Set<FieldType>(['image', 'video', 'file'])\n\n/**\n * Whether a stored field value is a relative media-storage path (`media/...`)\n * rather than an already-absolute URL or external link. Only these are\n * rewritten to delivery URLs — `http(s)://`, `//`, and `data:` are left as-is.\n */\nexport function isStoredMediaPath(value: unknown): value is string {\n return typeof value === 'string' && value.startsWith('media/')\n}\n\n/** Join a relative media path onto the per-project delivery base. */\nexport function toDeliveryUrl(base: string, path: string): string {\n return `${base.replace(/\\/+$/, '')}/${path}`\n}\n\n/**\n * Rewrite a stored media path to its absolute delivery URL. Non-media values\n * (external URLs, empty, non-strings) pass through untouched, so this is safe\n * to call on any field value.\n */\nexport function rewriteMediaUrl(base: string, value: unknown): unknown {\n return isStoredMediaPath(value) ? toDeliveryUrl(base, value) : value\n}\n\n/**\n * Rewrite media paths within a single field value, guided by its FieldDef so\n * only media fields — including those nested inside object/array fields — are\n * touched. Non-media values pass through.\n */\nexport function rewriteFieldMedia(value: unknown, field: FieldDef, base: string): unknown {\n if (value == null) return value\n\n if (MEDIA_FIELD_TYPES.has(field.type))\n return rewriteMediaUrl(base, value)\n\n if (field.type === 'object' && field.fields && typeof value === 'object' && !Array.isArray(value))\n return rewriteEntryMedia(value as Record<string, unknown>, field.fields, base)\n\n if (field.type === 'array' && Array.isArray(value)) {\n const itemDef: FieldDef | null = typeof field.items === 'string'\n ? { type: field.items as FieldType }\n : field.items ?? null\n if (!itemDef) return value\n return value.map(v => rewriteFieldMedia(v, itemDef, base))\n }\n\n return value\n}\n\n/**\n * Rewrite media paths across one entry/frontmatter object using a field map.\n * Returns a shallow copy (the input is never mutated); a no-op when the object\n * has no media fields.\n */\nexport function rewriteEntryMedia(\n entry: Record<string, unknown>,\n fields: Record<string, FieldDef>,\n base: string,\n): Record<string, unknown> {\n const out = { ...entry }\n for (const [fieldId, field] of Object.entries(fields)) {\n if (fieldId in out)\n out[fieldId] = rewriteFieldMedia(out[fieldId], field, base)\n }\n return out\n}\n\n/** Rewrite `media/...` markdown image/link targets and inline src/href attrs. */\nexport function rewriteMarkdownMedia(body: string, base: string): string {\n return body\n // markdown image/link target: ](media/...) — stops at whitespace, ) or \"\n .replace(/(\\]\\()(media\\/[^)\\s\"]+)/g, (_m, open, p) => `${open}${toDeliveryUrl(base, p)}`)\n // inline HTML src=/href=\"media/...\"\n .replace(/(\\s(?:src|href)=)([\"'])(media\\/[^\"']+)\\2/gi,\n (_m, attr, quote, p) => `${attr}${quote}${toDeliveryUrl(base, p)}${quote}`)\n}\n","import type { ContentrainConfig, EntryMeta, ModelDefinition, Vocabulary } from '@contentrain/types'\nimport { generateEntryId, validateEntryId, validateLocale, validateSlug } from '@contentrain/types'\nimport type { FileChange, RepoReader } from '../contracts/index.js'\nimport type { ContentEntry } from '../content-manager.js'\nimport { canonicalStringify, serializeMarkdownFrontmatter } from '../serialization/index.js'\nimport { rewriteEntryMedia, rewriteMarkdownMedia } from '../media/media-rewrite.js'\nimport type { ContentSaveEntryResult, ContentSavePlan } from './types.js'\nimport { contentFilePath, documentFilePath, metaFilePath } from './paths.js'\nimport { mergeEntryMeta } from '../meta-manager.js'\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.data))\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.data)\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 collisions = Object.keys(newData).filter(\n k => k in existing && existing[k] !== newData[k],\n )\n if (collisions.length > 0) {\n throw new Error(\n `Dictionary \"${model.id}\" (${locale}): ${collisions.length} key collision(s) — [${collisions.join(', ')}] already exist with different values. `\n + 'Read existing keys with contentrain_content_list first, or include all keys in a single save call.',\n )\n }\n\n const entryAdvisories: string[] = []\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.data))\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 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 fmData = { ...entry.data }\n const bodyContent = (fmData['body'] as string | undefined) ?? ''\n delete fmData['body']\n if (!fmData['slug']) fmData['slug'] = slug\n\n const dPath = documentFilePath(model, locale, slug)\n const mPath = metaFilePath(model, locale, defaultLocale, slug)\n\n let existingRaw: string | null = null\n try { existingRaw = await reader.readFile(dPath) }\n catch { /* not yet */ }\n const action: 'created' | 'updated' = existingRaw ? 'updated' : 'created'\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.data))\n\n result.push({ action, slug, locale })\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 type { FileChange, RepoReader } from '../contracts/index.js'\nimport { canonicalStringify } from '../serialization/index.js'\nimport type { OpPlan } from './types.js'\n\n/** Field order for canonical model JSON — preserves reader ergonomics. */\nconst MODEL_FIELD_ORDER = [\n 'id',\n 'name',\n 'kind',\n 'domain',\n 'i18n',\n 'description',\n 'content_path',\n 'locale_strategy',\n 'fields',\n]\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"],"mappings":"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"}
|
|
@@ -43,12 +43,23 @@ function documentFilePath(model, locale, slug) {
|
|
|
43
43
|
default: return `${dir}/${slug}/${locale}.md`;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Content-root-relative meta path.
|
|
48
|
+
*
|
|
49
|
+
* `i18n` is in the Pick and `defaultLocale` is required on purpose. A non-i18n
|
|
50
|
+
* model keeps all its content in one `data.json`, so it has exactly one meta
|
|
51
|
+
* record — pinned to the default locale. Passing the caller's locale straight
|
|
52
|
+
* through used to fan meta out across locales for that single content file
|
|
53
|
+
* (`meta/{id}/tr.json` *and* `meta/{id}/en.json` for one `data.json`), leaving
|
|
54
|
+
* every reader to guess which file was authoritative.
|
|
55
|
+
*/
|
|
56
|
+
function metaFilePath(model, locale, defaultLocale, slug) {
|
|
47
57
|
const base = `.contentrain/meta/${model.id}`;
|
|
48
|
-
|
|
49
|
-
return `${base}/${
|
|
58
|
+
const effective = model.i18n ? locale : defaultLocale;
|
|
59
|
+
if (model.kind === "document" && slug) return `${base}/${slug}/${effective}.json`;
|
|
60
|
+
return `${base}/${effective}.json`;
|
|
50
61
|
}
|
|
51
62
|
//#endregion
|
|
52
63
|
export { metaFilePath as i, contentFilePath as n, documentFilePath as r, contentDirPath as t };
|
|
53
64
|
|
|
54
|
-
//# sourceMappingURL=paths-
|
|
65
|
+
//# sourceMappingURL=paths-CmVw5Cw2.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths-
|
|
1
|
+
{"version":3,"file":"paths-CmVw5Cw2.mjs","names":[],"sources":["../src/core/ops/paths.ts"],"sourcesContent":["import type { ModelDefinition } from '@contentrain/types'\n\n/**\n * Content-root-relative path helpers for plan-API consumers.\n *\n * These mirror the logic inside `core/content-manager.ts:resolveJsonFilePath`\n * but return forward-slash relative strings suitable for `FileChange.path`.\n * Providers are responsible for resolving these against their backing store\n * (worktree on disk for LocalProvider, git tree path for GitHubProvider, ...).\n *\n * Kept inline to `core/ops/` for now; when Faz 2 finishes migrating every op\n * the legacy absolute-path helpers in `core/content-manager.ts` and\n * `core/meta-manager.ts` will call into these (or be replaced entirely).\n */\n\n/**\n * Content-root-relative content directory for a model (forward-slash paths).\n * Mirrors `resolveContentDir` with an empty `projectRoot`, so reader-backed\n * callers and plan-API consumers share the same path resolution.\n */\nexport function contentDirPath(\n model: Pick<ModelDefinition, 'domain' | 'id' | 'content_path'>,\n): string {\n if (model.content_path) return model.content_path\n return `.contentrain/content/${model.domain}/${model.id}`\n}\n\nfunction contentDir(model: Pick<ModelDefinition, 'domain' | 'id' | 'content_path'>): string {\n return contentDirPath(model)\n}\n\nexport function contentFilePath(\n model: Pick<ModelDefinition, 'id' | 'kind' | 'domain' | 'i18n' | 'content_path' | 'locale_strategy'>,\n locale: string,\n): string {\n const dir = contentDir(model)\n if (!model.i18n) return `${dir}/data.json`\n const strategy = model.locale_strategy ?? 'file'\n switch (strategy) {\n case 'suffix': return `${dir}/${model.id}.${locale}.json`\n case 'directory': return `${dir}/${locale}/${model.id}.json`\n case 'none': return `${dir}/${model.id}.json`\n default: return `${dir}/${locale}.json`\n }\n}\n\nexport function documentFilePath(\n model: Pick<ModelDefinition, 'id' | 'domain' | 'i18n' | 'content_path' | 'locale_strategy'>,\n locale: string,\n slug: string,\n): string {\n const dir = contentDir(model)\n if (!model.i18n) return `${dir}/${slug}.md`\n const strategy = model.locale_strategy ?? 'file'\n switch (strategy) {\n case 'suffix': return `${dir}/${slug}.${locale}.md`\n case 'directory': return `${dir}/${locale}/${slug}.md`\n case 'none': return `${dir}/${slug}.md`\n default: return `${dir}/${slug}/${locale}.md`\n }\n}\n\n/**\n * Content-root-relative meta path.\n *\n * `i18n` is in the Pick and `defaultLocale` is required on purpose. A non-i18n\n * model keeps all its content in one `data.json`, so it has exactly one meta\n * record — pinned to the default locale. Passing the caller's locale straight\n * through used to fan meta out across locales for that single content file\n * (`meta/{id}/tr.json` *and* `meta/{id}/en.json` for one `data.json`), leaving\n * every reader to guess which file was authoritative.\n */\nexport function metaFilePath(\n model: Pick<ModelDefinition, 'id' | 'kind' | 'i18n'>,\n locale: string,\n defaultLocale: string,\n slug?: string,\n): string {\n const base = `.contentrain/meta/${model.id}`\n const effective = model.i18n ? locale : defaultLocale\n if (model.kind === 'document' && slug) {\n return `${base}/${slug}/${effective}.json`\n }\n return `${base}/${effective}.json`\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoBA,SAAgB,eACd,OACQ;AACR,KAAI,MAAM,aAAc,QAAO,MAAM;AACrC,QAAO,wBAAwB,MAAM,OAAO,GAAG,MAAM;;AAGvD,SAAS,WAAW,OAAwE;AAC1F,QAAO,eAAe,MAAM;;AAG9B,SAAgB,gBACd,OACA,QACQ;CACR,MAAM,MAAM,WAAW,MAAM;AAC7B,KAAI,CAAC,MAAM,KAAM,QAAO,GAAG,IAAI;AAE/B,SADiB,MAAM,mBAAmB,QAC1C;EACE,KAAK,SAAU,QAAO,GAAG,IAAI,GAAG,MAAM,GAAG,GAAG,OAAO;EACnD,KAAK,YAAa,QAAO,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG;EACtD,KAAK,OAAQ,QAAO,GAAG,IAAI,GAAG,MAAM,GAAG;EACvC,QAAS,QAAO,GAAG,IAAI,GAAG,OAAO;;;AAIrC,SAAgB,iBACd,OACA,QACA,MACQ;CACR,MAAM,MAAM,WAAW,MAAM;AAC7B,KAAI,CAAC,MAAM,KAAM,QAAO,GAAG,IAAI,GAAG,KAAK;AAEvC,SADiB,MAAM,mBAAmB,QAC1C;EACE,KAAK,SAAU,QAAO,GAAG,IAAI,GAAG,KAAK,GAAG,OAAO;EAC/C,KAAK,YAAa,QAAO,GAAG,IAAI,GAAG,OAAO,GAAG,KAAK;EAClD,KAAK,OAAQ,QAAO,GAAG,IAAI,GAAG,KAAK;EACnC,QAAS,QAAO,GAAG,IAAI,GAAG,KAAK,GAAG,OAAO;;;;;;;;;;;;;AAc7C,SAAgB,aACd,OACA,QACA,eACA,MACQ;CACR,MAAM,OAAO,qBAAqB,MAAM;CACxC,MAAM,YAAY,MAAM,OAAO,SAAS;AACxC,KAAI,MAAM,SAAS,cAAc,KAC/B,QAAO,GAAG,KAAK,GAAG,KAAK,GAAG,UAAU;AAEtC,QAAO,GAAG,KAAK,GAAG,UAAU"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import "../../contracts-DfL0BfrD.mjs";
|
|
2
|
-
import "../../ops-
|
|
2
|
+
import "../../ops-B422KP_S.mjs";
|
|
3
3
|
import "../../serialization-B1CEzR4H.mjs";
|
|
4
4
|
import "../../fs-DLbVB-Ek.mjs";
|
|
5
|
-
import "../../
|
|
6
|
-
import
|
|
7
|
-
import "../../
|
|
8
|
-
import "../../context-
|
|
9
|
-
import "../../branch-lifecycle-
|
|
5
|
+
import "../../meta-manager-CJUiTgP2.mjs";
|
|
6
|
+
import "../../config-oxxgznz7.mjs";
|
|
7
|
+
import { C as LocalReader } from "../../model-manager-Rcj5Wzct.mjs";
|
|
8
|
+
import "../../context-Cppz4R65.mjs";
|
|
9
|
+
import "../../branch-lifecycle-BAfgSQBv.mjs";
|
|
10
10
|
import "../../id-DV_T9Ic8.mjs";
|
|
11
|
-
import "../../transaction-
|
|
12
|
-
import { t as LocalProvider } from "../../local-
|
|
11
|
+
import "../../transaction-C1P-WnVo.mjs";
|
|
12
|
+
import { t as LocalProvider } from "../../local-CUOVOu5U.mjs";
|
|
13
13
|
export { LocalProvider, LocalReader };
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import "../../contracts-DfL0BfrD.mjs";
|
|
2
|
-
import "../../ops-
|
|
2
|
+
import "../../ops-B422KP_S.mjs";
|
|
3
3
|
import "../../serialization-B1CEzR4H.mjs";
|
|
4
4
|
import "../../fs-DLbVB-Ek.mjs";
|
|
5
|
-
import "../../
|
|
6
|
-
import "../../
|
|
7
|
-
import "../../
|
|
8
|
-
import "../../context-
|
|
9
|
-
import "../../branch-lifecycle-
|
|
5
|
+
import "../../meta-manager-CJUiTgP2.mjs";
|
|
6
|
+
import "../../config-oxxgznz7.mjs";
|
|
7
|
+
import "../../model-manager-Rcj5Wzct.mjs";
|
|
8
|
+
import "../../context-Cppz4R65.mjs";
|
|
9
|
+
import "../../branch-lifecycle-BAfgSQBv.mjs";
|
|
10
10
|
import "../../id-DV_T9Ic8.mjs";
|
|
11
|
-
import "../../transaction-
|
|
12
|
-
import "../../local-
|
|
11
|
+
import "../../transaction-C1P-WnVo.mjs";
|
|
12
|
+
import "../../local-CUOVOu5U.mjs";
|
|
13
13
|
import "../../detect-wVSI9VuY.mjs";
|
|
14
14
|
import "../../annotations-D3tlsF38.mjs";
|
|
15
|
-
import { n as createServer } from "../../server-
|
|
16
|
-
import "../../validator-
|
|
15
|
+
import { n as createServer } from "../../server-DT8MN5d1.mjs";
|
|
16
|
+
import "../../validator-DLmW9MVF.mjs";
|
|
17
17
|
import "../../scan-config-BGUflS8t.mjs";
|
|
18
18
|
import "../../graph-builder-CRUX_8mA.mjs";
|
|
19
19
|
import "../../scanner-VOwrKLGC.mjs";
|
|
20
20
|
import "../../tsx-parser-B_aI_C2r.mjs";
|
|
21
|
-
import "../../apply-manager-
|
|
22
|
-
import "../../doctor-
|
|
21
|
+
import "../../apply-manager-BpKGvXmn.mjs";
|
|
22
|
+
import "../../doctor-D3usxlom.mjs";
|
|
23
23
|
import { randomUUID } from "node:crypto";
|
|
24
24
|
import http from "node:http";
|
|
25
25
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import { i as planContentDelete, n as planModelDelete, r as planContentSave, t as planModelSave } from "./ops-
|
|
2
|
-
import { n as contentFilePath, t as contentDirPath } from "./paths-
|
|
1
|
+
import { i as planContentDelete, n as planModelDelete, r as planContentSave, t as planModelSave } from "./ops-B422KP_S.mjs";
|
|
2
|
+
import { n as contentFilePath, t as contentDirPath } from "./paths-CmVw5Cw2.mjs";
|
|
3
3
|
import { a as readJson, n as ensureDir, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-DLbVB-Ek.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { n as
|
|
7
|
-
import { n as readContext } from "./context-
|
|
8
|
-
import { a as deleteRemoteBranch, c as pruneMergedRemoteBranches, i as cleanupMergedBranches, n as checkBranchHealth, s as listRemoteCrBranches } from "./branch-lifecycle-
|
|
9
|
-
import { i as mergeBranch, n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "./transaction-
|
|
10
|
-
import { n as isMerged, t as LocalProvider } from "./local-
|
|
4
|
+
import { a as writeMetaEntries, i as writeMeta, r as readMeta } from "./meta-manager-CJUiTgP2.mjs";
|
|
5
|
+
import { n as readVocabulary, t as readConfig } from "./config-oxxgznz7.mjs";
|
|
6
|
+
import { S as writeContent, _ as resolveMdFilePath, a as fieldDefZodSchema, c as validateModelDefinition, d as listContent, g as resolveLocaleStrategy, h as resolveJsonFilePath, l as writeModel, m as resolveContentDir, n as checkReferences, o as listModels, r as countEntries, s as readModel, u as deleteContent } from "./model-manager-Rcj5Wzct.mjs";
|
|
7
|
+
import { n as readContext } from "./context-Cppz4R65.mjs";
|
|
8
|
+
import { a as deleteRemoteBranch, c as pruneMergedRemoteBranches, i as cleanupMergedBranches, n as checkBranchHealth, s as listRemoteCrBranches } from "./branch-lifecycle-BAfgSQBv.mjs";
|
|
9
|
+
import { i as mergeBranch, n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "./transaction-C1P-WnVo.mjs";
|
|
10
|
+
import { n as isMerged, t as LocalProvider } from "./local-CUOVOu5U.mjs";
|
|
11
11
|
import { n as isToolAvailable, t as TOOL_REQUIREMENTS } from "./availability-sWWRz2W0.mjs";
|
|
12
12
|
import { t as detectStack } from "./detect-wVSI9VuY.mjs";
|
|
13
13
|
import { t as TOOL_ANNOTATIONS } from "./annotations-D3tlsF38.mjs";
|
|
14
14
|
import { n as listTemplates, t as getTemplate } from "./templates-D6EllixQ.mjs";
|
|
15
|
-
import { t as validateProject } from "./validator-
|
|
15
|
+
import { t as validateProject } from "./validator-DLmW9MVF.mjs";
|
|
16
16
|
import { t as OverlayReader } from "./overlay-reader-BOb105gS.mjs";
|
|
17
17
|
import { t as buildGraph } from "./graph-builder-CRUX_8mA.mjs";
|
|
18
18
|
import { n as scanSummary, t as scanCandidates } from "./scanner-VOwrKLGC.mjs";
|
|
19
|
-
import { n as applyExtract, r as applyReuse } from "./apply-manager-
|
|
20
|
-
import { t as runDoctor } from "./doctor-
|
|
19
|
+
import { n as applyExtract, r as applyReuse } from "./apply-manager-BpKGvXmn.mjs";
|
|
20
|
+
import { t as runDoctor } from "./doctor-D3usxlom.mjs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
23
23
|
import { CONTENTRAIN_BRANCH } from "@contentrain/types";
|
|
@@ -235,9 +235,10 @@ function registerContextTools(server, provider, projectRoot) {
|
|
|
235
235
|
}
|
|
236
236
|
},
|
|
237
237
|
i18n_disabled: {
|
|
238
|
-
description: "When a model has i18n:false, locale is ignored in
|
|
238
|
+
description: "When a model has i18n:false, locale is ignored in CONTENT paths — but not in meta paths. The model holds one content file, so it has exactly one meta record, and that record lives at the default locale. Saving under a non-default locale does not move it.",
|
|
239
239
|
json_pattern: "{content-dir}/data.json (always \"data.json\", no locale suffix)",
|
|
240
|
-
md_pattern: "{content-dir}/{slug}.md (no locale in path)"
|
|
240
|
+
md_pattern: "{content-dir}/{slug}.md (no locale in path)",
|
|
241
|
+
meta_pattern: ".contentrain/meta/{model-id}/{default-locale}.json (always the default locale, never \"data.json\")"
|
|
241
242
|
},
|
|
242
243
|
canonical_serialization: {
|
|
243
244
|
description: "All JSON files use deterministic serialization for clean git diffs.",
|
|
@@ -829,7 +830,7 @@ function registerModelTools(server, provider, projectRoot) {
|
|
|
829
830
|
}],
|
|
830
831
|
isError: true
|
|
831
832
|
};
|
|
832
|
-
const errors = validateModel(input);
|
|
833
|
+
const { errors, warnings: schemaWarnings } = validateModel(input);
|
|
833
834
|
if (errors.length > 0) return {
|
|
834
835
|
content: [{
|
|
835
836
|
type: "text",
|
|
@@ -923,6 +924,7 @@ function registerModelTools(server, provider, projectRoot) {
|
|
|
923
924
|
valid: true,
|
|
924
925
|
errors: []
|
|
925
926
|
},
|
|
927
|
+
...schemaWarnings.length > 0 ? { schema_warnings: schemaWarnings } : {},
|
|
926
928
|
git: {
|
|
927
929
|
branch,
|
|
928
930
|
action: workflowAction,
|
|
@@ -933,7 +935,7 @@ function registerModelTools(server, provider, projectRoot) {
|
|
|
933
935
|
content_path: contentPath + "/",
|
|
934
936
|
...displayPath ? { example_file: displayPath } : {},
|
|
935
937
|
...importSnippet ? { import_snippet: importSnippet } : {},
|
|
936
|
-
next_steps: ["Add content with contentrain_content_save"]
|
|
938
|
+
next_steps: [...schemaWarnings.length > 0 ? ["REVIEW: see schema_warnings — some declared constraints are not enforced by MCP"] : [], "Add content with contentrain_content_save"]
|
|
937
939
|
}, null, 2)
|
|
938
940
|
}] };
|
|
939
941
|
});
|
|
@@ -1132,6 +1134,19 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1132
1134
|
};
|
|
1133
1135
|
}
|
|
1134
1136
|
const entryIds = plan.result.map((r) => r.id ?? r.slug ?? r.locale).filter((v) => Boolean(v));
|
|
1137
|
+
const validationResult = await validateProject(new OverlayReader(provider, plan.changes), { model: input.model });
|
|
1138
|
+
const blocking = validationResult.issues.filter((i) => i.severity === "error" && touchesSavedEntries(i, plan.result));
|
|
1139
|
+
if (blocking.length > 0) return {
|
|
1140
|
+
content: [{
|
|
1141
|
+
type: "text",
|
|
1142
|
+
text: JSON.stringify({
|
|
1143
|
+
error: "Validation failed — nothing was written.",
|
|
1144
|
+
issues: blocking.map(describeIssue),
|
|
1145
|
+
hint: "Fix the values and call contentrain_content_save again. Nothing was committed, so there is no branch to clean up."
|
|
1146
|
+
}, null, 2)
|
|
1147
|
+
}],
|
|
1148
|
+
isError: true
|
|
1149
|
+
};
|
|
1135
1150
|
const branch = buildBranchName("content", input.model);
|
|
1136
1151
|
const message = `[contentrain] content: ${input.model}`;
|
|
1137
1152
|
const contextPayload = {
|
|
@@ -1162,8 +1177,8 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1162
1177
|
isError: true
|
|
1163
1178
|
};
|
|
1164
1179
|
}
|
|
1165
|
-
const validationResult = await validateProject(new OverlayReader(provider, plan.changes), { model: input.model });
|
|
1166
1180
|
const allAdvisories = plan.result.flatMap((r) => r.advisories ?? []);
|
|
1181
|
+
const warnings = validationResult.issues.filter((i) => i.severity === "warning" && touchesSavedEntries(i, plan.result)).map((i) => `${i.field ?? i.locale}: ${i.message}`);
|
|
1167
1182
|
return { content: [{
|
|
1168
1183
|
type: "text",
|
|
1169
1184
|
text: JSON.stringify({
|
|
@@ -1180,14 +1195,15 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1180
1195
|
advisories: allAdvisories,
|
|
1181
1196
|
advisory_note: "Save succeeded. Review these warnings and consider consolidating duplicate values."
|
|
1182
1197
|
} : {},
|
|
1198
|
+
...warnings.length > 0 ? { warnings } : {},
|
|
1183
1199
|
validation: {
|
|
1184
|
-
valid:
|
|
1185
|
-
errors:
|
|
1200
|
+
valid: true,
|
|
1201
|
+
errors: []
|
|
1186
1202
|
},
|
|
1187
1203
|
context_updated: true,
|
|
1188
1204
|
next_steps: [
|
|
1189
1205
|
...allAdvisories.length > 0 ? ["ADVISORY: Duplicate values detected — review advisories above"] : [],
|
|
1190
|
-
|
|
1206
|
+
...warnings.length > 0 ? ["REVIEW: see warnings above — the save was not blocked by them"] : [],
|
|
1191
1207
|
...model.kind === "collection" ? ["Use contentrain_content_list to verify", "Add more entries or publish"] : ["Use contentrain_content_list to verify"]
|
|
1192
1208
|
]
|
|
1193
1209
|
}, null, 2)
|
|
@@ -1201,7 +1217,8 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1201
1217
|
keys: z.array(z.string()).optional().describe("Dictionary only: specific keys to remove. Omit to delete entire locale file."),
|
|
1202
1218
|
confirm: z.literal(true).describe("Must be true to confirm deletion")
|
|
1203
1219
|
}, TOOL_ANNOTATIONS["contentrain_content_delete"], async (input) => {
|
|
1204
|
-
|
|
1220
|
+
const config = await readConfig(provider);
|
|
1221
|
+
if (!config) return {
|
|
1205
1222
|
content: [{
|
|
1206
1223
|
type: "text",
|
|
1207
1224
|
text: JSON.stringify({ error: "Project not initialized." })
|
|
@@ -1237,7 +1254,8 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1237
1254
|
id: input.id,
|
|
1238
1255
|
slug: input.slug,
|
|
1239
1256
|
locale: input.locale,
|
|
1240
|
-
keys: input.keys
|
|
1257
|
+
keys: input.keys,
|
|
1258
|
+
defaultLocale: config.locales.default
|
|
1241
1259
|
});
|
|
1242
1260
|
} catch (error) {
|
|
1243
1261
|
return {
|
|
@@ -1342,6 +1360,35 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1342
1360
|
}
|
|
1343
1361
|
});
|
|
1344
1362
|
}
|
|
1363
|
+
/** Trim a validation issue to the fields that locate it, dropping empty ones. */
|
|
1364
|
+
function describeIssue(issue) {
|
|
1365
|
+
const out = {};
|
|
1366
|
+
if (issue.entry) out["entry"] = issue.entry;
|
|
1367
|
+
if (issue.slug) out["slug"] = issue.slug;
|
|
1368
|
+
if (issue.locale) out["locale"] = issue.locale;
|
|
1369
|
+
if (issue.field) out["field"] = issue.field;
|
|
1370
|
+
out["message"] = issue.message;
|
|
1371
|
+
return out;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Does a validation issue belong to one of the entries this save is writing?
|
|
1375
|
+
*
|
|
1376
|
+
* The write gate blocks on errors, but `validateProject` inspects the whole model.
|
|
1377
|
+
* Without this filter, a pre-existing bad entry somewhere else in the model would
|
|
1378
|
+
* block an unrelated — and perfectly valid — save, leaving the caller stuck behind
|
|
1379
|
+
* a problem they did not create and may not be able to fix.
|
|
1380
|
+
*
|
|
1381
|
+
* Matching is by identity: entry ID for collections, slug for documents, and
|
|
1382
|
+
* locale alone for singletons and dictionaries, which have one record per locale.
|
|
1383
|
+
*/
|
|
1384
|
+
function touchesSavedEntries(issue, results) {
|
|
1385
|
+
return results.some((r) => {
|
|
1386
|
+
if (issue.locale && r.locale && issue.locale !== r.locale) return false;
|
|
1387
|
+
if (r.id) return issue.entry === r.id;
|
|
1388
|
+
if (r.slug) return issue.slug === r.slug;
|
|
1389
|
+
return !issue.entry && !issue.slug;
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1345
1392
|
//#endregion
|
|
1346
1393
|
//#region src/tools/workflow.ts
|
|
1347
1394
|
/**
|
|
@@ -2049,7 +2096,8 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2049
2096
|
model: z.string().describe("Model ID"),
|
|
2050
2097
|
source_locale: z.string().optional().describe("Source locale for copy_locale operation"),
|
|
2051
2098
|
target_locale: z.string().optional().describe("Target locale for copy_locale operation"),
|
|
2052
|
-
entry_ids: z.array(z.string()).optional().describe("Entry IDs for update_status or delete_entries"),
|
|
2099
|
+
entry_ids: z.array(z.string()).optional().describe("Entry IDs for update_status (collection models) or delete_entries"),
|
|
2100
|
+
locale: z.string().optional().describe("Scope update_status to a single locale (i18n models only; defaults to every supported locale)"),
|
|
2053
2101
|
status: z.enum([
|
|
2054
2102
|
"draft",
|
|
2055
2103
|
"in_review",
|
|
@@ -2138,19 +2186,21 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2138
2186
|
await writeJson(targetFile, sourceData);
|
|
2139
2187
|
if (model.kind === "collection") {
|
|
2140
2188
|
const entries = sourceData;
|
|
2141
|
-
const
|
|
2142
|
-
const
|
|
2143
|
-
locale: input.target_locale,
|
|
2144
|
-
entryId
|
|
2145
|
-
}, {
|
|
2189
|
+
const updates = {};
|
|
2190
|
+
for (const entryId of Object.keys(entries)) updates[entryId] = {
|
|
2146
2191
|
status: "draft",
|
|
2147
2192
|
source: "agent",
|
|
2148
2193
|
updated_by: "contentrain-mcp"
|
|
2149
|
-
}
|
|
2150
|
-
await
|
|
2151
|
-
|
|
2194
|
+
};
|
|
2195
|
+
copiedCount = (await writeMetaEntries(wt, model, {
|
|
2196
|
+
locale: input.target_locale,
|
|
2197
|
+
defaultLocale: config.locales.default
|
|
2198
|
+
}, updates)).length;
|
|
2152
2199
|
} else {
|
|
2153
|
-
await writeMeta(wt, model, {
|
|
2200
|
+
await writeMeta(wt, model, {
|
|
2201
|
+
locale: input.target_locale,
|
|
2202
|
+
defaultLocale: config.locales.default
|
|
2203
|
+
}, {
|
|
2154
2204
|
status: "draft",
|
|
2155
2205
|
source: "agent",
|
|
2156
2206
|
updated_by: "contentrain-mcp"
|
|
@@ -2194,57 +2244,106 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2194
2244
|
}
|
|
2195
2245
|
}
|
|
2196
2246
|
case "update_status": {
|
|
2197
|
-
if (!input.
|
|
2247
|
+
if (!input.status) return {
|
|
2198
2248
|
content: [{
|
|
2199
2249
|
type: "text",
|
|
2200
|
-
text: JSON.stringify({ error: "update_status requires
|
|
2250
|
+
text: JSON.stringify({ error: "update_status requires status" })
|
|
2201
2251
|
}],
|
|
2202
2252
|
isError: true
|
|
2203
2253
|
};
|
|
2204
|
-
if (
|
|
2254
|
+
if (model.kind === "document") return {
|
|
2205
2255
|
content: [{
|
|
2206
2256
|
type: "text",
|
|
2207
|
-
text: JSON.stringify({ error: "update_status
|
|
2257
|
+
text: JSON.stringify({ error: `Model "${input.model}" is a document model. update_status does not support documents yet — their meta is keyed by slug, not entry ID.` })
|
|
2208
2258
|
}],
|
|
2209
2259
|
isError: true
|
|
2210
2260
|
};
|
|
2211
|
-
|
|
2261
|
+
const keyedByEntry = model.kind === "collection";
|
|
2262
|
+
if (keyedByEntry && (!input.entry_ids || input.entry_ids.length === 0)) return {
|
|
2263
|
+
content: [{
|
|
2264
|
+
type: "text",
|
|
2265
|
+
text: JSON.stringify({ error: `update_status requires entry_ids for collection model "${input.model}".` })
|
|
2266
|
+
}],
|
|
2267
|
+
isError: true
|
|
2268
|
+
};
|
|
2269
|
+
if (!keyedByEntry && input.entry_ids && input.entry_ids.length > 0) return {
|
|
2270
|
+
content: [{
|
|
2271
|
+
type: "text",
|
|
2272
|
+
text: JSON.stringify({ error: `Model "${input.model}" is a ${model.kind} model — it has one meta record per locale, so entry_ids do not apply. Omit entry_ids.` })
|
|
2273
|
+
}],
|
|
2274
|
+
isError: true
|
|
2275
|
+
};
|
|
2276
|
+
if (input.locale && !config.locales.supported.includes(input.locale)) return {
|
|
2212
2277
|
content: [{
|
|
2213
2278
|
type: "text",
|
|
2214
|
-
text: JSON.stringify({ error: "
|
|
2279
|
+
text: JSON.stringify({ error: `Locale "${input.locale}" is not supported. Supported: [${config.locales.supported.join(", ")}]` })
|
|
2215
2280
|
}],
|
|
2216
2281
|
isError: true
|
|
2217
2282
|
};
|
|
2283
|
+
const targetLocales = model.i18n ? input.locale ? [input.locale] : config.locales.supported : [config.locales.default];
|
|
2218
2284
|
const branch = buildBranchName("bulk", input.model);
|
|
2219
2285
|
const tx = await createTransaction(projectRoot, branch);
|
|
2220
2286
|
try {
|
|
2221
|
-
|
|
2222
|
-
const
|
|
2287
|
+
const updatedPerLocale = {};
|
|
2288
|
+
const foundIds = /* @__PURE__ */ new Set();
|
|
2223
2289
|
await tx.write(async (wt) => {
|
|
2224
|
-
for (const locale of
|
|
2225
|
-
const metaData = await readMeta(wt, model, {
|
|
2226
|
-
|
|
2290
|
+
for (const locale of targetLocales) if (keyedByEntry) {
|
|
2291
|
+
const metaData = await readMeta(wt, model, {
|
|
2292
|
+
locale,
|
|
2293
|
+
defaultLocale: config.locales.default
|
|
2294
|
+
});
|
|
2295
|
+
const updates = {};
|
|
2227
2296
|
for (const entryId of input.entry_ids) {
|
|
2228
2297
|
const existing = metaData?.[entryId];
|
|
2229
|
-
if (existing)
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
updated_by: "contentrain-mcp"
|
|
2237
|
-
}));
|
|
2238
|
-
updatedCount++;
|
|
2239
|
-
} else if (locale === config.locales.default) notFound.push(entryId);
|
|
2298
|
+
if (!existing) continue;
|
|
2299
|
+
updates[entryId] = {
|
|
2300
|
+
...existing,
|
|
2301
|
+
status: input.status,
|
|
2302
|
+
updated_by: "contentrain-mcp"
|
|
2303
|
+
};
|
|
2304
|
+
foundIds.add(entryId);
|
|
2240
2305
|
}
|
|
2241
|
-
await
|
|
2306
|
+
const written = await writeMetaEntries(wt, model, {
|
|
2307
|
+
locale,
|
|
2308
|
+
defaultLocale: config.locales.default
|
|
2309
|
+
}, updates);
|
|
2310
|
+
if (written.length > 0) updatedPerLocale[locale] = written;
|
|
2311
|
+
} else {
|
|
2312
|
+
const existing = await readMeta(wt, model, {
|
|
2313
|
+
locale,
|
|
2314
|
+
defaultLocale: config.locales.default
|
|
2315
|
+
});
|
|
2316
|
+
if (!existing) continue;
|
|
2317
|
+
await writeMeta(wt, model, {
|
|
2318
|
+
locale,
|
|
2319
|
+
defaultLocale: config.locales.default
|
|
2320
|
+
}, {
|
|
2321
|
+
...existing,
|
|
2322
|
+
status: input.status,
|
|
2323
|
+
updated_by: "contentrain-mcp"
|
|
2324
|
+
});
|
|
2325
|
+
updatedPerLocale[locale] = [model.id];
|
|
2242
2326
|
}
|
|
2243
2327
|
});
|
|
2328
|
+
const updatedCount = Object.values(updatedPerLocale).reduce((n, ids) => n + ids.length, 0);
|
|
2329
|
+
const notFound = keyedByEntry ? input.entry_ids.filter((id) => !foundIds.has(id)) : [];
|
|
2330
|
+
if (updatedCount === 0) {
|
|
2331
|
+
await tx.cleanup();
|
|
2332
|
+
return {
|
|
2333
|
+
content: [{
|
|
2334
|
+
type: "text",
|
|
2335
|
+
text: JSON.stringify({
|
|
2336
|
+
error: keyedByEntry ? `No meta records found for the given entry_ids in model "${input.model}" across locales [${targetLocales.join(", ")}]. Nothing was changed.` : `No meta record found for model "${input.model}" across locales [${targetLocales.join(", ")}]. Nothing was changed.`,
|
|
2337
|
+
not_found: notFound.length > 0 ? notFound : void 0
|
|
2338
|
+
}, null, 2)
|
|
2339
|
+
}],
|
|
2340
|
+
isError: true
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2244
2343
|
await tx.commit(`[contentrain] bulk: update status → ${input.status} for ${input.model}`, {
|
|
2245
2344
|
tool: "contentrain_bulk",
|
|
2246
2345
|
model: input.model,
|
|
2247
|
-
entries: input.entry_ids
|
|
2346
|
+
...keyedByEntry ? { entries: input.entry_ids } : {}
|
|
2248
2347
|
});
|
|
2249
2348
|
const gitResult = await tx.complete();
|
|
2250
2349
|
return { content: [{
|
|
@@ -2252,8 +2351,9 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2252
2351
|
text: JSON.stringify({
|
|
2253
2352
|
status: "committed",
|
|
2254
2353
|
operation: "update_status",
|
|
2255
|
-
message: `Updated ${updatedCount} meta
|
|
2354
|
+
message: `Updated ${updatedCount} meta record(s) to status "${input.status}" across locales [${Object.keys(updatedPerLocale).join(", ")}].`,
|
|
2256
2355
|
updated: updatedCount,
|
|
2356
|
+
updated_by_locale: updatedPerLocale,
|
|
2257
2357
|
not_found: notFound.length > 0 ? notFound : void 0,
|
|
2258
2358
|
git: {
|
|
2259
2359
|
branch,
|
|
@@ -2305,7 +2405,10 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2305
2405
|
const allRemoved = [];
|
|
2306
2406
|
await tx.write(async (wt) => {
|
|
2307
2407
|
for (const entryId of input.entry_ids) {
|
|
2308
|
-
const removed = await deleteContent(wt, model, {
|
|
2408
|
+
const removed = await deleteContent(wt, model, {
|
|
2409
|
+
id: entryId,
|
|
2410
|
+
defaultLocale: config.locales.default
|
|
2411
|
+
});
|
|
2309
2412
|
allRemoved.push(...removed);
|
|
2310
2413
|
}
|
|
2311
2414
|
});
|
|
@@ -2498,7 +2601,7 @@ function registerMediaTools(server, provider, _projectRoot) {
|
|
|
2498
2601
|
}
|
|
2499
2602
|
//#endregion
|
|
2500
2603
|
//#region package.json
|
|
2501
|
-
var version = "
|
|
2604
|
+
var version = "2.0.0";
|
|
2502
2605
|
//#endregion
|
|
2503
2606
|
//#region src/server.ts
|
|
2504
2607
|
/**
|
|
@@ -2568,4 +2671,4 @@ function resolveServerContext(input) {
|
|
|
2568
2671
|
//#endregion
|
|
2569
2672
|
export { createServer as n, DEFAULT_INSTRUCTIONS as t };
|
|
2570
2673
|
|
|
2571
|
-
//# sourceMappingURL=server-
|
|
2674
|
+
//# sourceMappingURL=server-DT8MN5d1.mjs.map
|