@contentrain/mcp 3.1.7 → 3.1.8
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-DsNq7Zhm.mjs → apply-manager-eZ6zA99Y.mjs} +7 -9
- package/dist/apply-manager-eZ6zA99Y.mjs.map +1 -0
- package/dist/{content-manager-Bx94DXSF.d.mts → content-manager-9yUO24W2.d.mts} +9 -1
- package/dist/content-manager-9yUO24W2.d.mts.map +1 -0
- package/dist/{context-CxpTATh4.mjs → context-BHfO4WBv.mjs} +2 -2
- package/dist/{context-CxpTATh4.mjs.map → context-BHfO4WBv.mjs.map} +1 -1
- package/dist/core/apply-manager.mjs +5 -5
- package/dist/core/content-manager.d.mts +1 -1
- package/dist/core/content-manager.mjs +2 -2
- package/dist/core/context.mjs +3 -3
- package/dist/core/doctor.mjs +3 -3
- package/dist/core/meta-manager.d.mts +18 -2
- package/dist/core/meta-manager.d.mts.map +1 -1
- package/dist/core/meta-manager.mjs +1 -1
- package/dist/core/model-manager.d.mts +56 -5
- package/dist/core/model-manager.d.mts.map +1 -1
- package/dist/core/model-manager.mjs +3 -3
- package/dist/core/ops/index.d.mts +1 -1
- package/dist/core/ops/index.mjs +6 -6
- package/dist/core/validator/index.d.mts.map +1 -1
- package/dist/core/validator/index.mjs +3 -3
- package/dist/{doctor-B7G0oFxO.mjs → doctor-VnOzDTbk.mjs} +2 -2
- package/dist/{doctor-B7G0oFxO.mjs.map → doctor-VnOzDTbk.mjs.map} +1 -1
- package/dist/git/reconcile.mjs +7 -7
- package/dist/git/transaction.mjs +4 -4
- package/dist/index.mjs +12 -12
- package/dist/{local-bnn6yrG7.mjs → local-BVww7ArO.mjs} +4 -4
- package/dist/{local-bnn6yrG7.mjs.map → local-BVww7ArO.mjs.map} +1 -1
- package/dist/{meta-manager-g3shoZES.mjs → meta-manager-Cd6AfuGS.mjs} +12 -4
- package/dist/meta-manager-Cd6AfuGS.mjs.map +1 -0
- package/dist/{model-manager-DqwL8ptm.mjs → model-manager-CfwJWvz7.mjs} +121 -29
- package/dist/model-manager-CfwJWvz7.mjs.map +1 -0
- package/dist/{ops-BfrdZ6GK.mjs → ops-B2P11b8v.mjs} +8 -8
- package/dist/ops-B2P11b8v.mjs.map +1 -0
- package/dist/providers/local/index.mjs +8 -8
- package/dist/{reconcile-D5FTVW5z.mjs → reconcile-B8qhwneb.mjs} +4 -4
- package/dist/{reconcile-D5FTVW5z.mjs.map → reconcile-B8qhwneb.mjs.map} +1 -1
- package/dist/{reconcile-M7KBZtdn.mjs → reconcile-MiURoMUx.mjs} +3 -3
- package/dist/{reconcile-M7KBZtdn.mjs.map → reconcile-MiURoMUx.mjs.map} +1 -1
- package/dist/server/http/index.mjs +12 -12
- package/dist/{server-BRkKIO3r.mjs → server-CXfGWDd4.mjs} +70 -32
- package/dist/server-CXfGWDd4.mjs.map +1 -0
- package/dist/server.mjs +12 -12
- package/dist/{transaction-BxDiiakv.mjs → transaction-BR2jg4uj.mjs} +2 -2
- package/dist/{transaction-BxDiiakv.mjs.map → transaction-BR2jg4uj.mjs.map} +1 -1
- package/dist/{validator-B2GhPM3L.mjs → validator-CLugFhHw.mjs} +47 -4
- package/dist/validator-CLugFhHw.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/apply-manager-DsNq7Zhm.mjs.map +0 -1
- package/dist/content-manager-Bx94DXSF.d.mts.map +0 -1
- package/dist/meta-manager-g3shoZES.mjs.map +0 -1
- package/dist/model-manager-DqwL8ptm.mjs.map +0 -1
- package/dist/ops-BfrdZ6GK.mjs.map +0 -1
- package/dist/server-BRkKIO3r.mjs.map +0 -1
- package/dist/validator-B2GhPM3L.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile-M7KBZtdn.mjs","names":[],"sources":["../src/git/ref-reader.ts","../src/git/reconcile.ts"],"sourcesContent":["import { type SimpleGit } from 'simple-git'\nimport { createGit } from './identity.js'\nimport type { RepoReader } from '../core/contracts/index.js'\n\n/**\n * GitRefReader — `RepoReader` over an arbitrary local git ref, without a\n * checkout. `git show <ref>:<path>` reads blobs, `ls-tree` lists a tree\n * level, `cat-file -e` answers existence. This is what lets a reconcile\n * planner read three trees (merge-base, contentrain, base branch) side by\n * side while the developer's working tree stays untouched.\n *\n * The ref is fixed at construction; the per-call `ref?` parameter of the\n * `RepoReader` contract is ignored — a bound reader answering for a\n * different ref than it was built for would be a lie. Bind another instance\n * instead.\n */\nexport class GitRefReader implements RepoReader {\n private readonly git: SimpleGit\n\n constructor(\n public readonly projectRoot: string,\n public readonly ref: string,\n ) {\n this.git = createGit(projectRoot)\n }\n\n async readFile(path: string, _ref?: string): Promise<string> {\n return this.git.show([`${this.ref}:${normalize(path)}`])\n }\n\n async listDirectory(path: string, _ref?: string): Promise<string[]> {\n try {\n // Trailing slash = \"list the tree at this path\", bare names via\n // --name-only; ls-tree is non-recursive by default, matching the\n // contract's \"does not recurse\".\n const dir = normalize(path)\n const args = dir && dir !== '.'\n ? ['ls-tree', '--name-only', this.ref, `${dir}/`]\n : ['ls-tree', '--name-only', this.ref]\n const raw = await this.git.raw(args)\n return raw\n .split('\\n')\n .map(line => line.trim())\n .filter(Boolean)\n .map(entry => entry.slice(entry.lastIndexOf('/') + 1))\n } catch {\n return []\n }\n }\n\n async fileExists(path: string, _ref?: string): Promise<boolean> {\n try {\n await this.git.raw(['cat-file', '-e', `${this.ref}:${normalize(path)}`])\n return true\n } catch {\n return false\n }\n }\n}\n\n/** Strip a trailing slash — `<ref>:dir/` and `<ref>:dir` name different things to git. */\nfunction normalize(path: string): string {\n return path.endsWith('/') ? path.slice(0, -1) : path\n}\n","import { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { BaseAdvance, ConflictResolution, ContextSource, RemotePush, SyncResult } from '@contentrain/types'\nimport { createGit, authorConfig } from './identity.js'\nimport { networkGit } from './branch-lifecycle.js'\nimport { GitRefReader } from './ref-reader.js'\nimport {\n NETWORK_TIMEOUT_MS,\n ensureContentBranch,\n isAncestor,\n pushContentBranches,\n selectiveSync,\n} from './transaction.js'\nimport { readConfig } from '../core/config.js'\nimport { planReconcile } from '../core/ops/reconcile/index.js'\nimport type { ReconcilePlan } from '../core/ops/reconcile/index.js'\nimport { applyChangesToWorktree } from '../core/ops/apply-to-worktree.js'\n\nexport interface ReconcileBranchesOptions {\n /** Preview only — plan, never touch a ref. Default true. */\n dryRun?: boolean\n /** Decisions for previously reported conflicts, matched by id. */\n resolutions?: ConflictResolution[]\n /** Stamped into the regenerated context.json. */\n source?: ContextSource\n}\n\nexport interface ReconcileBranchesResult {\n /**\n * `in_sync` — nothing to reconcile (base already contained in contentrain\n * and no content differences; a base merely behind was fast-forwarded).\n * `preview` — dry-run: the plan says what WOULD happen.\n * `conflicts` — unresolved conflicts remain; nothing was written.\n * `reconciled` — the merge commit landed and the base branch advanced.\n */\n action: 'in_sync' | 'preview' | 'conflicts' | 'reconciled'\n plan: ReconcilePlan\n base: string\n merge_base: string | null\n commit?: string\n base_advance?: BaseAdvance\n sync?: SyncResult\n remote_push?: RemotePush\n}\n\n/**\n * Content-aware reconcile of a diverged `contentrain` ↔ base pair, local\n * executor. Plans with three `GitRefReader`s (no checkout, developer tree\n * untouched), then — when clean or fully resolved — performs a REAL\n * `git merge --no-commit` in a temp worktree and writes the ENTIRE plan\n * output over it before committing: for content-owned paths the planner is\n * authoritative, never git's textual auto-merge (its line-level result can\n * be non-canonical or wrong without ever surfacing as a conflict). Git\n * still resolves everything the planner does not own — source files, lock\n * files — exactly as a manual merge would. The commit joins both parents,\n * so afterwards the base branch is an ancestor again and the ordinary\n * fast-forward advance works.\n *\n * Reconcile never runs inside a write path: it is an explicit, separate\n * step (tool, CLI, or Studio background job).\n */\nexport async function reconcileBranches(\n projectRoot: string,\n options: ReconcileBranchesOptions = {},\n): Promise<ReconcileBranchesResult> {\n const dryRun = options.dryRun ?? true\n const git = createGit(projectRoot)\n const config = await readConfig(projectRoot)\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n const baseBranch = process.env['CONTENTRAIN_BRANCH']\n ?? config?.repository?.default_branch\n ?? ((await git.raw(['branch', '--show-current'])).trim() || 'main')\n\n const currentBranch = (await git.raw(['branch', '--show-current']).catch(() => '')).trim()\n if (currentBranch === CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `The '${CONTENTRAIN_BRANCH}' branch is checked out in your working directory. `\n + `Switch to your working branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_CHECKED_OUT',\n agent_hint: 'Ask the developer to switch to their working branch, then retry.',\n developer_action: `git checkout ${baseBranch}`,\n })\n }\n\n await ensureContentBranch(projectRoot)\n\n let hasRemote = false\n try {\n const remotes = await git.getRemotes()\n hasRemote = remotes.some(r => r.name === remoteName)\n } catch {\n hasRemote = false\n }\n if (hasRemote) {\n const net = networkGit(projectRoot, NETWORK_TIMEOUT_MS)\n await Promise.all([\n net.fetch(remoteName, baseBranch).catch(() => {}),\n net.fetch(remoteName, CONTENTRAIN_BRANCH).catch(() => {}),\n ])\n }\n\n const [contentrainTip, baseTip] = await Promise.all([\n git.raw(['rev-parse', CONTENTRAIN_BRANCH]).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n ])\n\n let mergeBase: string | null = null\n try {\n mergeBase = (await git.raw(['merge-base', CONTENTRAIN_BRANCH, baseBranch])).trim() || null\n } catch {\n mergeBase = null\n }\n if (!mergeBase) {\n throw Object.assign(new Error(\n `\"${CONTENTRAIN_BRANCH}\" and \"${baseBranch}\" share no history — reconcile needs a common ancestor.`,\n ), {\n code: 'RECONCILE_NO_MERGE_BASE',\n agent_hint: 'The branches are unrelated. This needs a human decision about which history is canonical.',\n })\n }\n\n const plan = await planReconcile({\n base: new GitRefReader(projectRoot, mergeBase),\n ours: new GitRefReader(projectRoot, CONTENTRAIN_BRANCH),\n theirs: new GitRefReader(projectRoot, baseBranch),\n resolutions: options.resolutions,\n source: options.source ?? 'mcp-local',\n })\n\n // Base already contained in contentrain: there is no divergence. A base\n // merely BEHIND is fast-forwarded on the spot (that is what the ordinary\n // write path would do next anyway).\n if (await isAncestor(git, baseTip, contentrainTip)) {\n if (!dryRun && baseTip !== contentrainTip) {\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n return { action: 'reconciled', plan, base: baseBranch, merge_base: mergeBase, commit: contentrainTip, base_advance: 'advanced' }\n }\n return { action: 'in_sync', plan, base: baseBranch, merge_base: mergeBase }\n }\n\n if (dryRun) {\n return { action: 'preview', plan, base: baseBranch, merge_base: mergeBase }\n }\n if (plan.conflicts.length > 0) {\n return { action: 'conflicts', plan, base: baseBranch, merge_base: mergeBase }\n }\n\n // Execute: real merge in a temp worktree, planner output written on top.\n const worktreePath = join(tmpdir(), `cr-reconcile-${randomUUID()}`)\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\n const wtGit = createGit(worktreePath, { config: authorConfig() })\n\n try {\n try {\n await wtGit.merge([baseBranch, '--no-commit', '--no-ff'])\n } catch {\n // Conflicts inside the worktree are expected — the planner's output\n // overwrites every content-owned path next. Anything left unmerged\n // after that is genuinely outside the planner's scope.\n }\n\n await applyChangesToWorktree(worktreePath, plan.changes)\n await wtGit.raw(['add', '-A'])\n\n const unmerged = (await wtGit.raw(['ls-files', '-u'])).trim()\n if (unmerged) {\n const paths = [...new Set(unmerged.split('\\n').map(l => l.split('\\t')[1]).filter(Boolean))]\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Reconcile stopped: ${paths.length} non-content file(s) conflict between \"${CONTENTRAIN_BRANCH}\" and \"${baseBranch}\": ${paths.join(', ')}. `\n + `These are outside the content planner's scope and need a manual merge.`,\n ), {\n code: 'RECONCILE_SOURCE_CONFLICT',\n agent_hint: 'Non-content files conflict. Ask the developer to merge the branches manually for those paths.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${baseBranch}`,\n })\n }\n\n await wtGit.commit(\n `[contentrain] reconcile: merge ${baseBranch} into ${CONTENTRAIN_BRANCH}`,\n { '--no-verify': null },\n )\n\n const [newTip, previousBaseRef, statusBeforeUpdate] = await Promise.all([\n wtGit.raw(['rev-parse', 'HEAD']).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n git.status(),\n ])\n const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map(f => f.path))\n\n // The merge commit has baseBranch as a parent, so this is a true\n // fast-forward again — the whole point of the exercise.\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, newTip])\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n const sync = await selectiveSync(projectRoot, worktreePath, newTip, previousBaseRef, dirtyFilesBeforeUpdate)\n\n let remotePush: RemotePush = 'no-remote'\n if (hasRemote) {\n remotePush = await pushContentBranches(projectRoot, worktreePath, wtGit, remoteName, baseBranch, true)\n }\n\n return {\n action: 'reconciled',\n plan,\n base: baseBranch,\n merge_base: mergeBase,\n commit: newTip,\n base_advance: 'advanced',\n sync,\n remote_push: remotePush,\n }\n } finally {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reconcile-MiURoMUx.mjs","names":[],"sources":["../src/git/ref-reader.ts","../src/git/reconcile.ts"],"sourcesContent":["import { type SimpleGit } from 'simple-git'\nimport { createGit } from './identity.js'\nimport type { RepoReader } from '../core/contracts/index.js'\n\n/**\n * GitRefReader — `RepoReader` over an arbitrary local git ref, without a\n * checkout. `git show <ref>:<path>` reads blobs, `ls-tree` lists a tree\n * level, `cat-file -e` answers existence. This is what lets a reconcile\n * planner read three trees (merge-base, contentrain, base branch) side by\n * side while the developer's working tree stays untouched.\n *\n * The ref is fixed at construction; the per-call `ref?` parameter of the\n * `RepoReader` contract is ignored — a bound reader answering for a\n * different ref than it was built for would be a lie. Bind another instance\n * instead.\n */\nexport class GitRefReader implements RepoReader {\n private readonly git: SimpleGit\n\n constructor(\n public readonly projectRoot: string,\n public readonly ref: string,\n ) {\n this.git = createGit(projectRoot)\n }\n\n async readFile(path: string, _ref?: string): Promise<string> {\n return this.git.show([`${this.ref}:${normalize(path)}`])\n }\n\n async listDirectory(path: string, _ref?: string): Promise<string[]> {\n try {\n // Trailing slash = \"list the tree at this path\", bare names via\n // --name-only; ls-tree is non-recursive by default, matching the\n // contract's \"does not recurse\".\n const dir = normalize(path)\n const args = dir && dir !== '.'\n ? ['ls-tree', '--name-only', this.ref, `${dir}/`]\n : ['ls-tree', '--name-only', this.ref]\n const raw = await this.git.raw(args)\n return raw\n .split('\\n')\n .map(line => line.trim())\n .filter(Boolean)\n .map(entry => entry.slice(entry.lastIndexOf('/') + 1))\n } catch {\n return []\n }\n }\n\n async fileExists(path: string, _ref?: string): Promise<boolean> {\n try {\n await this.git.raw(['cat-file', '-e', `${this.ref}:${normalize(path)}`])\n return true\n } catch {\n return false\n }\n }\n}\n\n/** Strip a trailing slash — `<ref>:dir/` and `<ref>:dir` name different things to git. */\nfunction normalize(path: string): string {\n return path.endsWith('/') ? path.slice(0, -1) : path\n}\n","import { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { BaseAdvance, ConflictResolution, ContextSource, RemotePush, SyncResult } from '@contentrain/types'\nimport { createGit, authorConfig } from './identity.js'\nimport { networkGit } from './branch-lifecycle.js'\nimport { GitRefReader } from './ref-reader.js'\nimport {\n NETWORK_TIMEOUT_MS,\n ensureContentBranch,\n isAncestor,\n pushContentBranches,\n selectiveSync,\n} from './transaction.js'\nimport { readConfig } from '../core/config.js'\nimport { planReconcile } from '../core/ops/reconcile/index.js'\nimport type { ReconcilePlan } from '../core/ops/reconcile/index.js'\nimport { applyChangesToWorktree } from '../core/ops/apply-to-worktree.js'\n\nexport interface ReconcileBranchesOptions {\n /** Preview only — plan, never touch a ref. Default true. */\n dryRun?: boolean\n /** Decisions for previously reported conflicts, matched by id. */\n resolutions?: ConflictResolution[]\n /** Stamped into the regenerated context.json. */\n source?: ContextSource\n}\n\nexport interface ReconcileBranchesResult {\n /**\n * `in_sync` — nothing to reconcile (base already contained in contentrain\n * and no content differences; a base merely behind was fast-forwarded).\n * `preview` — dry-run: the plan says what WOULD happen.\n * `conflicts` — unresolved conflicts remain; nothing was written.\n * `reconciled` — the merge commit landed and the base branch advanced.\n */\n action: 'in_sync' | 'preview' | 'conflicts' | 'reconciled'\n plan: ReconcilePlan\n base: string\n merge_base: string | null\n commit?: string\n base_advance?: BaseAdvance\n sync?: SyncResult\n remote_push?: RemotePush\n}\n\n/**\n * Content-aware reconcile of a diverged `contentrain` ↔ base pair, local\n * executor. Plans with three `GitRefReader`s (no checkout, developer tree\n * untouched), then — when clean or fully resolved — performs a REAL\n * `git merge --no-commit` in a temp worktree and writes the ENTIRE plan\n * output over it before committing: for content-owned paths the planner is\n * authoritative, never git's textual auto-merge (its line-level result can\n * be non-canonical or wrong without ever surfacing as a conflict). Git\n * still resolves everything the planner does not own — source files, lock\n * files — exactly as a manual merge would. The commit joins both parents,\n * so afterwards the base branch is an ancestor again and the ordinary\n * fast-forward advance works.\n *\n * Reconcile never runs inside a write path: it is an explicit, separate\n * step (tool, CLI, or Studio background job).\n */\nexport async function reconcileBranches(\n projectRoot: string,\n options: ReconcileBranchesOptions = {},\n): Promise<ReconcileBranchesResult> {\n const dryRun = options.dryRun ?? true\n const git = createGit(projectRoot)\n const config = await readConfig(projectRoot)\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n const baseBranch = process.env['CONTENTRAIN_BRANCH']\n ?? config?.repository?.default_branch\n ?? ((await git.raw(['branch', '--show-current'])).trim() || 'main')\n\n const currentBranch = (await git.raw(['branch', '--show-current']).catch(() => '')).trim()\n if (currentBranch === CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `The '${CONTENTRAIN_BRANCH}' branch is checked out in your working directory. `\n + `Switch to your working branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_CHECKED_OUT',\n agent_hint: 'Ask the developer to switch to their working branch, then retry.',\n developer_action: `git checkout ${baseBranch}`,\n })\n }\n\n await ensureContentBranch(projectRoot)\n\n let hasRemote = false\n try {\n const remotes = await git.getRemotes()\n hasRemote = remotes.some(r => r.name === remoteName)\n } catch {\n hasRemote = false\n }\n if (hasRemote) {\n const net = networkGit(projectRoot, NETWORK_TIMEOUT_MS)\n await Promise.all([\n net.fetch(remoteName, baseBranch).catch(() => {}),\n net.fetch(remoteName, CONTENTRAIN_BRANCH).catch(() => {}),\n ])\n }\n\n const [contentrainTip, baseTip] = await Promise.all([\n git.raw(['rev-parse', CONTENTRAIN_BRANCH]).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n ])\n\n let mergeBase: string | null = null\n try {\n mergeBase = (await git.raw(['merge-base', CONTENTRAIN_BRANCH, baseBranch])).trim() || null\n } catch {\n mergeBase = null\n }\n if (!mergeBase) {\n throw Object.assign(new Error(\n `\"${CONTENTRAIN_BRANCH}\" and \"${baseBranch}\" share no history — reconcile needs a common ancestor.`,\n ), {\n code: 'RECONCILE_NO_MERGE_BASE',\n agent_hint: 'The branches are unrelated. This needs a human decision about which history is canonical.',\n })\n }\n\n const plan = await planReconcile({\n base: new GitRefReader(projectRoot, mergeBase),\n ours: new GitRefReader(projectRoot, CONTENTRAIN_BRANCH),\n theirs: new GitRefReader(projectRoot, baseBranch),\n resolutions: options.resolutions,\n source: options.source ?? 'mcp-local',\n })\n\n // Base already contained in contentrain: there is no divergence. A base\n // merely BEHIND is fast-forwarded on the spot (that is what the ordinary\n // write path would do next anyway).\n if (await isAncestor(git, baseTip, contentrainTip)) {\n if (!dryRun && baseTip !== contentrainTip) {\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n return { action: 'reconciled', plan, base: baseBranch, merge_base: mergeBase, commit: contentrainTip, base_advance: 'advanced' }\n }\n return { action: 'in_sync', plan, base: baseBranch, merge_base: mergeBase }\n }\n\n if (dryRun) {\n return { action: 'preview', plan, base: baseBranch, merge_base: mergeBase }\n }\n if (plan.conflicts.length > 0) {\n return { action: 'conflicts', plan, base: baseBranch, merge_base: mergeBase }\n }\n\n // Execute: real merge in a temp worktree, planner output written on top.\n const worktreePath = join(tmpdir(), `cr-reconcile-${randomUUID()}`)\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\n const wtGit = createGit(worktreePath, { config: authorConfig() })\n\n try {\n try {\n await wtGit.merge([baseBranch, '--no-commit', '--no-ff'])\n } catch {\n // Conflicts inside the worktree are expected — the planner's output\n // overwrites every content-owned path next. Anything left unmerged\n // after that is genuinely outside the planner's scope.\n }\n\n await applyChangesToWorktree(worktreePath, plan.changes)\n await wtGit.raw(['add', '-A'])\n\n const unmerged = (await wtGit.raw(['ls-files', '-u'])).trim()\n if (unmerged) {\n const paths = [...new Set(unmerged.split('\\n').map(l => l.split('\\t')[1]).filter(Boolean))]\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Reconcile stopped: ${paths.length} non-content file(s) conflict between \"${CONTENTRAIN_BRANCH}\" and \"${baseBranch}\": ${paths.join(', ')}. `\n + `These are outside the content planner's scope and need a manual merge.`,\n ), {\n code: 'RECONCILE_SOURCE_CONFLICT',\n agent_hint: 'Non-content files conflict. Ask the developer to merge the branches manually for those paths.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${baseBranch}`,\n })\n }\n\n await wtGit.commit(\n `[contentrain] reconcile: merge ${baseBranch} into ${CONTENTRAIN_BRANCH}`,\n { '--no-verify': null },\n )\n\n const [newTip, previousBaseRef, statusBeforeUpdate] = await Promise.all([\n wtGit.raw(['rev-parse', 'HEAD']).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n git.status(),\n ])\n const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map(f => f.path))\n\n // The merge commit has baseBranch as a parent, so this is a true\n // fast-forward again — the whole point of the exercise.\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, newTip])\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n const sync = await selectiveSync(projectRoot, worktreePath, newTip, previousBaseRef, dirtyFilesBeforeUpdate)\n\n let remotePush: RemotePush = 'no-remote'\n if (hasRemote) {\n remotePush = await pushContentBranches(projectRoot, worktreePath, wtGit, remoteName, baseBranch, true)\n }\n\n return {\n action: 'reconciled',\n plan,\n base: baseBranch,\n merge_base: mergeBase,\n commit: newTip,\n base_advance: 'advanced',\n sync,\n remote_push: remotePush,\n }\n } finally {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
import "../../contracts-DfL0BfrD.mjs";
|
|
2
|
-
import "../../reconcile-
|
|
3
|
-
import "../../context-
|
|
4
|
-
import "../../model-manager-
|
|
5
|
-
import "../../ops-
|
|
2
|
+
import "../../reconcile-B8qhwneb.mjs";
|
|
3
|
+
import "../../context-BHfO4WBv.mjs";
|
|
4
|
+
import "../../model-manager-CfwJWvz7.mjs";
|
|
5
|
+
import "../../ops-B2P11b8v.mjs";
|
|
6
6
|
import "../../fs-DJv5G8uL.mjs";
|
|
7
|
-
import "../../meta-manager-
|
|
7
|
+
import "../../meta-manager-Cd6AfuGS.mjs";
|
|
8
8
|
import "../../config-B8a4JBT1.mjs";
|
|
9
|
-
import "../../validator-
|
|
9
|
+
import "../../validator-CLugFhHw.mjs";
|
|
10
10
|
import "../../identity-CxAB9Qb8.mjs";
|
|
11
11
|
import "../../branch-lifecycle-DUOoyNWn.mjs";
|
|
12
12
|
import "../../id-CjGMPP1k.mjs";
|
|
13
|
-
import "../../transaction-
|
|
14
|
-
import "../../local-
|
|
13
|
+
import "../../transaction-BR2jg4uj.mjs";
|
|
14
|
+
import "../../local-BVww7ArO.mjs";
|
|
15
15
|
import "../../detect-CI32ynpq.mjs";
|
|
16
16
|
import "../../annotations-CXHPnS-M.mjs";
|
|
17
|
-
import { n as createServer } from "../../server-
|
|
17
|
+
import { n as createServer } from "../../server-CXfGWDd4.mjs";
|
|
18
18
|
import "../../errors-BmLE-JSC.mjs";
|
|
19
|
-
import "../../reconcile-
|
|
19
|
+
import "../../reconcile-MiURoMUx.mjs";
|
|
20
20
|
import "../../scan-config-C5zzygeH.mjs";
|
|
21
21
|
import "../../graph-builder-C9NS7y-a.mjs";
|
|
22
22
|
import "../../scanner-CEiT8Tgx.mjs";
|
|
23
23
|
import "../../tsx-parser-go5V9zoc.mjs";
|
|
24
|
-
import "../../apply-manager-
|
|
25
|
-
import "../../doctor-
|
|
24
|
+
import "../../apply-manager-eZ6zA99Y.mjs";
|
|
25
|
+
import "../../doctor-VnOzDTbk.mjs";
|
|
26
26
|
import { randomUUID } from "node:crypto";
|
|
27
27
|
import http from "node:http";
|
|
28
28
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { n as readContext } from "./context-
|
|
2
|
-
import {
|
|
3
|
-
import { a as planModelDelete, i as planModelSave, n as planVocabularySave, o as planContentSave, s as planContentDelete, t as planVocabularyDelete } from "./ops-
|
|
1
|
+
import { n as readContext } from "./context-BHfO4WBv.mjs";
|
|
2
|
+
import { A as contentDirPath, C as resolveMdFilePath, O as writeContent, S as resolveLocaleStrategy, _ as listContent, a as countEntries, b as resolveContentDir, d as readModel, g as deleteContent, h as writeModel, i as collectFieldPaths, j as contentFilePath, m as validateModelDefinition, r as checkReferences, s as fieldDefZodSchema, u as listModels, x as resolveJsonFilePath } from "./model-manager-CfwJWvz7.mjs";
|
|
3
|
+
import { a as planModelDelete, i as planModelSave, n as planVocabularySave, o as planContentSave, s as planContentDelete, t as planVocabularyDelete } from "./ops-B2P11b8v.mjs";
|
|
4
4
|
import { a as readJson, n as ensureDir, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-DJv5G8uL.mjs";
|
|
5
|
-
import { a as writeMeta, i as readMeta, o as writeMetaEntries, r as mergeEntryMeta, t as applyStatusChange } from "./meta-manager-
|
|
5
|
+
import { a as writeMeta, i as readMeta, o as writeMetaEntries, r as mergeEntryMeta, t as applyStatusChange } from "./meta-manager-Cd6AfuGS.mjs";
|
|
6
6
|
import { t as OverlayReader } from "./overlay-reader-Bx1YxGXq.mjs";
|
|
7
7
|
import { n as readVocabulary, t as readConfig } from "./config-B8a4JBT1.mjs";
|
|
8
|
-
import { t as validateProject } from "./validator-
|
|
8
|
+
import { t as validateProject } from "./validator-CLugFhHw.mjs";
|
|
9
9
|
import { r as createGit } from "./identity-CxAB9Qb8.mjs";
|
|
10
10
|
import { a as contentBranchRelation, c as listRemoteCrBranches, i as cleanupMergedBranches, n as checkBranchHealth, o as deleteRemoteBranch, u as pruneMergedRemoteBranches } from "./branch-lifecycle-DUOoyNWn.mjs";
|
|
11
|
-
import { i as ensureContentBranch, n as buildBranchName, o as mergeBranch, r as createTransaction } from "./transaction-
|
|
12
|
-
import { n as isMerged, t as LocalProvider } from "./local-
|
|
11
|
+
import { i as ensureContentBranch, n as buildBranchName, o as mergeBranch, r as createTransaction } from "./transaction-BR2jg4uj.mjs";
|
|
12
|
+
import { n as isMerged, t as LocalProvider } from "./local-BVww7ArO.mjs";
|
|
13
13
|
import { n as isToolAvailable, t as TOOL_REQUIREMENTS } from "./availability-DYHNoP0_.mjs";
|
|
14
14
|
import { t as detectStack } from "./detect-CI32ynpq.mjs";
|
|
15
15
|
import { t as TOOL_ANNOTATIONS } from "./annotations-CXHPnS-M.mjs";
|
|
16
16
|
import { n as listTemplates, t as getTemplate } from "./templates-BZbohJc8.mjs";
|
|
17
17
|
import { t as normalizeOperationError } from "./errors-BmLE-JSC.mjs";
|
|
18
|
-
import { t as reconcileBranches } from "./reconcile-
|
|
18
|
+
import { t as reconcileBranches } from "./reconcile-MiURoMUx.mjs";
|
|
19
19
|
import { t as buildGraph } from "./graph-builder-C9NS7y-a.mjs";
|
|
20
20
|
import { n as scanSummary, t as scanCandidates } from "./scanner-CEiT8Tgx.mjs";
|
|
21
|
-
import { n as applyExtract, r as applyReuse } from "./apply-manager-
|
|
22
|
-
import { t as runDoctor } from "./doctor-
|
|
21
|
+
import { n as applyExtract, r as applyReuse } from "./apply-manager-eZ6zA99Y.mjs";
|
|
22
|
+
import { t as runDoctor } from "./doctor-VnOzDTbk.mjs";
|
|
23
23
|
import { join } from "node:path";
|
|
24
24
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
25
25
|
import { CONTENTRAIN_BRANCH } from "@contentrain/types";
|
|
@@ -243,8 +243,8 @@ function registerContextTools(server, provider, projectRoot) {
|
|
|
243
243
|
updated_by: "Author identifier string",
|
|
244
244
|
approved_by: "Optional: approver identifier",
|
|
245
245
|
version: "Optional: version string",
|
|
246
|
-
publish_at: "Optional: ISO 8601 date for scheduled publishing",
|
|
247
|
-
expire_at: "Optional: ISO 8601 date for scheduled expiry (must be after publish_at)"
|
|
246
|
+
publish_at: "Optional: ISO 8601 date for scheduled publishing. A delivery gate on top of status \"published\" — it never changes status, so a draft with a past publish_at stays a draft. Meta only; never written to the content file.",
|
|
247
|
+
expire_at: "Optional: ISO 8601 date for scheduled expiry (must be after publish_at). Same gate semantics as publish_at."
|
|
248
248
|
}
|
|
249
249
|
},
|
|
250
250
|
locale_strategies: {
|
|
@@ -902,7 +902,7 @@ function registerModelTools(server, provider, projectRoot) {
|
|
|
902
902
|
}],
|
|
903
903
|
isError: true
|
|
904
904
|
};
|
|
905
|
-
const { errors, warnings: schemaWarnings } = validateModel(input);
|
|
905
|
+
const { errors, warnings: schemaWarnings } = validateModel(input, { existingFieldPaths: collectFieldPaths((await readModel(provider, input.id))?.fields) });
|
|
906
906
|
if (errors.length > 0) return {
|
|
907
907
|
content: [{
|
|
908
908
|
type: "text",
|
|
@@ -1110,8 +1110,8 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1110
1110
|
slug: z.string().optional().describe("Slug (document only)"),
|
|
1111
1111
|
locale: z.string().optional().describe("Locale code (defaults to config default)"),
|
|
1112
1112
|
data: z.record(z.string(), z.unknown()).describe("Content data. For documents, include \"body\" key for markdown content."),
|
|
1113
|
-
publish_at: z.string().optional().describe("ISO 8601
|
|
1114
|
-
expire_at: z.string().optional().describe("ISO 8601
|
|
1113
|
+
publish_at: z.string().nullable().optional().describe("Scheduled publish date, ISO 8601. Stored in meta only — never in the content file — and it does NOT change status: a draft stays a draft, and a published entry is delivered only once this date has passed. Publish with contentrain_bulk update_status. Pass null to clear a previously set date; omit to leave it unchanged."),
|
|
1114
|
+
expire_at: z.string().nullable().optional().describe("Scheduled expiry, ISO 8601, must be after publish_at. Same rules as publish_at: meta only, never changes status, null clears, omit leaves unchanged.")
|
|
1115
1115
|
})).describe("Content entries to save")
|
|
1116
1116
|
}, TOOL_ANNOTATIONS["contentrain_content_save"], async (input) => {
|
|
1117
1117
|
const config = await readConfig(provider);
|
|
@@ -1138,7 +1138,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1138
1138
|
}],
|
|
1139
1139
|
isError: true
|
|
1140
1140
|
};
|
|
1141
|
-
if (entry.publish_at
|
|
1141
|
+
if (typeof entry.publish_at === "string") {
|
|
1142
1142
|
const d = new Date(entry.publish_at);
|
|
1143
1143
|
if (Number.isNaN(d.getTime())) return {
|
|
1144
1144
|
content: [{
|
|
@@ -1148,7 +1148,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1148
1148
|
isError: true
|
|
1149
1149
|
};
|
|
1150
1150
|
}
|
|
1151
|
-
if (entry.expire_at
|
|
1151
|
+
if (typeof entry.expire_at === "string") {
|
|
1152
1152
|
const d = new Date(entry.expire_at);
|
|
1153
1153
|
if (Number.isNaN(d.getTime())) return {
|
|
1154
1154
|
content: [{
|
|
@@ -1158,7 +1158,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1158
1158
|
isError: true
|
|
1159
1159
|
};
|
|
1160
1160
|
}
|
|
1161
|
-
if (entry.publish_at
|
|
1161
|
+
if (typeof entry.publish_at === "string" && typeof entry.expire_at === "string") {
|
|
1162
1162
|
if (new Date(entry.expire_at) <= new Date(entry.publish_at)) return {
|
|
1163
1163
|
content: [{
|
|
1164
1164
|
type: "text",
|
|
@@ -1167,9 +1167,8 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1167
1167
|
isError: true
|
|
1168
1168
|
};
|
|
1169
1169
|
}
|
|
1170
|
-
if (entry.publish_at !== void 0) entry.data["publish_at"] = entry.publish_at;
|
|
1171
|
-
if (entry.expire_at !== void 0) entry.data["expire_at"] = entry.expire_at;
|
|
1172
1170
|
}
|
|
1171
|
+
const schedulingTouched = input.entries.some((e) => e.publish_at !== void 0 || e.expire_at !== void 0);
|
|
1173
1172
|
const readiness = await provider.checkWriteReadiness?.();
|
|
1174
1173
|
if (readiness?.blocked) return {
|
|
1175
1174
|
content: [{
|
|
@@ -1262,6 +1261,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1262
1261
|
advisory_note: "Save succeeded. Review these warnings and consider consolidating duplicate values."
|
|
1263
1262
|
} : {},
|
|
1264
1263
|
...warnings.length > 0 ? { warnings } : {},
|
|
1264
|
+
...schedulingTouched ? { scheduling_note: "publish_at/expire_at were written to meta only — not to the content file — and status is unchanged: a draft is still a draft. They gate delivery of a *published* entry (served once publish_at has passed, until expire_at). To publish, call contentrain_bulk update_status." } : {},
|
|
1265
1265
|
validation: {
|
|
1266
1266
|
valid: true,
|
|
1267
1267
|
errors: []
|
|
@@ -1271,6 +1271,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1271
1271
|
...divergenceNextSteps(commit),
|
|
1272
1272
|
...allAdvisories.length > 0 ? ["ADVISORY: Duplicate values detected — review advisories above"] : [],
|
|
1273
1273
|
...warnings.length > 0 ? ["REVIEW: see warnings above — the save was not blocked by them"] : [],
|
|
1274
|
+
...schedulingTouched ? ["PUBLISH: publish_at did not change status — use contentrain_bulk update_status if this entry should go live"] : [],
|
|
1274
1275
|
...model.kind === "collection" ? ["Use contentrain_content_list to verify", "Add more entries or publish"] : ["Use contentrain_content_list to verify"]
|
|
1275
1276
|
]
|
|
1276
1277
|
}, null, 2)
|
|
@@ -1661,9 +1662,9 @@ async function resolveMergeBranch(git, input) {
|
|
|
1661
1662
|
}))).toSorted((left, right) => right.t - left.t)[0].b };
|
|
1662
1663
|
}
|
|
1663
1664
|
function registerWorkflowTools(server, provider, projectRoot) {
|
|
1664
|
-
server.tool("contentrain_validate", "Validate project content against model schemas. Detects required field violations, type mismatches, broken relations, secret leaks, i18n parity issues, and more. If fix:true, auto-fixes structural issues (canonical sort, orphan meta, missing locale files, stray non-i18n meta layout) and backfills a missing model title_field — do NOT manually edit .contentrain/ files.", {
|
|
1665
|
+
server.tool("contentrain_validate", "Validate project content against model schemas. Detects required field violations, type mismatches, broken relations, secret leaks, i18n parity issues, and more. If fix:true, auto-fixes structural issues (canonical sort, orphan meta, missing locale files, stray non-i18n meta layout, scheduling keys an older content_save leaked into document frontmatter) and backfills a missing model title_field — do NOT manually edit .contentrain/ files.", {
|
|
1665
1666
|
model: z.string().optional().describe("Model ID to validate (omit for all models)"),
|
|
1666
|
-
fix: z.boolean().optional().describe("Auto-fix structural issues (canonical sort, orphan meta, missing locale files, stray non-i18n meta layout) and backfill a missing model title_field. Each backfilled title_field is reported as a notice naming the rule that chose it. Default: false")
|
|
1667
|
+
fix: z.boolean().optional().describe("Auto-fix structural issues (canonical sort, orphan meta, missing locale files, stray non-i18n meta layout, publish_at/expire_at copies an older content_save left in document frontmatter when meta holds the same key) and backfill a missing model title_field. Each backfilled title_field is reported as a notice naming the rule that chose it. Default: false")
|
|
1667
1668
|
}, TOOL_ANNOTATIONS["contentrain_validate"], async (input) => {
|
|
1668
1669
|
if (input.fix && !projectRoot) return capabilityError("contentrain_validate", "localWorktree");
|
|
1669
1670
|
if (!await readConfig(provider)) return {
|
|
@@ -2421,6 +2422,7 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2421
2422
|
source_locale: z.string().optional().describe("Source locale for copy_locale operation"),
|
|
2422
2423
|
target_locale: z.string().optional().describe("Target locale for copy_locale operation"),
|
|
2423
2424
|
entry_ids: z.array(z.string()).optional().describe("Entry IDs for update_status (collection models) or delete_entries"),
|
|
2425
|
+
slugs: z.array(z.string()).optional().describe("Document slugs for update_status (document models) — a document is addressed by slug, the same identity contentrain_content_save uses, not by entry ID"),
|
|
2424
2426
|
locale: z.string().optional().describe("Scope update_status to a single locale (i18n models only; defaults to every supported locale)"),
|
|
2425
2427
|
status: z.enum([
|
|
2426
2428
|
"draft",
|
|
@@ -2571,22 +2573,39 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2571
2573
|
}],
|
|
2572
2574
|
isError: true
|
|
2573
2575
|
};
|
|
2574
|
-
|
|
2576
|
+
const keyedByEntry = model.kind === "collection";
|
|
2577
|
+
const keyedBySlug = model.kind === "document";
|
|
2578
|
+
const hasEntryIds = Boolean(input.entry_ids && input.entry_ids.length > 0);
|
|
2579
|
+
const hasSlugs = Boolean(input.slugs && input.slugs.length > 0);
|
|
2580
|
+
if (keyedBySlug && hasEntryIds) return {
|
|
2575
2581
|
content: [{
|
|
2576
2582
|
type: "text",
|
|
2577
|
-
text: JSON.stringify({ error: `Model "${input.model}" is a document model
|
|
2583
|
+
text: JSON.stringify({ error: `Model "${input.model}" is a document model — documents are keyed by slug, not entry ID. Pass slugs instead of entry_ids.` })
|
|
2578
2584
|
}],
|
|
2579
2585
|
isError: true
|
|
2580
2586
|
};
|
|
2581
|
-
|
|
2582
|
-
|
|
2587
|
+
if (keyedBySlug && !hasSlugs) return {
|
|
2588
|
+
content: [{
|
|
2589
|
+
type: "text",
|
|
2590
|
+
text: JSON.stringify({ error: `update_status requires slugs for document model "${input.model}".` })
|
|
2591
|
+
}],
|
|
2592
|
+
isError: true
|
|
2593
|
+
};
|
|
2594
|
+
if (!keyedBySlug && hasSlugs) return {
|
|
2595
|
+
content: [{
|
|
2596
|
+
type: "text",
|
|
2597
|
+
text: JSON.stringify({ error: `slugs only apply to document models — "${input.model}" is a ${model.kind} model. ${keyedByEntry ? "Pass entry_ids." : "Omit both entry_ids and slugs."}` })
|
|
2598
|
+
}],
|
|
2599
|
+
isError: true
|
|
2600
|
+
};
|
|
2601
|
+
if (keyedByEntry && !hasEntryIds) return {
|
|
2583
2602
|
content: [{
|
|
2584
2603
|
type: "text",
|
|
2585
2604
|
text: JSON.stringify({ error: `update_status requires entry_ids for collection model "${input.model}".` })
|
|
2586
2605
|
}],
|
|
2587
2606
|
isError: true
|
|
2588
2607
|
};
|
|
2589
|
-
if (!keyedByEntry &&
|
|
2608
|
+
if (!keyedByEntry && !keyedBySlug && hasEntryIds) return {
|
|
2590
2609
|
content: [{
|
|
2591
2610
|
type: "text",
|
|
2592
2611
|
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.` })
|
|
@@ -2624,6 +2643,24 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2624
2643
|
defaultLocale: config.locales.default
|
|
2625
2644
|
}, updates);
|
|
2626
2645
|
if (written.length > 0) updatedPerLocale[locale] = written;
|
|
2646
|
+
} else if (keyedBySlug) {
|
|
2647
|
+
const written = [];
|
|
2648
|
+
for (const slug of input.slugs) {
|
|
2649
|
+
const existing = await readMeta(wt, model, {
|
|
2650
|
+
locale,
|
|
2651
|
+
slug,
|
|
2652
|
+
defaultLocale: config.locales.default
|
|
2653
|
+
});
|
|
2654
|
+
if (!existing) continue;
|
|
2655
|
+
await writeMeta(wt, model, {
|
|
2656
|
+
locale,
|
|
2657
|
+
slug,
|
|
2658
|
+
defaultLocale: config.locales.default
|
|
2659
|
+
}, applyStatusChange(existing, input.status));
|
|
2660
|
+
written.push(slug);
|
|
2661
|
+
foundIds.add(slug);
|
|
2662
|
+
}
|
|
2663
|
+
if (written.length > 0) updatedPerLocale[locale] = written;
|
|
2627
2664
|
} else {
|
|
2628
2665
|
const existing = await readMeta(wt, model, {
|
|
2629
2666
|
locale,
|
|
@@ -2638,14 +2675,15 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2638
2675
|
}
|
|
2639
2676
|
});
|
|
2640
2677
|
const updatedCount = Object.values(updatedPerLocale).reduce((n, ids) => n + ids.length, 0);
|
|
2641
|
-
const
|
|
2678
|
+
const requested = keyedByEntry ? input.entry_ids : keyedBySlug ? input.slugs : [];
|
|
2679
|
+
const notFound = requested.filter((key) => !foundIds.has(key));
|
|
2642
2680
|
if (updatedCount === 0) {
|
|
2643
2681
|
await tx.cleanup();
|
|
2644
2682
|
return {
|
|
2645
2683
|
content: [{
|
|
2646
2684
|
type: "text",
|
|
2647
2685
|
text: JSON.stringify({
|
|
2648
|
-
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.`,
|
|
2686
|
+
error: keyedByEntry ? `No meta records found for the given entry_ids in model "${input.model}" across locales [${targetLocales.join(", ")}]. Nothing was changed.` : keyedBySlug ? `No meta records found for the given slugs in model "${input.model}" across locales [${targetLocales.join(", ")}]. Nothing was changed. Check the slugs with contentrain_content_list.` : `No meta record found for model "${input.model}" across locales [${targetLocales.join(", ")}]. Nothing was changed.`,
|
|
2649
2687
|
not_found: notFound.length > 0 ? notFound : void 0
|
|
2650
2688
|
}, null, 2)
|
|
2651
2689
|
}],
|
|
@@ -2655,7 +2693,7 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2655
2693
|
await tx.commit(`[contentrain] bulk: update status → ${input.status} for ${input.model}`, {
|
|
2656
2694
|
tool: "contentrain_bulk",
|
|
2657
2695
|
model: input.model,
|
|
2658
|
-
...
|
|
2696
|
+
...requested.length > 0 ? { entries: requested } : {}
|
|
2659
2697
|
});
|
|
2660
2698
|
const gitResult = await tx.complete();
|
|
2661
2699
|
return { content: [{
|
|
@@ -2921,7 +2959,7 @@ function registerMediaTools(server, provider, _projectRoot) {
|
|
|
2921
2959
|
}
|
|
2922
2960
|
//#endregion
|
|
2923
2961
|
//#region package.json
|
|
2924
|
-
var version = "3.1.
|
|
2962
|
+
var version = "3.1.8";
|
|
2925
2963
|
//#endregion
|
|
2926
2964
|
//#region src/server.ts
|
|
2927
2965
|
/**
|
|
@@ -2996,4 +3034,4 @@ function resolveServerContext(input) {
|
|
|
2996
3034
|
//#endregion
|
|
2997
3035
|
export { createServer as n, DEFAULT_INSTRUCTIONS as t };
|
|
2998
3036
|
|
|
2999
|
-
//# sourceMappingURL=server-
|
|
3037
|
+
//# sourceMappingURL=server-CXfGWDd4.mjs.map
|