@contentrain/mcp 3.0.2 → 3.1.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.
Files changed (162) hide show
  1. package/README.md +6 -3
  2. package/dist/{annotations-C-tQVmot.mjs → annotations-CXHPnS-M.mjs} +8 -1
  3. package/dist/annotations-CXHPnS-M.mjs.map +1 -0
  4. package/dist/{apply-manager-BybvnUVC.mjs → apply-manager-DsNq7Zhm.mjs} +7 -7
  5. package/dist/{apply-manager-BybvnUVC.mjs.map → apply-manager-DsNq7Zhm.mjs.map} +1 -1
  6. package/dist/{astro-parser-Cb9uuBpW.mjs → astro-parser-dhFgc2wA.mjs} +3 -3
  7. package/dist/{astro-parser-Cb9uuBpW.mjs.map → astro-parser-dhFgc2wA.mjs.map} +1 -1
  8. package/dist/{availability-Dgcmvv99.mjs → availability-DYHNoP0_.mjs} +5 -1
  9. package/dist/availability-DYHNoP0_.mjs.map +1 -0
  10. package/dist/{branch-lifecycle-DhzHfzs3.d.mts → branch-lifecycle-DGlgjnC7.d.mts} +32 -2
  11. package/dist/branch-lifecycle-DGlgjnC7.d.mts.map +1 -0
  12. package/dist/{branch-lifecycle-B6PXZ2N7.mjs → branch-lifecycle-DUOoyNWn.mjs} +32 -4
  13. package/dist/branch-lifecycle-DUOoyNWn.mjs.map +1 -0
  14. package/dist/{compiler-B6hNQeCC.mjs → compiler-CR9F0wZZ.mjs} +2 -2
  15. package/dist/{compiler-B6hNQeCC.mjs.map → compiler-CR9F0wZZ.mjs.map} +1 -1
  16. package/dist/{compiler-sfc.cjs-8yY7b5bx.mjs → compiler-sfc.cjs-Cpexs92i.mjs} +2 -2
  17. package/dist/{compiler-sfc.cjs-8yY7b5bx.mjs.map → compiler-sfc.cjs-Cpexs92i.mjs.map} +1 -1
  18. package/dist/{config-VlRKRwAS.mjs → config-B8a4JBT1.mjs} +2 -2
  19. package/dist/{config-VlRKRwAS.mjs.map → config-B8a4JBT1.mjs.map} +1 -1
  20. package/dist/{content-manager-Dn4tqDw1.d.mts → content-manager-Bx94DXSF.d.mts} +2 -2
  21. package/dist/{content-manager-Dn4tqDw1.d.mts.map → content-manager-Bx94DXSF.d.mts.map} +1 -1
  22. package/dist/{context-C3x6I3cE.mjs → context-CxpTATh4.mjs} +7 -7
  23. package/dist/context-CxpTATh4.mjs.map +1 -0
  24. package/dist/core/apply-manager.mjs +10 -11
  25. package/dist/core/config.d.mts +1 -1
  26. package/dist/core/config.mjs +2 -2
  27. package/dist/core/content-manager.d.mts +1 -1
  28. package/dist/core/content-manager.mjs +3 -3
  29. package/dist/core/context.d.mts +1 -1
  30. package/dist/core/context.mjs +5 -6
  31. package/dist/core/contracts/index.d.mts +1 -1
  32. package/dist/core/doctor.mjs +8 -8
  33. package/dist/core/graph-builder.mjs +3 -3
  34. package/dist/core/meta-manager.mjs +2 -2
  35. package/dist/core/model-manager.d.mts +1 -1
  36. package/dist/core/model-manager.mjs +3 -3
  37. package/dist/core/ops/index.d.mts +17 -29
  38. package/dist/core/ops/index.d.mts.map +1 -1
  39. package/dist/core/ops/index.mjs +9 -6
  40. package/dist/core/overlay-reader.d.mts +1 -1
  41. package/dist/core/overlay-reader.mjs +1 -1
  42. package/dist/core/scan-config.mjs +2 -2
  43. package/dist/core/scanner.mjs +4 -4
  44. package/dist/core/validator/index.d.mts +1 -1
  45. package/dist/core/validator/index.mjs +5 -5
  46. package/dist/{detect-B0oXbQaP.mjs → detect-CI32ynpq.mjs} +2 -2
  47. package/dist/{detect-B0oXbQaP.mjs.map → detect-CI32ynpq.mjs.map} +1 -1
  48. package/dist/{doctor-DNPuGgcg.mjs → doctor-B7G0oFxO.mjs} +7 -7
  49. package/dist/{doctor-DNPuGgcg.mjs.map → doctor-B7G0oFxO.mjs.map} +1 -1
  50. package/dist/errors-BmLE-JSC.mjs +55 -0
  51. package/dist/errors-BmLE-JSC.mjs.map +1 -0
  52. package/dist/{errors-CvBS12e7.mjs → errors-DLkAWnzQ.mjs} +1 -1
  53. package/dist/{errors-CvBS12e7.mjs.map → errors-DLkAWnzQ.mjs.map} +1 -1
  54. package/dist/{fs-DLbVB-Ek.mjs → fs-DJv5G8uL.mjs} +1 -1
  55. package/dist/{fs-DLbVB-Ek.mjs.map → fs-DJv5G8uL.mjs.map} +1 -1
  56. package/dist/git/branch-lifecycle.d.mts +2 -2
  57. package/dist/git/branch-lifecycle.mjs +5 -5
  58. package/dist/git/errors.d.mts +30 -0
  59. package/dist/git/errors.d.mts.map +1 -0
  60. package/dist/git/errors.mjs +2 -0
  61. package/dist/git/identity.mjs +1 -1
  62. package/dist/git/reconcile.d.mts +49 -0
  63. package/dist/git/reconcile.d.mts.map +1 -0
  64. package/dist/git/reconcile.mjs +13 -0
  65. package/dist/git/transaction.d.mts +60 -15
  66. package/dist/git/transaction.d.mts.map +1 -1
  67. package/dist/git/transaction.mjs +10 -11
  68. package/dist/{graph-builder-BEocwW13.mjs → graph-builder-C9NS7y-a.mjs} +3 -3
  69. package/dist/{graph-builder-BEocwW13.mjs.map → graph-builder-C9NS7y-a.mjs.map} +1 -1
  70. package/dist/{id-BjhaIxTL.mjs → id-CjGMPP1k.mjs} +2 -2
  71. package/dist/{id-BjhaIxTL.mjs.map → id-CjGMPP1k.mjs.map} +1 -1
  72. package/dist/{identity-D8QoGxhT.mjs → identity-CxAB9Qb8.mjs} +1 -1
  73. package/dist/{identity-D8QoGxhT.mjs.map → identity-CxAB9Qb8.mjs.map} +1 -1
  74. package/dist/index-_HJVkDJa.d.mts +85 -0
  75. package/dist/index-_HJVkDJa.d.mts.map +1 -0
  76. package/dist/index.mjs +24 -22
  77. package/dist/index.mjs.map +1 -1
  78. package/dist/{local-Db5X9mjZ.mjs → local-bnn6yrG7.mjs} +83 -8
  79. package/dist/local-bnn6yrG7.mjs.map +1 -0
  80. package/dist/{meta-manager-B_Se183X.mjs → meta-manager-g3shoZES.mjs} +2 -2
  81. package/dist/{meta-manager-B_Se183X.mjs.map → meta-manager-g3shoZES.mjs.map} +1 -1
  82. package/dist/{model-manager-DgyKMjLN.mjs → model-manager-DqwL8ptm.mjs} +66 -5
  83. package/dist/model-manager-DqwL8ptm.mjs.map +1 -0
  84. package/dist/{node-DUGiIkv1.mjs → node-CpLm3qr6.mjs} +1 -1
  85. package/dist/{node-DUGiIkv1.mjs.map → node-CpLm3qr6.mjs.map} +1 -1
  86. package/dist/{ops-BzbThweu.mjs → ops-BfrdZ6GK.mjs} +24 -34
  87. package/dist/ops-BfrdZ6GK.mjs.map +1 -0
  88. package/dist/{overlay-reader-CkU_pGXZ.mjs → overlay-reader-Bx1YxGXq.mjs} +1 -1
  89. package/dist/{overlay-reader-CkU_pGXZ.mjs.map → overlay-reader-Bx1YxGXq.mjs.map} +1 -1
  90. package/dist/{paths-CdPPWpbG.mjs → paths-C6_C1gLp.mjs} +1 -1
  91. package/dist/{paths-CdPPWpbG.mjs.map → paths-C6_C1gLp.mjs.map} +1 -1
  92. package/dist/providers/github/index.d.mts +10 -1
  93. package/dist/providers/github/index.d.mts.map +1 -1
  94. package/dist/providers/github/index.mjs +104 -4
  95. package/dist/providers/github/index.mjs.map +1 -1
  96. package/dist/providers/gitlab/index.d.mts +8 -1
  97. package/dist/providers/gitlab/index.d.mts.map +1 -1
  98. package/dist/providers/gitlab/index.mjs +15 -2
  99. package/dist/providers/gitlab/index.mjs.map +1 -1
  100. package/dist/providers/local/index.d.mts +26 -2
  101. package/dist/providers/local/index.d.mts.map +1 -1
  102. package/dist/providers/local/index.mjs +13 -12
  103. package/dist/reconcile-D5FTVW5z.mjs +1704 -0
  104. package/dist/reconcile-D5FTVW5z.mjs.map +1 -0
  105. package/dist/reconcile-M7KBZtdn.mjs +249 -0
  106. package/dist/reconcile-M7KBZtdn.mjs.map +1 -0
  107. package/dist/{scan-config-Dx49nCqk.mjs → scan-config-C5zzygeH.mjs} +2 -2
  108. package/dist/{scan-config-Dx49nCqk.mjs.map → scan-config-C5zzygeH.mjs.map} +1 -1
  109. package/dist/{scanner-BSvXjfkp.mjs → scanner-CEiT8Tgx.mjs} +7 -7
  110. package/dist/{scanner-BSvXjfkp.mjs.map → scanner-CEiT8Tgx.mjs.map} +1 -1
  111. package/dist/server/http/index.d.mts +1 -1
  112. package/dist/server/http/index.mjs +24 -22
  113. package/dist/server/http/index.mjs.map +1 -1
  114. package/dist/{server-DdxWpNBH.mjs → server-BtzVY7yp.mjs} +343 -227
  115. package/dist/server-BtzVY7yp.mjs.map +1 -0
  116. package/dist/{server-CLPNnhYL.d.mts → server-SDsD0XGM.d.mts} +2 -2
  117. package/dist/{server-CLPNnhYL.d.mts.map → server-SDsD0XGM.d.mts.map} +1 -1
  118. package/dist/server.d.mts +1 -1
  119. package/dist/server.mjs +24 -22
  120. package/dist/{source-map-generator-DhjiQdV3.mjs → source-map-generator-Clb2Eqse.mjs} +1 -1
  121. package/dist/{source-map-generator-DhjiQdV3.mjs.map → source-map-generator-Clb2Eqse.mjs.map} +1 -1
  122. package/dist/{svelte-parser-CwySu9yn.mjs → svelte-parser-CBN7Jxb7.mjs} +3 -3
  123. package/dist/{svelte-parser-CwySu9yn.mjs.map → svelte-parser-CBN7Jxb7.mjs.map} +1 -1
  124. package/dist/templates/index.mjs +1 -1
  125. package/dist/{templates-CAkTIRkE.mjs → templates-BZbohJc8.mjs} +1 -1
  126. package/dist/{templates-CAkTIRkE.mjs.map → templates-BZbohJc8.mjs.map} +1 -1
  127. package/dist/testing/memory-provider.d.mts +26 -1
  128. package/dist/testing/memory-provider.d.mts.map +1 -1
  129. package/dist/testing/memory-provider.mjs +62 -2
  130. package/dist/testing/memory-provider.mjs.map +1 -1
  131. package/dist/tools/annotations.d.mts.map +1 -1
  132. package/dist/tools/annotations.mjs +1 -1
  133. package/dist/tools/availability.d.mts.map +1 -1
  134. package/dist/tools/availability.mjs +1 -1
  135. package/dist/{transaction-BW9ektWu.mjs → transaction-BxDiiakv.mjs} +128 -64
  136. package/dist/transaction-BxDiiakv.mjs.map +1 -0
  137. package/dist/tsx-parser-DOQpJVgK.mjs +2 -0
  138. package/dist/{tsx-parser-BvsK6lyG.mjs → tsx-parser-go5V9zoc.mjs} +1 -1
  139. package/dist/{tsx-parser-BvsK6lyG.mjs.map → tsx-parser-go5V9zoc.mjs.map} +1 -1
  140. package/dist/util/detect.mjs +2 -2
  141. package/dist/util/fs.mjs +1 -1
  142. package/dist/util/id.mjs +1 -1
  143. package/dist/{validator-0sJ9vh1j.mjs → validator-B2GhPM3L.mjs} +5 -6
  144. package/dist/{validator-0sJ9vh1j.mjs.map → validator-B2GhPM3L.mjs.map} +1 -1
  145. package/dist/{vue-parser-auedZiJG.mjs → vue-parser-5Mp8w-mb.mjs} +3 -3
  146. package/dist/{vue-parser-auedZiJG.mjs.map → vue-parser-5Mp8w-mb.mjs.map} +1 -1
  147. package/package.json +13 -5
  148. package/dist/annotations-C-tQVmot.mjs.map +0 -1
  149. package/dist/availability-Dgcmvv99.mjs.map +0 -1
  150. package/dist/branch-lifecycle-B6PXZ2N7.mjs.map +0 -1
  151. package/dist/branch-lifecycle-DhzHfzs3.d.mts.map +0 -1
  152. package/dist/context-C3x6I3cE.mjs.map +0 -1
  153. package/dist/local-Db5X9mjZ.mjs.map +0 -1
  154. package/dist/model-manager-DgyKMjLN.mjs.map +0 -1
  155. package/dist/ops-BzbThweu.mjs.map +0 -1
  156. package/dist/paths-CmVw5Cw2.mjs +0 -65
  157. package/dist/paths-CmVw5Cw2.mjs.map +0 -1
  158. package/dist/serialization-tmgfFBOh.mjs +0 -2
  159. package/dist/server-DdxWpNBH.mjs.map +0 -1
  160. package/dist/transaction-BW9ektWu.mjs.map +0 -1
  161. package/dist/tsx-parser-Byc7HlJ8.mjs +0 -2
  162. /package/dist/{index-BWbTJlXm.d.mts → index-ByrVsRJE.d.mts} +0 -0
