@contentrain/mcp 3.0.3 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/{annotations-C-tQVmot.mjs → annotations-CXHPnS-M.mjs} +8 -1
- package/dist/annotations-CXHPnS-M.mjs.map +1 -0
- package/dist/{apply-manager-BybvnUVC.mjs → apply-manager-DsNq7Zhm.mjs} +7 -7
- package/dist/{apply-manager-BybvnUVC.mjs.map → apply-manager-DsNq7Zhm.mjs.map} +1 -1
- package/dist/{astro-parser-Cb9uuBpW.mjs → astro-parser-dhFgc2wA.mjs} +3 -3
- package/dist/{astro-parser-Cb9uuBpW.mjs.map → astro-parser-dhFgc2wA.mjs.map} +1 -1
- package/dist/{availability-Dgcmvv99.mjs → availability-DYHNoP0_.mjs} +5 -1
- package/dist/availability-DYHNoP0_.mjs.map +1 -0
- package/dist/{branch-lifecycle-DhzHfzs3.d.mts → branch-lifecycle-DGlgjnC7.d.mts} +32 -2
- package/dist/branch-lifecycle-DGlgjnC7.d.mts.map +1 -0
- package/dist/{branch-lifecycle-B6PXZ2N7.mjs → branch-lifecycle-DUOoyNWn.mjs} +32 -4
- package/dist/branch-lifecycle-DUOoyNWn.mjs.map +1 -0
- package/dist/{compiler-B6hNQeCC.mjs → compiler-CR9F0wZZ.mjs} +2 -2
- package/dist/{compiler-B6hNQeCC.mjs.map → compiler-CR9F0wZZ.mjs.map} +1 -1
- package/dist/{compiler-sfc.cjs-8yY7b5bx.mjs → compiler-sfc.cjs-Cpexs92i.mjs} +2 -2
- package/dist/{compiler-sfc.cjs-8yY7b5bx.mjs.map → compiler-sfc.cjs-Cpexs92i.mjs.map} +1 -1
- package/dist/{config-VlRKRwAS.mjs → config-B8a4JBT1.mjs} +2 -2
- package/dist/{config-VlRKRwAS.mjs.map → config-B8a4JBT1.mjs.map} +1 -1
- package/dist/{content-manager-Dn4tqDw1.d.mts → content-manager-Bx94DXSF.d.mts} +2 -2
- package/dist/{content-manager-Dn4tqDw1.d.mts.map → content-manager-Bx94DXSF.d.mts.map} +1 -1
- package/dist/{context-C3x6I3cE.mjs → context-CxpTATh4.mjs} +7 -7
- package/dist/context-CxpTATh4.mjs.map +1 -0
- package/dist/core/apply-manager.mjs +10 -11
- package/dist/core/config.d.mts +1 -1
- package/dist/core/config.mjs +2 -2
- package/dist/core/content-manager.d.mts +1 -1
- package/dist/core/content-manager.mjs +3 -3
- package/dist/core/context.d.mts +1 -1
- package/dist/core/context.mjs +5 -6
- package/dist/core/contracts/index.d.mts +1 -1
- package/dist/core/doctor.mjs +8 -8
- package/dist/core/graph-builder.mjs +3 -3
- package/dist/core/meta-manager.mjs +2 -2
- package/dist/core/model-manager.d.mts +1 -1
- package/dist/core/model-manager.mjs +3 -3
- package/dist/core/ops/index.d.mts +17 -29
- package/dist/core/ops/index.d.mts.map +1 -1
- package/dist/core/ops/index.mjs +9 -6
- package/dist/core/overlay-reader.d.mts +1 -1
- package/dist/core/overlay-reader.mjs +1 -1
- package/dist/core/scan-config.mjs +2 -2
- package/dist/core/scanner.mjs +4 -4
- package/dist/core/validator/index.d.mts +1 -1
- package/dist/core/validator/index.mjs +5 -5
- package/dist/{detect-B0oXbQaP.mjs → detect-CI32ynpq.mjs} +2 -2
- package/dist/{detect-B0oXbQaP.mjs.map → detect-CI32ynpq.mjs.map} +1 -1
- package/dist/{doctor-DNPuGgcg.mjs → doctor-B7G0oFxO.mjs} +7 -7
- package/dist/{doctor-DNPuGgcg.mjs.map → doctor-B7G0oFxO.mjs.map} +1 -1
- package/dist/errors-BmLE-JSC.mjs +55 -0
- package/dist/errors-BmLE-JSC.mjs.map +1 -0
- package/dist/{errors-CvBS12e7.mjs → errors-DLkAWnzQ.mjs} +1 -1
- package/dist/{errors-CvBS12e7.mjs.map → errors-DLkAWnzQ.mjs.map} +1 -1
- package/dist/{fs-DLbVB-Ek.mjs → fs-DJv5G8uL.mjs} +1 -1
- package/dist/{fs-DLbVB-Ek.mjs.map → fs-DJv5G8uL.mjs.map} +1 -1
- package/dist/git/branch-lifecycle.d.mts +2 -2
- package/dist/git/branch-lifecycle.mjs +5 -5
- package/dist/git/errors.d.mts +30 -0
- package/dist/git/errors.d.mts.map +1 -0
- package/dist/git/errors.mjs +2 -0
- package/dist/git/identity.mjs +1 -1
- package/dist/git/reconcile.d.mts +49 -0
- package/dist/git/reconcile.d.mts.map +1 -0
- package/dist/git/reconcile.mjs +13 -0
- package/dist/git/transaction.d.mts +60 -15
- package/dist/git/transaction.d.mts.map +1 -1
- package/dist/git/transaction.mjs +10 -11
- package/dist/{graph-builder-BEocwW13.mjs → graph-builder-C9NS7y-a.mjs} +3 -3
- package/dist/{graph-builder-BEocwW13.mjs.map → graph-builder-C9NS7y-a.mjs.map} +1 -1
- package/dist/{id-BjhaIxTL.mjs → id-CjGMPP1k.mjs} +2 -2
- package/dist/{id-BjhaIxTL.mjs.map → id-CjGMPP1k.mjs.map} +1 -1
- package/dist/{identity-D8QoGxhT.mjs → identity-CxAB9Qb8.mjs} +1 -1
- package/dist/{identity-D8QoGxhT.mjs.map → identity-CxAB9Qb8.mjs.map} +1 -1
- package/dist/index-_HJVkDJa.d.mts +85 -0
- package/dist/index-_HJVkDJa.d.mts.map +1 -0
- package/dist/index.mjs +24 -22
- package/dist/index.mjs.map +1 -1
- package/dist/{local-Db5X9mjZ.mjs → local-bnn6yrG7.mjs} +83 -8
- package/dist/local-bnn6yrG7.mjs.map +1 -0
- package/dist/{meta-manager-B_Se183X.mjs → meta-manager-g3shoZES.mjs} +2 -2
- package/dist/{meta-manager-B_Se183X.mjs.map → meta-manager-g3shoZES.mjs.map} +1 -1
- package/dist/{model-manager-DgyKMjLN.mjs → model-manager-DqwL8ptm.mjs} +66 -5
- package/dist/model-manager-DqwL8ptm.mjs.map +1 -0
- package/dist/{node-DUGiIkv1.mjs → node-CpLm3qr6.mjs} +1 -1
- package/dist/{node-DUGiIkv1.mjs.map → node-CpLm3qr6.mjs.map} +1 -1
- package/dist/{ops-BzbThweu.mjs → ops-BfrdZ6GK.mjs} +24 -34
- package/dist/ops-BfrdZ6GK.mjs.map +1 -0
- package/dist/{overlay-reader-CkU_pGXZ.mjs → overlay-reader-Bx1YxGXq.mjs} +1 -1
- package/dist/{overlay-reader-CkU_pGXZ.mjs.map → overlay-reader-Bx1YxGXq.mjs.map} +1 -1
- package/dist/{paths-CdPPWpbG.mjs → paths-C6_C1gLp.mjs} +1 -1
- package/dist/{paths-CdPPWpbG.mjs.map → paths-C6_C1gLp.mjs.map} +1 -1
- package/dist/providers/github/index.d.mts +10 -1
- package/dist/providers/github/index.d.mts.map +1 -1
- package/dist/providers/github/index.mjs +104 -4
- package/dist/providers/github/index.mjs.map +1 -1
- package/dist/providers/gitlab/index.d.mts +8 -1
- package/dist/providers/gitlab/index.d.mts.map +1 -1
- package/dist/providers/gitlab/index.mjs +15 -2
- package/dist/providers/gitlab/index.mjs.map +1 -1
- package/dist/providers/local/index.d.mts +26 -2
- package/dist/providers/local/index.d.mts.map +1 -1
- package/dist/providers/local/index.mjs +13 -12
- package/dist/reconcile-D5FTVW5z.mjs +1704 -0
- package/dist/reconcile-D5FTVW5z.mjs.map +1 -0
- package/dist/reconcile-M7KBZtdn.mjs +249 -0
- package/dist/reconcile-M7KBZtdn.mjs.map +1 -0
- package/dist/{scan-config-Dx49nCqk.mjs → scan-config-C5zzygeH.mjs} +2 -2
- package/dist/{scan-config-Dx49nCqk.mjs.map → scan-config-C5zzygeH.mjs.map} +1 -1
- package/dist/{scanner-BSvXjfkp.mjs → scanner-CEiT8Tgx.mjs} +7 -7
- package/dist/{scanner-BSvXjfkp.mjs.map → scanner-CEiT8Tgx.mjs.map} +1 -1
- package/dist/server/http/index.d.mts +1 -1
- package/dist/server/http/index.mjs +24 -22
- package/dist/server/http/index.mjs.map +1 -1
- package/dist/{server-B7QA0Q1M.mjs → server-B6abaEU2.mjs} +343 -227
- package/dist/server-B6abaEU2.mjs.map +1 -0
- package/dist/{server-CLPNnhYL.d.mts → server-SDsD0XGM.d.mts} +2 -2
- package/dist/{server-CLPNnhYL.d.mts.map → server-SDsD0XGM.d.mts.map} +1 -1
- package/dist/server.d.mts +1 -1
- package/dist/server.mjs +24 -22
- package/dist/{source-map-generator-DhjiQdV3.mjs → source-map-generator-Clb2Eqse.mjs} +1 -1
- package/dist/{source-map-generator-DhjiQdV3.mjs.map → source-map-generator-Clb2Eqse.mjs.map} +1 -1
- package/dist/{svelte-parser-CwySu9yn.mjs → svelte-parser-CBN7Jxb7.mjs} +3 -3
- package/dist/{svelte-parser-CwySu9yn.mjs.map → svelte-parser-CBN7Jxb7.mjs.map} +1 -1
- package/dist/templates/index.mjs +1 -1
- package/dist/{templates-CAkTIRkE.mjs → templates-BZbohJc8.mjs} +1 -1
- package/dist/{templates-CAkTIRkE.mjs.map → templates-BZbohJc8.mjs.map} +1 -1
- package/dist/testing/memory-provider.d.mts +26 -1
- package/dist/testing/memory-provider.d.mts.map +1 -1
- package/dist/testing/memory-provider.mjs +62 -2
- package/dist/testing/memory-provider.mjs.map +1 -1
- package/dist/tools/annotations.d.mts.map +1 -1
- package/dist/tools/annotations.mjs +1 -1
- package/dist/tools/availability.d.mts.map +1 -1
- package/dist/tools/availability.mjs +1 -1
- package/dist/{transaction-BW9ektWu.mjs → transaction-BxDiiakv.mjs} +128 -64
- package/dist/transaction-BxDiiakv.mjs.map +1 -0
- package/dist/tsx-parser-DOQpJVgK.mjs +2 -0
- package/dist/{tsx-parser-BvsK6lyG.mjs → tsx-parser-go5V9zoc.mjs} +1 -1
- package/dist/{tsx-parser-BvsK6lyG.mjs.map → tsx-parser-go5V9zoc.mjs.map} +1 -1
- package/dist/util/detect.mjs +2 -2
- package/dist/util/fs.mjs +1 -1
- package/dist/util/id.mjs +1 -1
- package/dist/{validator-0sJ9vh1j.mjs → validator-B2GhPM3L.mjs} +5 -6
- package/dist/{validator-0sJ9vh1j.mjs.map → validator-B2GhPM3L.mjs.map} +1 -1
- package/dist/{vue-parser-auedZiJG.mjs → vue-parser-5Mp8w-mb.mjs} +3 -3
- package/dist/{vue-parser-auedZiJG.mjs.map → vue-parser-5Mp8w-mb.mjs.map} +1 -1
- package/package.json +13 -5
- package/dist/annotations-C-tQVmot.mjs.map +0 -1
- package/dist/availability-Dgcmvv99.mjs.map +0 -1
- package/dist/branch-lifecycle-B6PXZ2N7.mjs.map +0 -1
- package/dist/branch-lifecycle-DhzHfzs3.d.mts.map +0 -1
- package/dist/context-C3x6I3cE.mjs.map +0 -1
- package/dist/local-Db5X9mjZ.mjs.map +0 -1
- package/dist/model-manager-DgyKMjLN.mjs.map +0 -1
- package/dist/ops-BzbThweu.mjs.map +0 -1
- package/dist/paths-CmVw5Cw2.mjs +0 -65
- package/dist/paths-CmVw5Cw2.mjs.map +0 -1
- package/dist/serialization-tmgfFBOh.mjs +0 -2
- package/dist/server-B7QA0Q1M.mjs.map +0 -1
- package/dist/transaction-BW9ektWu.mjs.map +0 -1
- package/dist/tsx-parser-Byc7HlJ8.mjs +0 -2
- /package/dist/{index-BWbTJlXm.d.mts → index-ByrVsRJE.d.mts} +0 -0
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { a as
|
|
4
|
-
import { a as
|
|
5
|
-
import { r as
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { i as
|
|
11
|
-
import { n as
|
|
12
|
-
import { n as
|
|
13
|
-
import { t as
|
|
14
|
-
import { t as
|
|
15
|
-
import {
|
|
16
|
-
import { n as
|
|
17
|
-
import { t as
|
|
18
|
-
import { t as
|
|
19
|
-
import { t as buildGraph } from "./graph-builder-
|
|
20
|
-
import { n as scanSummary, t as scanCandidates } from "./scanner-
|
|
21
|
-
import { n as applyExtract, r as applyReuse } from "./apply-manager-
|
|
22
|
-
import { t as runDoctor } from "./doctor-
|
|
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")))
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
955
|
-
let workflowAction;
|
|
956
|
-
let sync;
|
|
955
|
+
let commit;
|
|
957
956
|
try {
|
|
958
|
-
|
|
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
|
-
|
|
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: [
|
|
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
|
|
1064
|
+
const commit = await commitThroughProvider(provider, {
|
|
1065
1065
|
branch,
|
|
1066
1066
|
changes: deletePlan.changes,
|
|
1067
1067
|
message,
|
|
1068
1068
|
contextPayload
|
|
1069
1069
|
});
|
|
1070
|
-
|
|
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
|
-
|
|
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
|
|
1225
|
-
let workflowAction;
|
|
1226
|
-
let sync;
|
|
1226
|
+
let commit;
|
|
1227
1227
|
try {
|
|
1228
|
-
|
|
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
|
-
|
|
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
|
|
1342
|
+
const commit = await commitThroughProvider(provider, {
|
|
1345
1343
|
branch,
|
|
1346
1344
|
changes: deletePlan.changes,
|
|
1347
1345
|
message,
|
|
1348
1346
|
contextPayload
|
|
1349
1347
|
});
|
|
1350
|
-
|
|
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
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2924
|
+
var version = "3.1.1";
|
|
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-
|
|
2999
|
+
//# sourceMappingURL=server-B6abaEU2.mjs.map
|