@contentrain/mcp 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/{annotations-Clhbw2lh.mjs → annotations-BFJbfSKe.mjs} +13 -1
- package/dist/{annotations-Clhbw2lh.mjs.map → annotations-BFJbfSKe.mjs.map} +1 -1
- package/dist/{apply-manager-D5ym5Jba.mjs → apply-manager-CDss8ZIr.mjs} +5 -5
- package/dist/{apply-manager-D5ym5Jba.mjs.map → apply-manager-CDss8ZIr.mjs.map} +1 -1
- package/dist/{branch-lifecycle-CFVpTpNT.mjs → branch-lifecycle-DjX3MXNG.mjs} +10 -7
- package/dist/{branch-lifecycle-CFVpTpNT.mjs.map → branch-lifecycle-DjX3MXNG.mjs.map} +1 -1
- package/dist/{config-BIl5Ybj0.mjs → config-Lw7vpK9n.mjs} +4 -2
- package/dist/config-Lw7vpK9n.mjs.map +1 -0
- package/dist/{context-J0OU9Nfp.mjs → context-DS40WOdw.mjs} +2 -2
- package/dist/{context-J0OU9Nfp.mjs.map → context-DS40WOdw.mjs.map} +1 -1
- package/dist/core/apply-manager.mjs +5 -5
- package/dist/core/config.d.mts.map +1 -1
- package/dist/core/config.mjs +1 -1
- package/dist/core/context.mjs +2 -2
- package/dist/core/doctor.mjs +3 -3
- package/dist/core/validator/index.d.mts.map +1 -1
- package/dist/core/validator/index.mjs +2 -2
- package/dist/{doctor-CgBoT6h1.mjs → doctor-Dxu_SzZy.mjs} +3 -3
- package/dist/{doctor-CgBoT6h1.mjs.map → doctor-Dxu_SzZy.mjs.map} +1 -1
- package/dist/git/branch-lifecycle.d.mts.map +1 -1
- package/dist/git/branch-lifecycle.mjs +2 -2
- package/dist/git/transaction.d.mts.map +1 -1
- package/dist/git/transaction.mjs +3 -3
- package/dist/index.mjs +10 -10
- package/dist/{local-DvP4Y4T-.mjs → local-BEl5L7p5.mjs} +4 -4
- package/dist/{local-DvP4Y4T-.mjs.map → local-BEl5L7p5.mjs.map} +1 -1
- package/dist/providers/local/index.mjs +4 -4
- package/dist/server/http/index.mjs +10 -10
- package/dist/{server-BkhhtEE2.mjs → server-BiiVUjgs.mjs} +254 -48
- package/dist/server-BiiVUjgs.mjs.map +1 -0
- package/dist/server.mjs +10 -10
- package/dist/tools/annotations.d.mts.map +1 -1
- package/dist/tools/annotations.mjs +1 -1
- package/dist/{transaction-DgkNgWSI.mjs → transaction-0D_edyh0.mjs} +48 -5
- package/dist/transaction-0D_edyh0.mjs.map +1 -0
- package/dist/{validator-B0nNRFeU.mjs → validator-BX2ou7V-.mjs} +46 -17
- package/dist/validator-BX2ou7V-.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/config-BIl5Ybj0.mjs.map +0 -1
- package/dist/server-BkhhtEE2.mjs.map +0 -1
- package/dist/transaction-DgkNgWSI.mjs.map +0 -1
- package/dist/validator-B0nNRFeU.mjs.map +0 -1
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { i as planContentDelete, n as planModelDelete, r as planContentSave, t as planModelSave } from "./ops-DjEWi5Wa.mjs";
|
|
2
2
|
import { n as contentFilePath, t as contentDirPath } from "./paths-CYl5vCBD.mjs";
|
|
3
3
|
import { a as readJson, n as ensureDir, r as pathExists, s as writeJson, t as contentrainDir } from "./fs-DLbVB-Ek.mjs";
|
|
4
|
-
import { n as readVocabulary, t as readConfig } from "./config-
|
|
4
|
+
import { n as readVocabulary, t as readConfig } from "./config-Lw7vpK9n.mjs";
|
|
5
5
|
import { S as writeContent, _ as resolveMdFilePath, a as fieldDefZodSchema, c as validateModelDefinition, d as listContent, g as resolveLocaleStrategy, h as resolveJsonFilePath, l as writeModel, m as resolveContentDir, n as checkReferences, o as listModels, r as countEntries, s as readModel, u as deleteContent } from "./model-manager-BhLsUgaB.mjs";
|
|
6
6
|
import { n as readMeta, r as writeMeta } from "./meta-manager-C900rEyG.mjs";
|
|
7
|
-
import { n as readContext, t as buildContextChange } from "./context-
|
|
8
|
-
import { i as mergeBranch, n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "./transaction-
|
|
9
|
-
import { t as LocalProvider } from "./local-
|
|
7
|
+
import { n as readContext, t as buildContextChange } from "./context-DS40WOdw.mjs";
|
|
8
|
+
import { i as mergeBranch, n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "./transaction-0D_edyh0.mjs";
|
|
9
|
+
import { n as isMerged, t as LocalProvider } from "./local-BEl5L7p5.mjs";
|
|
10
10
|
import { t as detectStack } from "./detect-DL9mFmaS.mjs";
|
|
11
|
-
import { n as checkBranchHealth, r as cleanupMergedBranches } from "./branch-lifecycle-
|
|
12
|
-
import { t as TOOL_ANNOTATIONS } from "./annotations-
|
|
11
|
+
import { n as checkBranchHealth, r as cleanupMergedBranches } from "./branch-lifecycle-DjX3MXNG.mjs";
|
|
12
|
+
import { t as TOOL_ANNOTATIONS } from "./annotations-BFJbfSKe.mjs";
|
|
13
13
|
import { n as listTemplates, t as getTemplate } from "./templates-VrLVqfe5.mjs";
|
|
14
14
|
import { t as OverlayReader } from "./overlay-reader-DOdyDRIv.mjs";
|
|
15
|
-
import { t as validateProject } from "./validator-
|
|
15
|
+
import { t as validateProject } from "./validator-BX2ou7V-.mjs";
|
|
16
16
|
import { t as buildGraph } from "./graph-builder-DXv2JEp-.mjs";
|
|
17
17
|
import { n as scanSummary, t as scanCandidates } from "./scanner-DKMwc03k.mjs";
|
|
18
|
-
import { n as applyExtract, r as applyReuse } from "./apply-manager-
|
|
19
|
-
import { t as runDoctor } from "./doctor-
|
|
18
|
+
import { n as applyExtract, r as applyReuse } from "./apply-manager-CDss8ZIr.mjs";
|
|
19
|
+
import { t as runDoctor } from "./doctor-Dxu_SzZy.mjs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
22
22
|
import { CONTENTRAIN_BRANCH } from "@contentrain/types";
|
|
@@ -42,6 +42,17 @@ function registerContextTools(server, provider, projectRoot) {
|
|
|
42
42
|
const models = await listModels(provider);
|
|
43
43
|
const context = projectRoot ? await readContext(projectRoot) : await readContext(provider);
|
|
44
44
|
const vocabulary = await readVocabulary(provider);
|
|
45
|
+
let stats = context?.stats ?? null;
|
|
46
|
+
if (projectRoot) {
|
|
47
|
+
const fullModels = await Promise.all(models.map((m) => readModel(provider, m.id)));
|
|
48
|
+
const entryCounts = await Promise.all(fullModels.filter((m) => m !== null).map((m) => countEntries(provider, m)));
|
|
49
|
+
stats = {
|
|
50
|
+
models: models.length,
|
|
51
|
+
entries: entryCounts.reduce((acc, c) => acc + c.total, 0),
|
|
52
|
+
locales: config?.locales.supported ?? context?.stats?.locales ?? ["en"],
|
|
53
|
+
...context?.stats?.lastSync ? { lastSync: context.stats.lastSync } : {}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
45
56
|
const errors = [];
|
|
46
57
|
if (!config) errors.push(".contentrain/config.json missing");
|
|
47
58
|
const result = {
|
|
@@ -54,9 +65,9 @@ function registerContextTools(server, provider, projectRoot) {
|
|
|
54
65
|
repository: config.repository
|
|
55
66
|
} : null,
|
|
56
67
|
models,
|
|
57
|
-
context: context ? {
|
|
58
|
-
lastOperation: context
|
|
59
|
-
stats
|
|
68
|
+
context: context || stats ? {
|
|
69
|
+
lastOperation: context?.lastOperation ?? null,
|
|
70
|
+
stats
|
|
60
71
|
} : null,
|
|
61
72
|
vocabulary: vocabulary && Object.keys(vocabulary.terms).length > 0 ? {
|
|
62
73
|
size: Object.keys(vocabulary.terms).length,
|
|
@@ -362,6 +373,47 @@ function generateImportSnippet(model, stack, locale) {
|
|
|
362
373
|
return snippets;
|
|
363
374
|
}
|
|
364
375
|
//#endregion
|
|
376
|
+
//#region src/git/errors.ts
|
|
377
|
+
/**
|
|
378
|
+
* Error normalization for git-backed MCP operations.
|
|
379
|
+
*
|
|
380
|
+
* `simple-git` surfaces a failed `git commit`/`push`/`merge` as a `GitError`
|
|
381
|
+
* whose `message` is the raw subprocess stderr — which, for hook tooling
|
|
382
|
+
* (commitlint, husky, lefthook), is colorized multi-line text. Returning that
|
|
383
|
+
* verbatim inside a JSON error produces an unreadable ANSI-escaped blob.
|
|
384
|
+
*
|
|
385
|
+
* This helper strips ANSI, preserves any structured fields the thrown error
|
|
386
|
+
* already carries (`code`/`agent_hint`/`developer_action`), and detects git
|
|
387
|
+
* hook rejections so the agent gets an actionable, structured envelope.
|
|
388
|
+
*/
|
|
389
|
+
const ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
390
|
+
function stripAnsi(input) {
|
|
391
|
+
return input.replace(ANSI_RE, "");
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Normalize any thrown error into a structured, ANSI-free envelope for MCP
|
|
395
|
+
* tool responses. `stage` labels where it happened (e.g. 'content_save').
|
|
396
|
+
*/
|
|
397
|
+
function normalizeOperationError(err, stage) {
|
|
398
|
+
const e = err;
|
|
399
|
+
const message = stripAnsi(e?.message ?? String(err)).trim();
|
|
400
|
+
const out = { error: message };
|
|
401
|
+
if (stage) out.stage = stage;
|
|
402
|
+
if (e?.code) out.code = e.code;
|
|
403
|
+
if (e?.agent_hint) out.agent_hint = e.agent_hint;
|
|
404
|
+
if (e?.developer_action) out.developer_action = e.developer_action;
|
|
405
|
+
if (!out.code) {
|
|
406
|
+
const hookName = /\b(commit-msg|pre-commit|prepare-commit-msg|pre-push)\b/i.exec(message)?.[1];
|
|
407
|
+
const hookTool = /\b(commitlint|husky|lefthook)\b/i.exec(message)?.[1];
|
|
408
|
+
if (hookName || hookTool) {
|
|
409
|
+
out.stage = out.stage ?? "git-commit";
|
|
410
|
+
out.hook = hookName ?? hookTool;
|
|
411
|
+
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.";
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return out;
|
|
415
|
+
}
|
|
416
|
+
//#endregion
|
|
365
417
|
//#region src/tools/guards.ts
|
|
366
418
|
/**
|
|
367
419
|
* Emit a uniform "capability not available" response for tools that
|
|
@@ -413,7 +465,10 @@ function registerSetupTools(server, _provider, projectRoot) {
|
|
|
413
465
|
"-n",
|
|
414
466
|
"1",
|
|
415
467
|
"--all"
|
|
416
|
-
]).then((out) => out.trim().length > 0).catch(() => false)) await git.commit("initial commit", {
|
|
468
|
+
]).then((out) => out.trim().length > 0).catch(() => false)) await git.commit("initial commit", {
|
|
469
|
+
"--allow-empty": null,
|
|
470
|
+
"--no-verify": null
|
|
471
|
+
});
|
|
417
472
|
const initHealth = await checkBranchHealth(projectRoot);
|
|
418
473
|
if (initHealth.blocked) return {
|
|
419
474
|
content: [{
|
|
@@ -513,7 +568,7 @@ function registerSetupTools(server, _provider, projectRoot) {
|
|
|
513
568
|
return {
|
|
514
569
|
content: [{
|
|
515
570
|
type: "text",
|
|
516
|
-
text: JSON.stringify({ error
|
|
571
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "init") })
|
|
517
572
|
}],
|
|
518
573
|
isError: true
|
|
519
574
|
};
|
|
@@ -608,7 +663,7 @@ function registerSetupTools(server, _provider, projectRoot) {
|
|
|
608
663
|
return {
|
|
609
664
|
content: [{
|
|
610
665
|
type: "text",
|
|
611
|
-
text: JSON.stringify({ error
|
|
666
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "scaffold") })
|
|
612
667
|
}],
|
|
613
668
|
isError: true
|
|
614
669
|
};
|
|
@@ -836,7 +891,7 @@ function registerModelTools(server, provider, projectRoot) {
|
|
|
836
891
|
return {
|
|
837
892
|
content: [{
|
|
838
893
|
type: "text",
|
|
839
|
-
text: JSON.stringify({ error
|
|
894
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "model_save") })
|
|
840
895
|
}],
|
|
841
896
|
isError: true
|
|
842
897
|
};
|
|
@@ -959,7 +1014,7 @@ function registerModelTools(server, provider, projectRoot) {
|
|
|
959
1014
|
return {
|
|
960
1015
|
content: [{
|
|
961
1016
|
type: "text",
|
|
962
|
-
text: JSON.stringify({ error
|
|
1017
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "model_delete") })
|
|
963
1018
|
}],
|
|
964
1019
|
isError: true
|
|
965
1020
|
};
|
|
@@ -1064,7 +1119,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1064
1119
|
return {
|
|
1065
1120
|
content: [{
|
|
1066
1121
|
type: "text",
|
|
1067
|
-
text: JSON.stringify({ error
|
|
1122
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "content_save") })
|
|
1068
1123
|
}],
|
|
1069
1124
|
isError: true
|
|
1070
1125
|
};
|
|
@@ -1095,12 +1150,12 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1095
1150
|
return {
|
|
1096
1151
|
content: [{
|
|
1097
1152
|
type: "text",
|
|
1098
|
-
text: JSON.stringify({ error
|
|
1153
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "content_save") })
|
|
1099
1154
|
}],
|
|
1100
1155
|
isError: true
|
|
1101
1156
|
};
|
|
1102
1157
|
}
|
|
1103
|
-
const validationResult =
|
|
1158
|
+
const validationResult = await validateProject(new OverlayReader(provider, plan.changes), { model: input.model });
|
|
1104
1159
|
const allAdvisories = plan.result.flatMap((r) => r.advisories ?? []);
|
|
1105
1160
|
return { content: [{
|
|
1106
1161
|
type: "text",
|
|
@@ -1181,7 +1236,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1181
1236
|
return {
|
|
1182
1237
|
content: [{
|
|
1183
1238
|
type: "text",
|
|
1184
|
-
text: JSON.stringify({ error
|
|
1239
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "content_delete") })
|
|
1185
1240
|
}],
|
|
1186
1241
|
isError: true
|
|
1187
1242
|
};
|
|
@@ -1226,7 +1281,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1226
1281
|
return {
|
|
1227
1282
|
content: [{
|
|
1228
1283
|
type: "text",
|
|
1229
|
-
text: JSON.stringify({ error
|
|
1284
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "content_delete") })
|
|
1230
1285
|
}],
|
|
1231
1286
|
isError: true
|
|
1232
1287
|
};
|
|
@@ -1273,7 +1328,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1273
1328
|
return {
|
|
1274
1329
|
content: [{
|
|
1275
1330
|
type: "text",
|
|
1276
|
-
text: JSON.stringify({ error
|
|
1331
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "content_list") })
|
|
1277
1332
|
}],
|
|
1278
1333
|
isError: true
|
|
1279
1334
|
};
|
|
@@ -1282,6 +1337,49 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1282
1337
|
}
|
|
1283
1338
|
//#endregion
|
|
1284
1339
|
//#region src/tools/workflow.ts
|
|
1340
|
+
/**
|
|
1341
|
+
* Resolve a merge target from either an exact branch name or a
|
|
1342
|
+
* model/locale/latest selector. Returns the concrete branch, or an error with
|
|
1343
|
+
* candidate branches when the selector is ambiguous or matches nothing.
|
|
1344
|
+
*/
|
|
1345
|
+
async function resolveMergeBranch(git, input) {
|
|
1346
|
+
const summary = await git.branchLocal();
|
|
1347
|
+
const crBranches = summary.all.filter((b) => b.startsWith("cr/") && b !== CONTENTRAIN_BRANCH);
|
|
1348
|
+
if (input.branch) {
|
|
1349
|
+
if (summary.all.includes(input.branch)) return { branch: input.branch };
|
|
1350
|
+
return {
|
|
1351
|
+
error: `Branch "${input.branch}" not found locally.`,
|
|
1352
|
+
candidates: crBranches
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
if (!input.model) return {
|
|
1356
|
+
error: "Provide either \"branch\" (exact name) or \"model\" (to resolve the branch).",
|
|
1357
|
+
candidates: crBranches
|
|
1358
|
+
};
|
|
1359
|
+
const prefix = input.locale ? `cr/content/${input.model}/${input.locale}/` : `cr/content/${input.model}/`;
|
|
1360
|
+
let matches = crBranches.filter((b) => b.startsWith(prefix));
|
|
1361
|
+
if (matches.length === 0) matches = crBranches.filter((b) => b.split("/").includes(input.model));
|
|
1362
|
+
if (matches.length === 0) return {
|
|
1363
|
+
error: `No pending cr/* branch found for model "${input.model}".`,
|
|
1364
|
+
candidates: crBranches
|
|
1365
|
+
};
|
|
1366
|
+
if (matches.length === 1) return { branch: matches[0] };
|
|
1367
|
+
if (!input.latest) return {
|
|
1368
|
+
error: `Multiple branches match model "${input.model}". Pass latest:true or an exact branch.`,
|
|
1369
|
+
candidates: matches
|
|
1370
|
+
};
|
|
1371
|
+
return { branch: (await Promise.all(matches.map(async (b) => {
|
|
1372
|
+
return {
|
|
1373
|
+
b,
|
|
1374
|
+
t: await git.raw([
|
|
1375
|
+
"log",
|
|
1376
|
+
"-1",
|
|
1377
|
+
"--format=%ct",
|
|
1378
|
+
b
|
|
1379
|
+
]).then((s) => Number(s.trim())).catch(() => 0)
|
|
1380
|
+
};
|
|
1381
|
+
}))).toSorted((left, right) => right.t - left.t)[0].b };
|
|
1382
|
+
}
|
|
1285
1383
|
function registerWorkflowTools(server, provider, projectRoot) {
|
|
1286
1384
|
server.tool("contentrain_validate", "Validate project content against model schemas. Detects required field violations, type mismatches, broken relations, secret leaks, i18n parity issues, and more. If fix:true, auto-fixes structural issues (canonical sort, orphan meta, missing locale files) — do NOT manually edit .contentrain/ files.", {
|
|
1287
1385
|
model: z.string().optional().describe("Model ID to validate (omit for all models)"),
|
|
@@ -1311,7 +1409,7 @@ function registerWorkflowTools(server, provider, projectRoot) {
|
|
|
1311
1409
|
isError: true
|
|
1312
1410
|
};
|
|
1313
1411
|
const branch = buildBranchName("fix", "validate");
|
|
1314
|
-
const tx = await createTransaction(projectRoot, branch);
|
|
1412
|
+
const tx = await createTransaction(projectRoot, branch, { workflowOverride: "auto-merge" });
|
|
1315
1413
|
try {
|
|
1316
1414
|
await tx.write(async (wt) => {
|
|
1317
1415
|
result = await validateProject(wt, {
|
|
@@ -1404,16 +1502,22 @@ function registerWorkflowTools(server, provider, projectRoot) {
|
|
|
1404
1502
|
} catch {
|
|
1405
1503
|
hasRemote = false;
|
|
1406
1504
|
}
|
|
1407
|
-
if (!hasRemote)
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1505
|
+
if (!hasRemote) {
|
|
1506
|
+
const pending = (await git.branchLocal().catch(() => ({ all: [] }))).all.filter((b) => b.startsWith("cr/") && b !== CONTENTRAIN_BRANCH);
|
|
1507
|
+
return {
|
|
1508
|
+
content: [{
|
|
1509
|
+
type: "text",
|
|
1510
|
+
text: JSON.stringify({
|
|
1511
|
+
error: `No remote "${remoteName}" configured — contentrain_submit (push) is unavailable.`,
|
|
1512
|
+
stage: "submit",
|
|
1513
|
+
agent_hint: "This project has no git remote. In a local/solo workflow, land pending review branches with contentrain_merge instead of submit, or set workflow:\"auto-merge\" in config.json so saves merge locally without a remote.",
|
|
1514
|
+
pending_branches: pending,
|
|
1515
|
+
next_steps: [pending.length > 0 ? `Merge a pending branch locally: contentrain_merge { branch: "${pending[0]}", confirm: true }` : "Make changes with contentrain_content_save — auto-merge lands them locally without a remote", `Or add a remote to enable submit: git remote add ${remoteName} <url>`]
|
|
1516
|
+
}, null, 2)
|
|
1517
|
+
}],
|
|
1518
|
+
isError: true
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1417
1521
|
try {
|
|
1418
1522
|
const baseBranch = config.repository?.default_branch ?? process.env["CONTENTRAIN_BRANCH"] ?? ((await git.raw(["branch", "--show-current"])).trim() || "main");
|
|
1419
1523
|
const branchSummary = await git.branchLocal();
|
|
@@ -1496,8 +1600,11 @@ function registerWorkflowTools(server, provider, projectRoot) {
|
|
|
1496
1600
|
};
|
|
1497
1601
|
}
|
|
1498
1602
|
});
|
|
1499
|
-
server.tool("contentrain_merge", "Merge a review-mode branch into contentrain. Local git operation — no external platform needed. Merges the feature branch into the contentrain branch, advances the base branch via update-ref,
|
|
1500
|
-
branch: z.string().describe("
|
|
1603
|
+
server.tool("contentrain_merge", "Merge a review-mode branch into contentrain. Local git operation — no external platform needed. Merges the feature branch into the contentrain branch, advances the base branch via update-ref, selectively syncs .contentrain/ files to the working tree, and prunes the merged branch. Target by exact \"branch\" name, or resolve by \"model\" (+ optional \"locale\"/\"latest\").", {
|
|
1604
|
+
branch: z.string().optional().describe("Exact branch name to merge (e.g. cr/content/blog-post/...). Omit to resolve by model."),
|
|
1605
|
+
model: z.string().optional().describe("Resolve the branch by model id (e.g. \"blog-post\")."),
|
|
1606
|
+
locale: z.string().optional().describe("Narrow model resolution to a locale."),
|
|
1607
|
+
latest: z.boolean().optional().describe("When multiple branches match the model, merge the most recently committed one."),
|
|
1501
1608
|
confirm: z.literal(true).describe("Must be true to confirm the merge")
|
|
1502
1609
|
}, TOOL_ANNOTATIONS["contentrain_merge"], async (input) => {
|
|
1503
1610
|
if (!provider.capabilities.localWorktree || !projectRoot) return capabilityError("contentrain_merge", "localWorktree");
|
|
@@ -1509,26 +1616,30 @@ function registerWorkflowTools(server, provider, projectRoot) {
|
|
|
1509
1616
|
isError: true
|
|
1510
1617
|
};
|
|
1511
1618
|
try {
|
|
1512
|
-
|
|
1619
|
+
const resolved = await resolveMergeBranch(simpleGit(projectRoot), input);
|
|
1620
|
+
if ("error" in resolved) return {
|
|
1513
1621
|
content: [{
|
|
1514
1622
|
type: "text",
|
|
1515
1623
|
text: JSON.stringify({
|
|
1516
|
-
error:
|
|
1517
|
-
|
|
1518
|
-
|
|
1624
|
+
error: resolved.error,
|
|
1625
|
+
candidates: resolved.candidates,
|
|
1626
|
+
next_steps: ["Pass an exact { branch } or { model, latest: true }", "List pending branches with contentrain_branch_list"]
|
|
1627
|
+
}, null, 2)
|
|
1519
1628
|
}],
|
|
1520
1629
|
isError: true
|
|
1521
1630
|
};
|
|
1522
|
-
const
|
|
1631
|
+
const targetBranch = resolved.branch;
|
|
1632
|
+
const result = await mergeBranch(projectRoot, targetBranch);
|
|
1523
1633
|
return { content: [{
|
|
1524
1634
|
type: "text",
|
|
1525
1635
|
text: JSON.stringify({
|
|
1526
1636
|
status: "merged",
|
|
1637
|
+
branch: targetBranch,
|
|
1527
1638
|
action: result.action,
|
|
1528
1639
|
commit: result.commit,
|
|
1529
1640
|
sync: result.sync,
|
|
1530
1641
|
next_steps: [
|
|
1531
|
-
"Run `npx contentrain generate` to update SDK client",
|
|
1642
|
+
"Run `contentrain generate` (or `npx contentrain-query generate`) to update the SDK client",
|
|
1532
1643
|
"Run contentrain_validate to verify content integrity",
|
|
1533
1644
|
result.sync.skipped.length > 0 ? `${result.sync.skipped.length} file(s) skipped due to local changes — resolve manually` : ""
|
|
1534
1645
|
].filter(Boolean)
|
|
@@ -1538,7 +1649,102 @@ function registerWorkflowTools(server, provider, projectRoot) {
|
|
|
1538
1649
|
return {
|
|
1539
1650
|
content: [{
|
|
1540
1651
|
type: "text",
|
|
1541
|
-
text: JSON.stringify(
|
|
1652
|
+
text: JSON.stringify(normalizeOperationError(error, "merge"), null, 2)
|
|
1653
|
+
}],
|
|
1654
|
+
isError: true
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
server.tool("contentrain_branch_list", "List pending contentrain (cr/*) branches with their merge status against the contentrain branch. Use this to discover branch names for contentrain_merge / contentrain_branch_delete, and to monitor branch-health limits (warning at 50, blocked at 80 unmerged).", { unmerged_only: z.boolean().optional().describe("Only list branches not yet merged into contentrain. Default: false") }, TOOL_ANNOTATIONS["contentrain_branch_list"], async (input) => {
|
|
1659
|
+
if (!provider.capabilities.localWorktree || !projectRoot) return capabilityError("contentrain_branch_list", "localWorktree");
|
|
1660
|
+
try {
|
|
1661
|
+
const git = simpleGit(projectRoot);
|
|
1662
|
+
const summary = await git.branchLocal();
|
|
1663
|
+
const crBranches = summary.all.filter((b) => b.startsWith("cr/") && b !== CONTENTRAIN_BRANCH);
|
|
1664
|
+
const ordered = (await Promise.all(crBranches.map(async (name) => {
|
|
1665
|
+
const [merged, ts] = await Promise.all([isMerged(projectRoot, name, CONTENTRAIN_BRANCH), git.raw([
|
|
1666
|
+
"log",
|
|
1667
|
+
"-1",
|
|
1668
|
+
"--format=%cI",
|
|
1669
|
+
name
|
|
1670
|
+
]).then((s) => s.trim()).catch(() => "")]);
|
|
1671
|
+
return {
|
|
1672
|
+
name,
|
|
1673
|
+
sha: summary.branches[name]?.commit ?? "",
|
|
1674
|
+
merged,
|
|
1675
|
+
lastCommit: ts
|
|
1676
|
+
};
|
|
1677
|
+
}))).toSorted((left, right) => right.lastCommit.localeCompare(left.lastCommit));
|
|
1678
|
+
const filtered = input.unmerged_only ? ordered.filter((b) => !b.merged) : ordered;
|
|
1679
|
+
const health = await checkBranchHealth(projectRoot);
|
|
1680
|
+
return { content: [{
|
|
1681
|
+
type: "text",
|
|
1682
|
+
text: JSON.stringify({
|
|
1683
|
+
total: ordered.length,
|
|
1684
|
+
unmerged: ordered.filter((b) => !b.merged).length,
|
|
1685
|
+
branches: filtered,
|
|
1686
|
+
health: {
|
|
1687
|
+
warning: health.warning,
|
|
1688
|
+
blocked: health.blocked,
|
|
1689
|
+
message: health.message
|
|
1690
|
+
},
|
|
1691
|
+
next_steps: ["Merge a branch: contentrain_merge { branch: \"...\", confirm: true }", "Delete a stale branch: contentrain_branch_delete { branch: \"...\", confirm: true }"]
|
|
1692
|
+
}, null, 2)
|
|
1693
|
+
}] };
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
return {
|
|
1696
|
+
content: [{
|
|
1697
|
+
type: "text",
|
|
1698
|
+
text: JSON.stringify(normalizeOperationError(error, "branch_list"), null, 2)
|
|
1699
|
+
}],
|
|
1700
|
+
isError: true
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
});
|
|
1704
|
+
server.tool("contentrain_branch_delete", "Delete a pending contentrain (cr/*) branch that will not be merged — e.g. a branch left behind by a failed operation, or a superseded draft. Only cr/* branches can be deleted; the contentrain branch is protected. This is destructive: the branch and its unmerged commits are removed.", {
|
|
1705
|
+
branch: z.string().describe("The cr/* branch to delete"),
|
|
1706
|
+
confirm: z.literal(true).describe("Must be true to confirm deletion")
|
|
1707
|
+
}, TOOL_ANNOTATIONS["contentrain_branch_delete"], async (input) => {
|
|
1708
|
+
if (!provider.capabilities.localWorktree || !projectRoot) return capabilityError("contentrain_branch_delete", "localWorktree");
|
|
1709
|
+
if (!input.branch.startsWith("cr/") || input.branch === CONTENTRAIN_BRANCH) return {
|
|
1710
|
+
content: [{
|
|
1711
|
+
type: "text",
|
|
1712
|
+
text: JSON.stringify({ error: `Refusing to delete "${input.branch}". Only cr/* feature branches can be deleted (the contentrain branch is protected).` })
|
|
1713
|
+
}],
|
|
1714
|
+
isError: true
|
|
1715
|
+
};
|
|
1716
|
+
try {
|
|
1717
|
+
const git = simpleGit(projectRoot);
|
|
1718
|
+
if (!(await git.branchLocal()).all.includes(input.branch)) return {
|
|
1719
|
+
content: [{
|
|
1720
|
+
type: "text",
|
|
1721
|
+
text: JSON.stringify({
|
|
1722
|
+
error: `Branch "${input.branch}" not found locally.`,
|
|
1723
|
+
next_steps: ["List branches with contentrain_branch_list"]
|
|
1724
|
+
})
|
|
1725
|
+
}],
|
|
1726
|
+
isError: true
|
|
1727
|
+
};
|
|
1728
|
+
const merged = await isMerged(projectRoot, input.branch, CONTENTRAIN_BRANCH);
|
|
1729
|
+
await git.raw([
|
|
1730
|
+
"branch",
|
|
1731
|
+
"-D",
|
|
1732
|
+
input.branch
|
|
1733
|
+
]);
|
|
1734
|
+
return { content: [{
|
|
1735
|
+
type: "text",
|
|
1736
|
+
text: JSON.stringify({
|
|
1737
|
+
status: "deleted",
|
|
1738
|
+
branch: input.branch,
|
|
1739
|
+
was_merged: merged,
|
|
1740
|
+
warning: merged ? void 0 : "Branch was not merged — its commits were discarded."
|
|
1741
|
+
}, null, 2)
|
|
1742
|
+
}] };
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
return {
|
|
1745
|
+
content: [{
|
|
1746
|
+
type: "text",
|
|
1747
|
+
text: JSON.stringify(normalizeOperationError(error, "branch_delete"), null, 2)
|
|
1542
1748
|
}],
|
|
1543
1749
|
isError: true
|
|
1544
1750
|
};
|
|
@@ -1938,7 +2144,7 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
1938
2144
|
return {
|
|
1939
2145
|
content: [{
|
|
1940
2146
|
type: "text",
|
|
1941
|
-
text: JSON.stringify({ error
|
|
2147
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "bulk_copy_locale") })
|
|
1942
2148
|
}],
|
|
1943
2149
|
isError: true
|
|
1944
2150
|
};
|
|
@@ -2022,7 +2228,7 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2022
2228
|
return {
|
|
2023
2229
|
content: [{
|
|
2024
2230
|
type: "text",
|
|
2025
|
-
text: JSON.stringify({ error
|
|
2231
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "bulk_update_status") })
|
|
2026
2232
|
}],
|
|
2027
2233
|
isError: true
|
|
2028
2234
|
};
|
|
@@ -2090,7 +2296,7 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2090
2296
|
return {
|
|
2091
2297
|
content: [{
|
|
2092
2298
|
type: "text",
|
|
2093
|
-
text: JSON.stringify({ error
|
|
2299
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "bulk_delete_entries") })
|
|
2094
2300
|
}],
|
|
2095
2301
|
isError: true
|
|
2096
2302
|
};
|
|
@@ -2122,7 +2328,7 @@ function registerDoctorTools(server, _provider, projectRoot) {
|
|
|
2122
2328
|
}
|
|
2123
2329
|
//#endregion
|
|
2124
2330
|
//#region package.json
|
|
2125
|
-
var version = "1.
|
|
2331
|
+
var version = "1.5.0";
|
|
2126
2332
|
//#endregion
|
|
2127
2333
|
//#region src/server.ts
|
|
2128
2334
|
function createServer(input) {
|
|
@@ -2163,4 +2369,4 @@ function resolveServerContext(input) {
|
|
|
2163
2369
|
//#endregion
|
|
2164
2370
|
export { createServer as t };
|
|
2165
2371
|
|
|
2166
|
-
//# sourceMappingURL=server-
|
|
2372
|
+
//# sourceMappingURL=server-BiiVUjgs.mjs.map
|