@@ -1,31 +1,31 @@
1
- import { a as planContentSave, i as planModelDelete, n as planVocabularySave, o as planContentDelete, r as planModelSave, t as planVocabularyDelete } from "./ops-BzbThweu.mjs";
2
- import { n as contentFilePath, t as contentDirPath } from "./paths-CmVw5Cw2.mjs";
3
- import { a as readJson, n as ensureDir, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-DLbVB-Ek.mjs";
4
- import { a as writeMeta, i as readMeta, o as writeMetaEntries, r as mergeEntryMeta, t as applyStatusChange } from "./meta-manager-B_Se183X.mjs";
5
- import { r as createGit } from "./identity-D8QoGxhT.mjs";
6
- import { n as readVocabulary, t as readConfig } from "./config-VlRKRwAS.mjs";
7
- import { a as deleteRemoteBranch, c as pruneMergedRemoteBranches, i as cleanupMergedBranches, n as checkBranchHealth, s as listRemoteCrBranches } from "./branch-lifecycle-B6PXZ2N7.mjs";
8
- import { T as writeContent, _ as resolveContentDir, b as resolveMdFilePath, c as listModels, d as validateModelDefinition, f as writeModel, i as countEntries, l as readModel, m as listContent, o as fieldDefZodSchema, p as deleteContent, r as checkReferences, v as resolveJsonFilePath, y as resolveLocaleStrategy } from "./model-manager-DgyKMjLN.mjs";
9
- import { n as readContext } from "./context-C3x6I3cE.mjs";
10
- import { i as mergeBranch, n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "./transaction-BW9ektWu.mjs";
11
- import { n as isMerged, t as LocalProvider } from "./local-Db5X9mjZ.mjs";
12
- import { n as isToolAvailable, t as TOOL_REQUIREMENTS } from "./availability-Dgcmvv99.mjs";
13
- import { t as detectStack } from "./detect-B0oXbQaP.mjs";
14
- import { t as TOOL_ANNOTATIONS } from "./annotations-C-tQVmot.mjs";
15
- import { n as listTemplates, t as getTemplate } from "./templates-CAkTIRkE.mjs";
16
- import { n as mapProviderError } from "./errors-CvBS12e7.mjs";
17
- import { t as validateProject } from "./validator-0sJ9vh1j.mjs";
18
- import { t as OverlayReader } from "./overlay-reader-CkU_pGXZ.mjs";
19
- import { t as buildGraph } from "./graph-builder-BEocwW13.mjs";
20
- import { n as scanSummary, t as scanCandidates } from "./scanner-BSvXjfkp.mjs";
21
- import { n as applyExtract, r as applyReuse } from "./apply-manager-BybvnUVC.mjs";
22
- import { t as runDoctor } from "./doctor-DNPuGgcg.mjs";
1
+ import { n as readContext } from "./context-CxpTATh4.mjs";
2
+ import { D as contentDirPath, O as contentFilePath, T as writeContent, _ as resolveContentDir, b as resolveMdFilePath, c as listModels, d as validateModelDefinition, f as writeModel, i as countEntries, l as readModel, m as listContent, o as fieldDefZodSchema, p as deleteContent, r as checkReferences, v as resolveJsonFilePath, y as resolveLocaleStrategy } from "./model-manager-DqwL8ptm.mjs";
3
+ import { a as planModelDelete, i as planModelSave, n as planVocabularySave, o as planContentSave, s as planContentDelete, t as planVocabularyDelete } from "./ops-BfrdZ6GK.mjs";
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-g3shoZES.mjs";
6
+ import { t as OverlayReader } from "./overlay-reader-Bx1YxGXq.mjs";
7
+ import { n as readVocabulary, t as readConfig } from "./config-B8a4JBT1.mjs";
8
+ import { t as validateProject } from "./validator-B2GhPM3L.mjs";
9
+ import { r as createGit } from "./identity-CxAB9Qb8.mjs";
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-BxDiiakv.mjs";
12
+ import { n as isMerged, t as LocalProvider } from "./local-bnn6yrG7.mjs";
13
+ import { n as isToolAvailable, t as TOOL_REQUIREMENTS } from "./availability-DYHNoP0_.mjs";
14
+ import { t as detectStack } from "./detect-CI32ynpq.mjs";
15
+ import { t as TOOL_ANNOTATIONS } from "./annotations-CXHPnS-M.mjs";
16
+ import { n as listTemplates, t as getTemplate } from "./templates-BZbohJc8.mjs";
17
+ import { t as normalizeOperationError } from "./errors-BmLE-JSC.mjs";
18
+ import { t as reconcileBranches } from "./reconcile-M7KBZtdn.mjs";
19
+ import { t as buildGraph } from "./graph-builder-C9NS7y-a.mjs";
20
+ import { n as scanSummary, t as scanCandidates } from "./scanner-CEiT8Tgx.mjs";
21
+ import { n as applyExtract, r as applyReuse } from "./apply-manager-DsNq7Zhm.mjs";
22
+ import { t as runDoctor } from "./doctor-B7G0oFxO.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";
26
26
  import { appendFile, readFile, writeFile } from "node:fs/promises";
27
- import "simple-git";
28
27
  import { z } from "zod";
28
+ import "simple-git";
29
29
  //#region src/tools/context.ts
30
30
  function registerContextTools(server, provider, projectRoot) {
31
31
  server.tool("contentrain_status", "Get full project status (read-only). Returns config, models, context. Do NOT manually edit .contentrain/ based on this output.", {}, TOOL_ANNOTATIONS["contentrain_status"], async () => {
@@ -78,18 +78,35 @@ function registerContextTools(server, provider, projectRoot) {
78
78
  } : { size: 0 }
79
79
  };
80
80
  if (projectRoot) {
81
- if (await pathExists(join(projectRoot, ".git"))) try {
82
- const cleanup = await cleanupMergedBranches(projectRoot);
83
- const health = await checkBranchHealth(projectRoot);
84
- result["branches"] = {
85
- total: health.total,
86
- merged: health.merged,
87
- unmerged: health.unmerged,
88
- cleaned_up: cleanup.deleted
89
- };
90
- if (health.message) result["branch_warning"] = health.message;
91
- if (health.blocked) errors.push(health.message);
92
- } catch {}
81
+ if (await pathExists(join(projectRoot, ".git"))) {
82
+ try {
83
+ const cleanup = await cleanupMergedBranches(projectRoot);
84
+ const health = await checkBranchHealth(projectRoot);
85
+ result["branches"] = {
86
+ total: health.total,
87
+ merged: health.merged,
88
+ unmerged: health.unmerged,
89
+ cleaned_up: cleanup.deleted
90
+ };
91
+ if (health.message) result["branch_warning"] = health.message;
92
+ if (health.blocked) errors.push(health.message);
93
+ } catch {}
94
+ try {
95
+ const git = createGit(projectRoot);
96
+ const branches = await git.branchLocal();
97
+ if (branches.all.includes(CONTENTRAIN_BRANCH)) {
98
+ const baseBranch = config?.repository?.default_branch ?? (branches.current !== CONTENTRAIN_BRANCH ? branches.current : "main");
99
+ const relation = await contentBranchRelation(git, baseBranch);
100
+ result["content_branch"] = {
101
+ base: baseBranch,
102
+ ahead: relation.ahead,
103
+ behind: relation.behind,
104
+ relation: relation.relation
105
+ };
106
+ if (relation.relation === "diverged") result["content_branch_warning"] = `"${CONTENTRAIN_BRANCH}" and "${baseBranch}" have diverged (${relation.ahead} ahead / ${relation.behind} behind). Writes still land on "${CONTENTRAIN_BRANCH}", but "${baseBranch}" cannot fast-forward until the branches are reconciled.`;
107
+ }
108
+ } catch {}
109
+ }
93
110
  }
94
111
  if (errors.length > 0) result["validation"] = {
95
112
  errors: errors.length,
@@ -411,57 +428,6 @@ function generateImportSnippet(model, stack, locale) {
411
428
  return snippets;
412
429
  }
413
430
  //#endregion
414
- //#region src/git/errors.ts
415
- /**
416
- * Error normalization for git-backed MCP operations.
417
- *
418
- * `simple-git` surfaces a failed `git commit`/`push`/`merge` as a `GitError`
419
- * whose `message` is the raw subprocess stderr — which, for hook tooling
420
- * (commitlint, husky, lefthook), is colorized multi-line text. Returning that
421
- * verbatim inside a JSON error produces an unreadable ANSI-escaped blob.
422
- *
423
- * This helper strips ANSI, preserves any structured fields the thrown error
424
- * already carries (`code`/`agent_hint`/`developer_action`), and detects git
425
- * hook rejections so the agent gets an actionable, structured envelope.
426
- */
427
- const ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
428
- function stripAnsi(input) {
429
- return input.replace(ANSI_RE, "");
430
- }
431
- /**
432
- * Normalize any thrown error into a structured, ANSI-free envelope for MCP
433
- * tool responses. `stage` labels where it happened (e.g. 'content_save').
434
- */
435
- function normalizeOperationError(err, stage) {
436
- const e = err;
437
- const message = stripAnsi(e?.message ?? String(err)).trim();
438
- const out = { error: message };
439
- if (stage) out.stage = stage;
440
- if (e?.code) out.code = e.code;
441
- if (e?.agent_hint) out.agent_hint = e.agent_hint;
442
- if (e?.developer_action) out.developer_action = e.developer_action;
443
- if (!out.code) {
444
- const mapped = mapProviderError(err);
445
- if (mapped) {
446
- out.error = mapped.error;
447
- out.code = mapped.code;
448
- out.agent_hint = mapped.agent_hint;
449
- if (mapped.developer_action) out.developer_action = mapped.developer_action;
450
- return out;
451
- }
452
- }
453
- if (!out.code) {
454
- const hookName = /\b(commit-msg|pre-commit|prepare-commit-msg|pre-push)\b/i.exec(message)?.[1];
455
- const hookTool = /\b(commitlint|husky|lefthook)\b/i.exec(message)?.[1];
456
- if (hookName || hookTool) {
457
- out.stage = out.stage ?? "git-commit";
458
- out.hook = hookName ?? hookTool;
459
- out.agent_hint = out.agent_hint ?? "A git hook rejected the Contentrain commit. Contentrain commits with --no-verify, but a wrapping CLI or server-side hook may still run. Ask the developer to allow machine-generated \"[contentrain]\" commits or to exclude cr/* branches from the hook.";
460
- }
461
- }
462
- return out;
463
- }
464
- //#endregion
465
431
  //#region src/tools/guards.ts
466
432
  /**
467
433
  * Emit a uniform "capability not available" response for tools that
@@ -484,6 +450,104 @@ function capabilityError(tool, capability) {
484
450
  };
485
451
  }
486
452
  //#endregion
453
+ //#region src/tools/commit-plan.ts
454
+ /**
455
+ * Context payload describing the operation, threaded into the local
456
+ * transaction so it can regenerate `.contentrain/context.json` on the
457
+ * contentrain branch after merge. Kept as a loose object so tool-specific
458
+ * payloads can add optional fields (`locale`, `entries`) without churning
459
+ * the helper's signature. Remote providers ignore it — see
460
+ * `commitThroughProvider` below.
461
+ */
462
+ const COMMIT_AUTHOR = {
463
+ name: "Contentrain",
464
+ email: "ai@contentrain.io"
465
+ };
466
+ /**
467
+ * The `git` object every write tool reports. One producer instead of nine
468
+ * hand-rolled literals, so `base_advance`/`remote_push` cannot drift between
469
+ * tools. Both fields appear only when the provider reported them (local
470
+ * auto-merge) — remote/review responses keep their previous shape.
471
+ */
472
+ function gitReport(input) {
473
+ return {
474
+ branch: input.branch,
475
+ action: input.action,
476
+ commit: input.commit,
477
+ ...input.sync ? { sync: input.sync } : {},
478
+ ...input.base_advance ? { base_advance: input.base_advance } : {},
479
+ ...input.remote_push ? { remote_push: input.remote_push } : {}
480
+ };
481
+ }
482
+ /**
483
+ * Next-step guidance for a partial success — empty when the base advanced
484
+ * and the push landed.
485
+ */
486
+ function divergenceNextSteps(input) {
487
+ const steps = [];
488
+ if (input.base_advance === "blocked_diverged") steps.push("DIVERGED: the content is safe on the contentrain branch, but the base branch has commits contentrain does not — run contentrain_reconcile (dry_run first) to merge them and restore the fast-forward advance");
489
+ if (input.remote_push === "rejected") steps.push("PUSH REJECTED: the remote refused the contentrain push even after a retry — the local and remote contentrain branches have diverged; fetch and run contentrain_reconcile");
490
+ return steps;
491
+ }
492
+ /**
493
+ * Commit a plan's changes through whichever provider is wired into the
494
+ * tool handler. Encapsulates the LocalProvider vs remote RepoProvider
495
+ * dispatch that every write-side tool (content_save, content_delete,
496
+ * model_save, model_delete) used to inline:
497
+ *
498
+ * - **localWorktree providers** — go through the worktree-backed transaction.
499
+ * `context` payload is threaded as an extra write-through and the
500
+ * transaction layer decides `auto-merged` vs `pending-review` based on
501
+ * the project's configured workflow. Selective-sync result is surfaced
502
+ * to the caller via `sync`.
503
+ *
504
+ * - **Any other RepoProvider** — only the plan's own changes are
505
+ * committed. Feature branches NEVER carry `.contentrain/context.json`:
506
+ * the file embeds timestamps, so two parallel cr/* branches forked from
507
+ * the same contentrain commit would always conflict on it and the
508
+ * second merge would fail permanently. This mirrors the local
509
+ * transaction flow, which regenerates context.json on the contentrain
510
+ * branch after merge (single-threaded, deterministic). Remote flows
511
+ * always report `pending-review`; Studio (or whatever orchestrator is
512
+ * driving the server) owns the merge AND the post-merge context
513
+ * regeneration on the contentrain branch (`buildContextChange` from
514
+ * `@contentrain/mcp/core/context` is exported for exactly that).
515
+ *
516
+ * The return shape is deliberately uniform so callers don't have to
517
+ * branch on provider type again.
518
+ */
519
+ async function commitThroughProvider(provider, input) {
520
+ const { branch, changes, message, contextPayload } = input;
521
+ if (provider.capabilities.localWorktree) {
522
+ const result = await provider.applyPlan({
523
+ branch,
524
+ changes,
525
+ message,
526
+ author: COMMIT_AUTHOR,
527
+ context: contextPayload
528
+ });
529
+ return {
530
+ commitSha: result.sha,
531
+ workflowAction: result.workflowAction ?? "pending-review",
532
+ sync: result.sync,
533
+ warning: result.warning,
534
+ base_advance: result.base_advance,
535
+ remote_push: result.remote_push
536
+ };
537
+ }
538
+ const allChanges = changes.toSorted((a, b) => a.path.localeCompare(b.path));
539
+ return {
540
+ commitSha: (await provider.applyPlan({
541
+ branch,
542
+ changes: allChanges,
543
+ message,
544
+ author: COMMIT_AUTHOR,
545
+ base: CONTENTRAIN_BRANCH
546
+ })).sha,
547
+ workflowAction: "pending-review"
548
+ };
549
+ }
550
+ //#endregion
487
551
  //#region src/tools/setup.ts
488
552
  function registerSetupTools(server, _provider, projectRoot) {
489
553
  server.tool("contentrain_init", "Initialize .contentrain/ structure. Changes are auto-committed to git — do NOT manually create .contentrain/ files.", {
@@ -602,12 +666,15 @@ function registerSetupTools(server, _provider, projectRoot) {
602
666
  ".contentrain/meta/"
603
667
  ],
604
668
  gitignore_updated: true,
605
- git: {
669
+ git: gitReport({
606
670
  branch,
607
671
  action: gitResult.action,
608
672
  commit: gitResult.commit,
609
- ...gitResult.sync ? { sync: gitResult.sync } : {}
610
- },
673
+ sync: gitResult.sync,
674
+ base_advance: gitResult.base_advance,
675
+ remote_push: gitResult.remote_push
676
+ }),
677
+ ...gitResult.warning ? { warning: gitResult.warning } : {},
611
678
  next_steps: ["Create models with contentrain_model_save or contentrain_scaffold", "Use contentrain_scan to find hardcoded strings"]
612
679
  }, null, 2)
613
680
  }] };
@@ -706,12 +773,15 @@ function registerSetupTools(server, _provider, projectRoot) {
706
773
  models_created: modelsCreated,
707
774
  content_created: contentCreated,
708
775
  vocabulary_terms_added: vocabAdded,
709
- git: {
776
+ git: gitReport({
710
777
  branch,
711
778
  action: gitResult.action,
712
779
  commit: gitResult.commit,
713
- ...gitResult.sync ? { sync: gitResult.sync } : {}
714
- },
780
+ sync: gitResult.sync,
781
+ base_advance: gitResult.base_advance,
782
+ remote_push: gitResult.remote_push
783
+ }),
784
+ ...gitResult.warning ? { warning: gitResult.warning } : {},
715
785
  context_updated: true,
716
786
  next_steps: [
717
787
  "Customize models with contentrain_model_save",
@@ -799,75 +869,6 @@ async function mergeVocabulary(projectRoot, newTerms, _locales) {
799
869
  return added;
800
870
  }
801
871
  //#endregion
802
- //#region src/tools/commit-plan.ts
803
- /**
804
- * Context payload describing the operation, threaded into the local
805
- * transaction so it can regenerate `.contentrain/context.json` on the
806
- * contentrain branch after merge. Kept as a loose object so tool-specific
807
- * payloads can add optional fields (`locale`, `entries`) without churning
808
- * the helper's signature. Remote providers ignore it — see
809
- * `commitThroughProvider` below.
810
- */
811
- const COMMIT_AUTHOR = {
812
- name: "Contentrain",
813
- email: "ai@contentrain.io"
814
- };
815
- /**
816
- * Commit a plan's changes through whichever provider is wired into the
817
- * tool handler. Encapsulates the LocalProvider vs remote RepoProvider
818
- * dispatch that every write-side tool (content_save, content_delete,
819
- * model_save, model_delete) used to inline:
820
- *
821
- * - **localWorktree providers** — go through the worktree-backed transaction.
822
- * `context` payload is threaded as an extra write-through and the
823
- * transaction layer decides `auto-merged` vs `pending-review` based on
824
- * the project's configured workflow. Selective-sync result is surfaced
825
- * to the caller via `sync`.
826
- *
827
- * - **Any other RepoProvider** — only the plan's own changes are
828
- * committed. Feature branches NEVER carry `.contentrain/context.json`:
829
- * the file embeds timestamps, so two parallel cr/* branches forked from
830
- * the same contentrain commit would always conflict on it and the
831
- * second merge would fail permanently. This mirrors the local
832
- * transaction flow, which regenerates context.json on the contentrain
833
- * branch after merge (single-threaded, deterministic). Remote flows
834
- * always report `pending-review`; Studio (or whatever orchestrator is
835
- * driving the server) owns the merge AND the post-merge context
836
- * regeneration on the contentrain branch (`buildContextChange` from
837
- * `@contentrain/mcp/core/context` is exported for exactly that).
838
- *
839
- * The return shape is deliberately uniform so callers don't have to
840
- * branch on provider type again.
841
- */
842
- async function commitThroughProvider(provider, input) {
843
- const { branch, changes, message, contextPayload } = input;
844
- if (provider.capabilities.localWorktree) {
845
- const result = await provider.applyPlan({
846
- branch,
847
- changes,
848
- message,
849
- author: COMMIT_AUTHOR,
850
- context: contextPayload
851
- });
852
- return {
853
- commitSha: result.sha,
854
- workflowAction: result.workflowAction ?? "pending-review",
855
- sync: result.sync
856
- };
857
- }
858
- const allChanges = changes.toSorted((a, b) => a.path.localeCompare(b.path));
859
- return {
860
- commitSha: (await provider.applyPlan({
861
- branch,
862
- changes: allChanges,
863
- message,
864
- author: COMMIT_AUTHOR,
865
- base: CONTENTRAIN_BRANCH
866
- })).sha,
867
- workflowAction: "pending-review"
868
- };
869
- }
870
- //#endregion
871
872
  //#region src/tools/model.ts
872
873
  const fieldDefSchema = fieldDefZodSchema;
873
874
  function registerModelTools(server, provider, projectRoot) {
@@ -951,19 +952,14 @@ function registerModelTools(server, provider, projectRoot) {
951
952
  tool: "contentrain_model_save",
952
953
  model: input.id
953
954
  };
954
- let commitSha;
955
- let workflowAction;
956
- let sync;
955
+ let commit;
957
956
  try {
958
- const result = await commitThroughProvider(provider, {
957
+ commit = await commitThroughProvider(provider, {
959
958
  branch,
960
959
  changes: savePlan.changes,
961
960
  message,
962
961
  contextPayload
963
962
  });
964
- commitSha = result.commitSha;
965
- workflowAction = result.workflowAction;
966
- sync = result.sync;
967
963
  } catch (error) {
968
964
  return {
969
965
  content: [{
@@ -995,17 +991,24 @@ function registerModelTools(server, provider, projectRoot) {
995
991
  errors: []
996
992
  },
997
993
  ...schemaWarnings.length > 0 ? { schema_warnings: schemaWarnings } : {},
998
- git: {
994
+ git: gitReport({
999
995
  branch,
1000
- action: workflowAction,
1001
- commit: commitSha,
1002
- ...sync ? { sync } : {}
1003
- },
996
+ action: commit.workflowAction,
997
+ commit: commit.commitSha,
998
+ sync: commit.sync,
999
+ base_advance: commit.base_advance,
1000
+ remote_push: commit.remote_push
1001
+ }),
1002
+ ...commit.warning ? { warning: commit.warning } : {},
1004
1003
  context_updated: true,
1005
1004
  content_path: contentPath + "/",
1006
1005
  ...displayPath ? { example_file: displayPath } : {},
1007
1006
  ...importSnippet ? { import_snippet: importSnippet } : {},
1008
- next_steps: [...schemaWarnings.length > 0 ? ["REVIEW: see schema_warnings — some declared constraints are not enforced by MCP"] : [], "Add content with contentrain_content_save"]
1007
+ next_steps: [
1008
+ ...divergenceNextSteps(commit),
1009
+ ...schemaWarnings.length > 0 ? ["REVIEW: see schema_warnings — some declared constraints are not enforced by MCP"] : [],
1010
+ "Add content with contentrain_content_save"
1011
+ ]
1009
1012
  }, null, 2)
1010
1013
  }] };
1011
1014
  });
@@ -1057,33 +1060,32 @@ function registerModelTools(server, provider, projectRoot) {
1057
1060
  tool: "contentrain_model_delete",
1058
1061
  model: modelId
1059
1062
  };
1060
- let commitSha;
1061
- let workflowAction;
1062
- let sync;
1063
1063
  try {
1064
- const result = await commitThroughProvider(provider, {
1064
+ const commit = await commitThroughProvider(provider, {
1065
1065
  branch,
1066
1066
  changes: deletePlan.changes,
1067
1067
  message,
1068
1068
  contextPayload
1069
1069
  });
1070
- commitSha = result.commitSha;
1071
- workflowAction = result.workflowAction;
1072
- sync = result.sync;
1070
+ const nextSteps = divergenceNextSteps(commit);
1073
1071
  return { content: [{
1074
1072
  type: "text",
1075
1073
  text: JSON.stringify({
1076
1074
  status: "committed",
1077
1075
  message: "Model deleted and committed to git. Do NOT manually edit .contentrain/ files.",
1078
1076
  deleted: true,
1079
- git: {
1077
+ git: gitReport({
1080
1078
  branch,
1081
- action: workflowAction,
1082
- commit: commitSha,
1083
- ...sync ? { sync } : {}
1084
- },
1079
+ action: commit.workflowAction,
1080
+ commit: commit.commitSha,
1081
+ sync: commit.sync,
1082
+ base_advance: commit.base_advance,
1083
+ remote_push: commit.remote_push
1084
+ }),
1085
+ ...commit.warning ? { warning: commit.warning } : {},
1085
1086
  files_removed: deletePlan.result,
1086
- context_updated: true
1087
+ context_updated: true,
1088
+ ...nextSteps.length > 0 ? { next_steps: nextSteps } : {}
1087
1089
  }, null, 2)
1088
1090
  }] };
1089
1091
  } catch (error) {
@@ -1221,19 +1223,14 @@ function registerContentTools(server, provider, projectRoot) {
1221
1223
  locale: input.entries[0]?.locale,
1222
1224
  entries: entryIds
1223
1225
  };
1224
- let commitSha;
1225
- let workflowAction;
1226
- let sync;
1226
+ let commit;
1227
1227
  try {
1228
- const result = await commitThroughProvider(provider, {
1228
+ commit = await commitThroughProvider(provider, {
1229
1229
  branch,
1230
1230
  changes: plan.changes,
1231
1231
  message,
1232
1232
  contextPayload
1233
1233
  });
1234
- commitSha = result.commitSha;
1235
- workflowAction = result.workflowAction;
1236
- sync = result.sync;
1237
1234
  } catch (error) {
1238
1235
  return {
1239
1236
  content: [{
@@ -1251,12 +1248,15 @@ function registerContentTools(server, provider, projectRoot) {
1251
1248
  status: "committed",
1252
1249
  message: "Content saved and committed to git. Do NOT manually edit .contentrain/ files.",
1253
1250
  results: plan.result,
1254
- git: {
1251
+ git: gitReport({
1255
1252
  branch,
1256
- action: workflowAction,
1257
- commit: commitSha,
1258
- ...sync ? { sync } : {}
1259
- },
1253
+ action: commit.workflowAction,
1254
+ commit: commit.commitSha,
1255
+ sync: commit.sync,
1256
+ base_advance: commit.base_advance,
1257
+ remote_push: commit.remote_push
1258
+ }),
1259
+ ...commit.warning ? { warning: commit.warning } : {},
1260
1260
  ...allAdvisories.length > 0 ? {
1261
1261
  advisories: allAdvisories,
1262
1262
  advisory_note: "Save succeeded. Review these warnings and consider consolidating duplicate values."
@@ -1268,6 +1268,7 @@ function registerContentTools(server, provider, projectRoot) {
1268
1268
  },
1269
1269
  context_updated: true,
1270
1270
  next_steps: [
1271
+ ...divergenceNextSteps(commit),
1271
1272
  ...allAdvisories.length > 0 ? ["ADVISORY: Duplicate values detected — review advisories above"] : [],
1272
1273
  ...warnings.length > 0 ? ["REVIEW: see warnings above — the save was not blocked by them"] : [],
1273
1274
  ...model.kind === "collection" ? ["Use contentrain_content_list to verify", "Add more entries or publish"] : ["Use contentrain_content_list to verify"]
@@ -1337,19 +1338,14 @@ function registerContentTools(server, provider, projectRoot) {
1337
1338
  model: input.model,
1338
1339
  locale: input.locale
1339
1340
  };
1340
- let commitSha;
1341
- let workflowAction;
1342
- let sync;
1343
1341
  try {
1344
- const result = await commitThroughProvider(provider, {
1342
+ const commit = await commitThroughProvider(provider, {
1345
1343
  branch,
1346
1344
  changes: deletePlan.changes,
1347
1345
  message,
1348
1346
  contextPayload
1349
1347
  });
1350
- commitSha = result.commitSha;
1351
- workflowAction = result.workflowAction;
1352
- sync = result.sync;
1348
+ const nextSteps = divergenceNextSteps(commit);
1353
1349
  return { content: [{
1354
1350
  type: "text",
1355
1351
  text: JSON.stringify({
@@ -1357,13 +1353,17 @@ function registerContentTools(server, provider, projectRoot) {
1357
1353
  message: "Content deleted and committed to git. Do NOT manually edit .contentrain/ files.",
1358
1354
  deleted: true,
1359
1355
  files_removed: deletePlan.result,
1360
- git: {
1356
+ git: gitReport({
1361
1357
  branch,
1362
- action: workflowAction,
1363
- commit: commitSha,
1364
- ...sync ? { sync } : {}
1365
- },
1366
- context_updated: true
1358
+ action: commit.workflowAction,
1359
+ commit: commit.commitSha,
1360
+ sync: commit.sync,
1361
+ base_advance: commit.base_advance,
1362
+ remote_push: commit.remote_push
1363
+ }),
1364
+ ...commit.warning ? { warning: commit.warning } : {},
1365
+ context_updated: true,
1366
+ ...nextSteps.length > 0 ? { next_steps: nextSteps } : {}
1367
1367
  }, null, 2)
1368
1368
  }] };
1369
1369
  } catch (error) {
@@ -1523,6 +1523,9 @@ function registerVocabularyTools(server, provider) {
1523
1523
  total_terms: plan.result.total,
1524
1524
  commit: commit.commitSha,
1525
1525
  workflow_action: commit.workflowAction,
1526
+ ...commit.base_advance ? { base_advance: commit.base_advance } : {},
1527
+ ...commit.remote_push ? { remote_push: commit.remote_push } : {},
1528
+ ...commit.warning ? { warning: commit.warning } : {},
1526
1529
  ...plan.advisories.length > 0 ? { advisories: plan.advisories } : {}
1527
1530
  }, null, 2)
1528
1531
  }] };
@@ -1595,6 +1598,9 @@ function registerVocabularyTools(server, provider) {
1595
1598
  total_terms: plan.result.total,
1596
1599
  commit: commit.commitSha,
1597
1600
  workflow_action: commit.workflowAction,
1601
+ ...commit.base_advance ? { base_advance: commit.base_advance } : {},
1602
+ ...commit.remote_push ? { remote_push: commit.remote_push } : {},
1603
+ ...commit.warning ? { warning: commit.warning } : {},
1598
1604
  ...plan.advisories.length > 0 ? { advisories: plan.advisories } : {}
1599
1605
  }, null, 2)
1600
1606
  }] };
@@ -1697,7 +1703,7 @@ function registerWorkflowTools(server, provider, projectRoot) {
1697
1703
  model: input.model ?? "*"
1698
1704
  });
1699
1705
  const gitResult = await tx.complete();
1700
- const nextSteps = [];
1706
+ const nextSteps = [...divergenceNextSteps(gitResult)];
1701
1707
  if (result.summary.errors > 0) nextSteps.push("Fix remaining errors manually");
1702
1708
  if (result.summary.warnings > 0) nextSteps.push("Review warnings");
1703
1709
  nextSteps.push("Run contentrain_validate again to verify");
@@ -1707,12 +1713,15 @@ function registerWorkflowTools(server, provider, projectRoot) {
1707
1713
  status: "committed",
1708
1714
  message: `Validation complete. ${result.fixed} issue(s) auto-fixed and committed to git. Do NOT manually edit .contentrain/ files.`,
1709
1715
  ...result,
1710
- git: {
1716
+ git: gitReport({
1711
1717
  branch,
1712
1718
  action: gitResult.action,
1713
1719
  commit: gitResult.commit,
1714
- ...gitResult.sync ? { sync: gitResult.sync } : {}
1715
- },
1720
+ sync: gitResult.sync,
1721
+ base_advance: gitResult.base_advance,
1722
+ remote_push: gitResult.remote_push
1723
+ }),
1724
+ ...gitResult.warning ? { warning: gitResult.warning } : {},
1716
1725
  context_updated: true,
1717
1726
  next_steps: nextSteps
1718
1727
  }, null, 2)
@@ -1918,8 +1927,12 @@ function registerWorkflowTools(server, provider, projectRoot) {
1918
1927
  action: result.action,
1919
1928
  commit: result.commit,
1920
1929
  sync: result.sync,
1930
+ base_advance: result.base_advance,
1931
+ remote_push: result.remote_push,
1932
+ ...result.warning ? { warning: result.warning } : {},
1921
1933
  ...result.remote ? { remote: result.remote } : {},
1922
1934
  next_steps: [
1935
+ ...divergenceNextSteps(result),
1923
1936
  "Run `contentrain generate` (or `npx contentrain-query generate`) to update the SDK client",
1924
1937
  "Run contentrain_validate to verify content integrity",
1925
1938
  result.sync.skipped.length > 0 ? `${result.sync.skipped.length} file(s) skipped due to local changes — resolve manually` : "",
@@ -2060,6 +2073,96 @@ function registerWorkflowTools(server, provider, projectRoot) {
2060
2073
  });
2061
2074
  }
2062
2075
  //#endregion
2076
+ //#region src/tools/reconcile.ts
2077
+ const resolutionSchema = z.union([z.object({
2078
+ id: z.string().describe("Conflict id from a previous dry-run"),
2079
+ choose: z.enum(["ours", "theirs"]).describe("Take the contentrain side (ours) or the base-branch side (theirs)")
2080
+ }), z.object({
2081
+ id: z.string().describe("Conflict id from a previous dry-run"),
2082
+ value: z.unknown().describe("Hand-authored replacement value")
2083
+ })]);
2084
+ function registerReconcileTools(server, provider, projectRoot) {
2085
+ server.tool("contentrain_reconcile", "Reconcile a diverged contentrain ↔ base-branch pair with a content-aware three-way merge. DRY RUN (default, dry_run:true): reports what would merge and which conflicts need a decision — touches nothing. EXECUTE (dry_run:false): performs the merge as a two-parent commit on contentrain and fast-forwards the base branch. Everything one side changed merges mechanically (entry-, key-, term+locale-level); only the same item changed differently on both sides becomes a conflict. Answer conflicts by passing resolutions (from the dry-run ids) and running again — a resolution whose values changed since the dry-run is dropped and the conflict re-reported. Recommended workflow: always run dry_run first, review the summary and conflicts, then execute.", {
2086
+ dry_run: z.boolean().optional().default(true).describe("Defaults to preview mode (dry_run:true). Set dry_run:false to execute after reviewing the preview."),
2087
+ resolutions: z.array(resolutionSchema).optional().describe("Decisions for conflicts reported by a previous dry-run, matched by id")
2088
+ }, TOOL_ANNOTATIONS["contentrain_reconcile"], async (input) => {
2089
+ if (!provider.capabilities.localWorktree || !projectRoot) return capabilityError("contentrain_reconcile", "localWorktree");
2090
+ if (!await readConfig(provider)) return {
2091
+ content: [{
2092
+ type: "text",
2093
+ text: JSON.stringify({ error: "Project not initialized. Run contentrain_init first." })
2094
+ }],
2095
+ isError: true
2096
+ };
2097
+ try {
2098
+ const result = await reconcileBranches(projectRoot, {
2099
+ dryRun: input.dry_run,
2100
+ resolutions: input.resolutions,
2101
+ source: "mcp-local"
2102
+ });
2103
+ if (result.action === "in_sync") return { content: [{
2104
+ type: "text",
2105
+ text: JSON.stringify({
2106
+ status: "in_sync",
2107
+ message: `"${result.base}" is already contained in the contentrain branch — nothing to reconcile.`,
2108
+ base: result.base
2109
+ }, null, 2)
2110
+ }] };
2111
+ if (result.action === "preview") return { content: [{
2112
+ type: "text",
2113
+ text: JSON.stringify({
2114
+ status: "preview",
2115
+ dry_run: true,
2116
+ base: result.base,
2117
+ merge_base: result.merge_base,
2118
+ summary: result.plan.result,
2119
+ changes: result.plan.changes.map((c) => ({
2120
+ path: c.path,
2121
+ action: c.content === null ? "delete" : "write"
2122
+ })),
2123
+ conflicts: result.plan.conflicts,
2124
+ ...result.plan.advisories.length > 0 ? { advisories: result.plan.advisories } : {},
2125
+ next_steps: result.plan.conflicts.length > 0 ? [`${result.plan.conflicts.length} conflict(s) need a decision — collect resolutions ({ id, choose: 'ours'|'theirs' } or { id, value }) and call contentrain_reconcile again`, "Conflicts are content decisions: ask the editor/user, do not guess"] : ["Plan is clean — run contentrain_reconcile with dry_run:false to execute"]
2126
+ }, null, 2)
2127
+ }] };
2128
+ if (result.action === "conflicts") return { content: [{
2129
+ type: "text",
2130
+ text: JSON.stringify({
2131
+ status: "conflicts",
2132
+ message: `${result.plan.conflicts.length} conflict(s) remain — nothing was written.`,
2133
+ base: result.base,
2134
+ conflicts: result.plan.conflicts,
2135
+ ...result.plan.advisories.length > 0 ? { advisories: result.plan.advisories } : {},
2136
+ agent_hint: "Each conflict is a content decision. Ask the editor/user which side (or value) should win, then pass resolutions and run again."
2137
+ }, null, 2)
2138
+ }] };
2139
+ return { content: [{
2140
+ type: "text",
2141
+ text: JSON.stringify({
2142
+ status: "reconciled",
2143
+ message: `Merged "${result.base}" into the contentrain branch and fast-forwarded "${result.base}".`,
2144
+ base: result.base,
2145
+ commit: result.commit,
2146
+ base_advance: result.base_advance,
2147
+ ...result.remote_push ? { remote_push: result.remote_push } : {},
2148
+ ...result.sync ? { sync: result.sync } : {},
2149
+ summary: result.plan.result,
2150
+ ...result.plan.advisories.length > 0 ? { advisories: result.plan.advisories } : {},
2151
+ next_steps: ["Run contentrain_validate to verify content integrity", "Run `contentrain generate` to refresh the SDK client"]
2152
+ }, null, 2)
2153
+ }] };
2154
+ } catch (error) {
2155
+ return {
2156
+ content: [{
2157
+ type: "text",
2158
+ text: JSON.stringify(normalizeOperationError(error, "reconcile"), null, 2)
2159
+ }],
2160
+ isError: true
2161
+ };
2162
+ }
2163
+ });
2164
+ }
2165
+ //#endregion
2063
2166
  //#region src/tools/normalize.ts
2064
2167
  function registerNormalizeTools(server, provider, projectRoot) {
2065
2168
  server.tool("contentrain_scan", "Scan project source code for content strings. Three modes: \"graph\" builds import/component graph for project intelligence, \"candidates\" extracts string literals with pre-filtering and pagination, \"summary\" provides quick overview stats. Read-only — no changes to disk or git. MCP finds strings deterministically; the agent decides what is content. Recommended workflow: start with \"summary\" or \"graph\" for orientation, then paginate through \"candidates\" to evaluate strings.", {
@@ -2434,12 +2537,16 @@ function registerBulkTools(server, _provider, projectRoot) {
2434
2537
  operation: "copy_locale",
2435
2538
  message: `Copied ${copiedCount} entries from ${input.source_locale} to ${input.target_locale}.`,
2436
2539
  copied: copiedCount,
2437
- git: {
2540
+ git: gitReport({
2438
2541
  branch,
2439
2542
  action: gitResult.action,
2440
2543
  commit: gitResult.commit,
2441
- ...gitResult.sync ? { sync: gitResult.sync } : {}
2442
- },
2544
+ sync: gitResult.sync,
2545
+ base_advance: gitResult.base_advance,
2546
+ remote_push: gitResult.remote_push
2547
+ }),
2548
+ ...gitResult.warning ? { warning: gitResult.warning } : {},
2549
+ ...divergenceNextSteps(gitResult).length > 0 ? { next_steps: divergenceNextSteps(gitResult) } : {},
2443
2550
  context_updated: true
2444
2551
  }, null, 2)
2445
2552
  }] };
@@ -2560,12 +2667,16 @@ function registerBulkTools(server, _provider, projectRoot) {
2560
2667
  updated: updatedCount,
2561
2668
  updated_by_locale: updatedPerLocale,
2562
2669
  not_found: notFound.length > 0 ? notFound : void 0,
2563
- git: {
2670
+ git: gitReport({
2564
2671
  branch,
2565
2672
  action: gitResult.action,
2566
2673
  commit: gitResult.commit,
2567
- ...gitResult.sync ? { sync: gitResult.sync } : {}
2568
- },
2674
+ sync: gitResult.sync,
2675
+ base_advance: gitResult.base_advance,
2676
+ remote_push: gitResult.remote_push
2677
+ }),
2678
+ ...gitResult.warning ? { warning: gitResult.warning } : {},
2679
+ ...divergenceNextSteps(gitResult).length > 0 ? { next_steps: divergenceNextSteps(gitResult) } : {},
2569
2680
  context_updated: true
2570
2681
  }, null, 2)
2571
2682
  }] };
@@ -2631,12 +2742,16 @@ function registerBulkTools(server, _provider, projectRoot) {
2631
2742
  message: `Deleted ${input.entry_ids.length} entries.`,
2632
2743
  deleted: input.entry_ids.length,
2633
2744
  files_removed: allRemoved,
2634
- git: {
2745
+ git: gitReport({
2635
2746
  branch,
2636
2747
  action: gitResult.action,
2637
2748
  commit: gitResult.commit,
2638
- ...gitResult.sync ? { sync: gitResult.sync } : {}
2639
- },
2749
+ sync: gitResult.sync,
2750
+ base_advance: gitResult.base_advance,
2751
+ remote_push: gitResult.remote_push
2752
+ }),
2753
+ ...gitResult.warning ? { warning: gitResult.warning } : {},
2754
+ ...divergenceNextSteps(gitResult).length > 0 ? { next_steps: divergenceNextSteps(gitResult) } : {},
2640
2755
  context_updated: true
2641
2756
  }, null, 2)
2642
2757
  }] };
@@ -2806,7 +2921,7 @@ function registerMediaTools(server, provider, _projectRoot) {
2806
2921
  }
2807
2922
  //#endregion
2808
2923
  //#region package.json
2809
- var version = "3.0.2";
2924
+ var version = "3.1.0";
2810
2925
  //#endregion
2811
2926
  //#region src/server.ts
2812
2927
  /**
@@ -2845,6 +2960,7 @@ function createServer(input) {
2845
2960
  registerContentTools(server, provider, projectRoot);
2846
2961
  registerVocabularyTools(server, provider);
2847
2962
  registerWorkflowTools(server, provider, projectRoot);
2963
+ registerReconcileTools(server, provider, projectRoot);
2848
2964
  registerNormalizeTools(server, provider, projectRoot);
2849
2965
  registerBulkTools(server, provider, projectRoot);
2850
2966
  registerDoctorTools(server, provider, projectRoot);
@@ -2880,4 +2996,4 @@ function resolveServerContext(input) {
2880
2996
  //#endregion
2881
2997
  export { createServer as n, DEFAULT_INSTRUCTIONS as t };
2882
2998
 
2883
- //# sourceMappingURL=server-DdxWpNBH.mjs.map
2999
+ //# sourceMappingURL=server-BtzVY7yp.mjs.map