@contentrain/mcp 1.4.0 → 1.5.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 -4
- 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/overlay-reader.mjs +1 -1
- 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/{overlay-reader-DOdyDRIv.mjs → overlay-reader-CUs9jgqk.mjs} +1 -1
- package/dist/{overlay-reader-DOdyDRIv.mjs.map → overlay-reader-CUs9jgqk.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-BpQvNZke.mjs} +266 -55
- package/dist/server-BpQvNZke.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-Bo06QPVB.mjs} +46 -17
- package/dist/validator-Bo06QPVB.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
|
|
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 } 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
|
-
import { t as
|
|
15
|
-
import { t as
|
|
14
|
+
import { t as validateProject } from "./validator-Bo06QPVB.mjs";
|
|
15
|
+
import { t as OverlayReader } from "./overlay-reader-CUs9jgqk.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
|
};
|
|
@@ -695,11 +750,17 @@ async function mergeVocabulary(projectRoot, newTerms, _locales) {
|
|
|
695
750
|
* the project's configured workflow. Selective-sync result is surfaced
|
|
696
751
|
* to the caller via `sync`.
|
|
697
752
|
*
|
|
698
|
-
* - **Any other RepoProvider** — the
|
|
699
|
-
*
|
|
700
|
-
*
|
|
753
|
+
* - **Any other RepoProvider** — only the plan's own changes are
|
|
754
|
+
* committed. Feature branches NEVER carry `.contentrain/context.json`:
|
|
755
|
+
* the file embeds timestamps, so two parallel cr/* branches forked from
|
|
756
|
+
* the same contentrain commit would always conflict on it and the
|
|
757
|
+
* second merge would fail permanently. This mirrors the local
|
|
758
|
+
* transaction flow, which regenerates context.json on the contentrain
|
|
759
|
+
* branch after merge (single-threaded, deterministic). Remote flows
|
|
701
760
|
* always report `pending-review`; Studio (or whatever orchestrator is
|
|
702
|
-
* driving the server) owns the merge
|
|
761
|
+
* driving the server) owns the merge AND the post-merge context
|
|
762
|
+
* regeneration on the contentrain branch (`buildContextChange` from
|
|
763
|
+
* `@contentrain/mcp/core/context` is exported for exactly that).
|
|
703
764
|
*
|
|
704
765
|
* The return shape is deliberately uniform so callers don't have to
|
|
705
766
|
* branch on provider type again.
|
|
@@ -719,8 +780,7 @@ async function commitThroughProvider(provider, input) {
|
|
|
719
780
|
sync: result.sync
|
|
720
781
|
};
|
|
721
782
|
}
|
|
722
|
-
const
|
|
723
|
-
const allChanges = [...changes, contextChange].toSorted((a, b) => a.path.localeCompare(b.path));
|
|
783
|
+
const allChanges = changes.toSorted((a, b) => a.path.localeCompare(b.path));
|
|
724
784
|
return {
|
|
725
785
|
commitSha: (await provider.applyPlan({
|
|
726
786
|
branch,
|
|
@@ -836,7 +896,7 @@ function registerModelTools(server, provider, projectRoot) {
|
|
|
836
896
|
return {
|
|
837
897
|
content: [{
|
|
838
898
|
type: "text",
|
|
839
|
-
text: JSON.stringify({ error
|
|
899
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "model_save") })
|
|
840
900
|
}],
|
|
841
901
|
isError: true
|
|
842
902
|
};
|
|
@@ -959,7 +1019,7 @@ function registerModelTools(server, provider, projectRoot) {
|
|
|
959
1019
|
return {
|
|
960
1020
|
content: [{
|
|
961
1021
|
type: "text",
|
|
962
|
-
text: JSON.stringify({ error
|
|
1022
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "model_delete") })
|
|
963
1023
|
}],
|
|
964
1024
|
isError: true
|
|
965
1025
|
};
|
|
@@ -1064,7 +1124,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1064
1124
|
return {
|
|
1065
1125
|
content: [{
|
|
1066
1126
|
type: "text",
|
|
1067
|
-
text: JSON.stringify({ error
|
|
1127
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "content_save") })
|
|
1068
1128
|
}],
|
|
1069
1129
|
isError: true
|
|
1070
1130
|
};
|
|
@@ -1095,12 +1155,12 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1095
1155
|
return {
|
|
1096
1156
|
content: [{
|
|
1097
1157
|
type: "text",
|
|
1098
|
-
text: JSON.stringify({ error
|
|
1158
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "content_save") })
|
|
1099
1159
|
}],
|
|
1100
1160
|
isError: true
|
|
1101
1161
|
};
|
|
1102
1162
|
}
|
|
1103
|
-
const validationResult =
|
|
1163
|
+
const validationResult = await validateProject(new OverlayReader(provider, plan.changes), { model: input.model });
|
|
1104
1164
|
const allAdvisories = plan.result.flatMap((r) => r.advisories ?? []);
|
|
1105
1165
|
return { content: [{
|
|
1106
1166
|
type: "text",
|
|
@@ -1181,7 +1241,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1181
1241
|
return {
|
|
1182
1242
|
content: [{
|
|
1183
1243
|
type: "text",
|
|
1184
|
-
text: JSON.stringify({ error
|
|
1244
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "content_delete") })
|
|
1185
1245
|
}],
|
|
1186
1246
|
isError: true
|
|
1187
1247
|
};
|
|
@@ -1226,7 +1286,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1226
1286
|
return {
|
|
1227
1287
|
content: [{
|
|
1228
1288
|
type: "text",
|
|
1229
|
-
text: JSON.stringify({ error
|
|
1289
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "content_delete") })
|
|
1230
1290
|
}],
|
|
1231
1291
|
isError: true
|
|
1232
1292
|
};
|
|
@@ -1273,7 +1333,7 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1273
1333
|
return {
|
|
1274
1334
|
content: [{
|
|
1275
1335
|
type: "text",
|
|
1276
|
-
text: JSON.stringify({ error
|
|
1336
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "content_list") })
|
|
1277
1337
|
}],
|
|
1278
1338
|
isError: true
|
|
1279
1339
|
};
|
|
@@ -1282,6 +1342,49 @@ function registerContentTools(server, provider, projectRoot) {
|
|
|
1282
1342
|
}
|
|
1283
1343
|
//#endregion
|
|
1284
1344
|
//#region src/tools/workflow.ts
|
|
1345
|
+
/**
|
|
1346
|
+
* Resolve a merge target from either an exact branch name or a
|
|
1347
|
+
* model/locale/latest selector. Returns the concrete branch, or an error with
|
|
1348
|
+
* candidate branches when the selector is ambiguous or matches nothing.
|
|
1349
|
+
*/
|
|
1350
|
+
async function resolveMergeBranch(git, input) {
|
|
1351
|
+
const summary = await git.branchLocal();
|
|
1352
|
+
const crBranches = summary.all.filter((b) => b.startsWith("cr/") && b !== CONTENTRAIN_BRANCH);
|
|
1353
|
+
if (input.branch) {
|
|
1354
|
+
if (summary.all.includes(input.branch)) return { branch: input.branch };
|
|
1355
|
+
return {
|
|
1356
|
+
error: `Branch "${input.branch}" not found locally.`,
|
|
1357
|
+
candidates: crBranches
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
if (!input.model) return {
|
|
1361
|
+
error: "Provide either \"branch\" (exact name) or \"model\" (to resolve the branch).",
|
|
1362
|
+
candidates: crBranches
|
|
1363
|
+
};
|
|
1364
|
+
const prefix = input.locale ? `cr/content/${input.model}/${input.locale}/` : `cr/content/${input.model}/`;
|
|
1365
|
+
let matches = crBranches.filter((b) => b.startsWith(prefix));
|
|
1366
|
+
if (matches.length === 0) matches = crBranches.filter((b) => b.split("/").includes(input.model));
|
|
1367
|
+
if (matches.length === 0) return {
|
|
1368
|
+
error: `No pending cr/* branch found for model "${input.model}".`,
|
|
1369
|
+
candidates: crBranches
|
|
1370
|
+
};
|
|
1371
|
+
if (matches.length === 1) return { branch: matches[0] };
|
|
1372
|
+
if (!input.latest) return {
|
|
1373
|
+
error: `Multiple branches match model "${input.model}". Pass latest:true or an exact branch.`,
|
|
1374
|
+
candidates: matches
|
|
1375
|
+
};
|
|
1376
|
+
return { branch: (await Promise.all(matches.map(async (b) => {
|
|
1377
|
+
return {
|
|
1378
|
+
b,
|
|
1379
|
+
t: await git.raw([
|
|
1380
|
+
"log",
|
|
1381
|
+
"-1",
|
|
1382
|
+
"--format=%ct",
|
|
1383
|
+
b
|
|
1384
|
+
]).then((s) => Number(s.trim())).catch(() => 0)
|
|
1385
|
+
};
|
|
1386
|
+
}))).toSorted((left, right) => right.t - left.t)[0].b };
|
|
1387
|
+
}
|
|
1285
1388
|
function registerWorkflowTools(server, provider, projectRoot) {
|
|
1286
1389
|
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
1390
|
model: z.string().optional().describe("Model ID to validate (omit for all models)"),
|
|
@@ -1311,7 +1414,7 @@ function registerWorkflowTools(server, provider, projectRoot) {
|
|
|
1311
1414
|
isError: true
|
|
1312
1415
|
};
|
|
1313
1416
|
const branch = buildBranchName("fix", "validate");
|
|
1314
|
-
const tx = await createTransaction(projectRoot, branch);
|
|
1417
|
+
const tx = await createTransaction(projectRoot, branch, { workflowOverride: "auto-merge" });
|
|
1315
1418
|
try {
|
|
1316
1419
|
await tx.write(async (wt) => {
|
|
1317
1420
|
result = await validateProject(wt, {
|
|
@@ -1404,16 +1507,22 @@ function registerWorkflowTools(server, provider, projectRoot) {
|
|
|
1404
1507
|
} catch {
|
|
1405
1508
|
hasRemote = false;
|
|
1406
1509
|
}
|
|
1407
|
-
if (!hasRemote)
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1510
|
+
if (!hasRemote) {
|
|
1511
|
+
const pending = (await git.branchLocal().catch(() => ({ all: [] }))).all.filter((b) => b.startsWith("cr/") && b !== CONTENTRAIN_BRANCH);
|
|
1512
|
+
return {
|
|
1513
|
+
content: [{
|
|
1514
|
+
type: "text",
|
|
1515
|
+
text: JSON.stringify({
|
|
1516
|
+
error: `No remote "${remoteName}" configured — contentrain_submit (push) is unavailable.`,
|
|
1517
|
+
stage: "submit",
|
|
1518
|
+
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.",
|
|
1519
|
+
pending_branches: pending,
|
|
1520
|
+
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>`]
|
|
1521
|
+
}, null, 2)
|
|
1522
|
+
}],
|
|
1523
|
+
isError: true
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1417
1526
|
try {
|
|
1418
1527
|
const baseBranch = config.repository?.default_branch ?? process.env["CONTENTRAIN_BRANCH"] ?? ((await git.raw(["branch", "--show-current"])).trim() || "main");
|
|
1419
1528
|
const branchSummary = await git.branchLocal();
|
|
@@ -1496,8 +1605,11 @@ function registerWorkflowTools(server, provider, projectRoot) {
|
|
|
1496
1605
|
};
|
|
1497
1606
|
}
|
|
1498
1607
|
});
|
|
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("
|
|
1608
|
+
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\").", {
|
|
1609
|
+
branch: z.string().optional().describe("Exact branch name to merge (e.g. cr/content/blog-post/...). Omit to resolve by model."),
|
|
1610
|
+
model: z.string().optional().describe("Resolve the branch by model id (e.g. \"blog-post\")."),
|
|
1611
|
+
locale: z.string().optional().describe("Narrow model resolution to a locale."),
|
|
1612
|
+
latest: z.boolean().optional().describe("When multiple branches match the model, merge the most recently committed one."),
|
|
1501
1613
|
confirm: z.literal(true).describe("Must be true to confirm the merge")
|
|
1502
1614
|
}, TOOL_ANNOTATIONS["contentrain_merge"], async (input) => {
|
|
1503
1615
|
if (!provider.capabilities.localWorktree || !projectRoot) return capabilityError("contentrain_merge", "localWorktree");
|
|
@@ -1509,26 +1621,30 @@ function registerWorkflowTools(server, provider, projectRoot) {
|
|
|
1509
1621
|
isError: true
|
|
1510
1622
|
};
|
|
1511
1623
|
try {
|
|
1512
|
-
|
|
1624
|
+
const resolved = await resolveMergeBranch(simpleGit(projectRoot), input);
|
|
1625
|
+
if ("error" in resolved) return {
|
|
1513
1626
|
content: [{
|
|
1514
1627
|
type: "text",
|
|
1515
1628
|
text: JSON.stringify({
|
|
1516
|
-
error:
|
|
1517
|
-
|
|
1518
|
-
|
|
1629
|
+
error: resolved.error,
|
|
1630
|
+
candidates: resolved.candidates,
|
|
1631
|
+
next_steps: ["Pass an exact { branch } or { model, latest: true }", "List pending branches with contentrain_branch_list"]
|
|
1632
|
+
}, null, 2)
|
|
1519
1633
|
}],
|
|
1520
1634
|
isError: true
|
|
1521
1635
|
};
|
|
1522
|
-
const
|
|
1636
|
+
const targetBranch = resolved.branch;
|
|
1637
|
+
const result = await mergeBranch(projectRoot, targetBranch);
|
|
1523
1638
|
return { content: [{
|
|
1524
1639
|
type: "text",
|
|
1525
1640
|
text: JSON.stringify({
|
|
1526
1641
|
status: "merged",
|
|
1642
|
+
branch: targetBranch,
|
|
1527
1643
|
action: result.action,
|
|
1528
1644
|
commit: result.commit,
|
|
1529
1645
|
sync: result.sync,
|
|
1530
1646
|
next_steps: [
|
|
1531
|
-
"Run `npx contentrain generate` to update SDK client",
|
|
1647
|
+
"Run `contentrain generate` (or `npx contentrain-query generate`) to update the SDK client",
|
|
1532
1648
|
"Run contentrain_validate to verify content integrity",
|
|
1533
1649
|
result.sync.skipped.length > 0 ? `${result.sync.skipped.length} file(s) skipped due to local changes — resolve manually` : ""
|
|
1534
1650
|
].filter(Boolean)
|
|
@@ -1538,7 +1654,102 @@ function registerWorkflowTools(server, provider, projectRoot) {
|
|
|
1538
1654
|
return {
|
|
1539
1655
|
content: [{
|
|
1540
1656
|
type: "text",
|
|
1541
|
-
text: JSON.stringify(
|
|
1657
|
+
text: JSON.stringify(normalizeOperationError(error, "merge"), null, 2)
|
|
1658
|
+
}],
|
|
1659
|
+
isError: true
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
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) => {
|
|
1664
|
+
if (!provider.capabilities.localWorktree || !projectRoot) return capabilityError("contentrain_branch_list", "localWorktree");
|
|
1665
|
+
try {
|
|
1666
|
+
const git = simpleGit(projectRoot);
|
|
1667
|
+
const summary = await git.branchLocal();
|
|
1668
|
+
const crBranches = summary.all.filter((b) => b.startsWith("cr/") && b !== CONTENTRAIN_BRANCH);
|
|
1669
|
+
const ordered = (await Promise.all(crBranches.map(async (name) => {
|
|
1670
|
+
const [merged, ts] = await Promise.all([isMerged(projectRoot, name, CONTENTRAIN_BRANCH), git.raw([
|
|
1671
|
+
"log",
|
|
1672
|
+
"-1",
|
|
1673
|
+
"--format=%cI",
|
|
1674
|
+
name
|
|
1675
|
+
]).then((s) => s.trim()).catch(() => "")]);
|
|
1676
|
+
return {
|
|
1677
|
+
name,
|
|
1678
|
+
sha: summary.branches[name]?.commit ?? "",
|
|
1679
|
+
merged,
|
|
1680
|
+
lastCommit: ts
|
|
1681
|
+
};
|
|
1682
|
+
}))).toSorted((left, right) => right.lastCommit.localeCompare(left.lastCommit));
|
|
1683
|
+
const filtered = input.unmerged_only ? ordered.filter((b) => !b.merged) : ordered;
|
|
1684
|
+
const health = await checkBranchHealth(projectRoot);
|
|
1685
|
+
return { content: [{
|
|
1686
|
+
type: "text",
|
|
1687
|
+
text: JSON.stringify({
|
|
1688
|
+
total: ordered.length,
|
|
1689
|
+
unmerged: ordered.filter((b) => !b.merged).length,
|
|
1690
|
+
branches: filtered,
|
|
1691
|
+
health: {
|
|
1692
|
+
warning: health.warning,
|
|
1693
|
+
blocked: health.blocked,
|
|
1694
|
+
message: health.message
|
|
1695
|
+
},
|
|
1696
|
+
next_steps: ["Merge a branch: contentrain_merge { branch: \"...\", confirm: true }", "Delete a stale branch: contentrain_branch_delete { branch: \"...\", confirm: true }"]
|
|
1697
|
+
}, null, 2)
|
|
1698
|
+
}] };
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
return {
|
|
1701
|
+
content: [{
|
|
1702
|
+
type: "text",
|
|
1703
|
+
text: JSON.stringify(normalizeOperationError(error, "branch_list"), null, 2)
|
|
1704
|
+
}],
|
|
1705
|
+
isError: true
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
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.", {
|
|
1710
|
+
branch: z.string().describe("The cr/* branch to delete"),
|
|
1711
|
+
confirm: z.literal(true).describe("Must be true to confirm deletion")
|
|
1712
|
+
}, TOOL_ANNOTATIONS["contentrain_branch_delete"], async (input) => {
|
|
1713
|
+
if (!provider.capabilities.localWorktree || !projectRoot) return capabilityError("contentrain_branch_delete", "localWorktree");
|
|
1714
|
+
if (!input.branch.startsWith("cr/") || input.branch === CONTENTRAIN_BRANCH) return {
|
|
1715
|
+
content: [{
|
|
1716
|
+
type: "text",
|
|
1717
|
+
text: JSON.stringify({ error: `Refusing to delete "${input.branch}". Only cr/* feature branches can be deleted (the contentrain branch is protected).` })
|
|
1718
|
+
}],
|
|
1719
|
+
isError: true
|
|
1720
|
+
};
|
|
1721
|
+
try {
|
|
1722
|
+
const git = simpleGit(projectRoot);
|
|
1723
|
+
if (!(await git.branchLocal()).all.includes(input.branch)) return {
|
|
1724
|
+
content: [{
|
|
1725
|
+
type: "text",
|
|
1726
|
+
text: JSON.stringify({
|
|
1727
|
+
error: `Branch "${input.branch}" not found locally.`,
|
|
1728
|
+
next_steps: ["List branches with contentrain_branch_list"]
|
|
1729
|
+
})
|
|
1730
|
+
}],
|
|
1731
|
+
isError: true
|
|
1732
|
+
};
|
|
1733
|
+
const merged = await isMerged(projectRoot, input.branch, CONTENTRAIN_BRANCH);
|
|
1734
|
+
await git.raw([
|
|
1735
|
+
"branch",
|
|
1736
|
+
"-D",
|
|
1737
|
+
input.branch
|
|
1738
|
+
]);
|
|
1739
|
+
return { content: [{
|
|
1740
|
+
type: "text",
|
|
1741
|
+
text: JSON.stringify({
|
|
1742
|
+
status: "deleted",
|
|
1743
|
+
branch: input.branch,
|
|
1744
|
+
was_merged: merged,
|
|
1745
|
+
warning: merged ? void 0 : "Branch was not merged — its commits were discarded."
|
|
1746
|
+
}, null, 2)
|
|
1747
|
+
}] };
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
return {
|
|
1750
|
+
content: [{
|
|
1751
|
+
type: "text",
|
|
1752
|
+
text: JSON.stringify(normalizeOperationError(error, "branch_delete"), null, 2)
|
|
1542
1753
|
}],
|
|
1543
1754
|
isError: true
|
|
1544
1755
|
};
|
|
@@ -1938,7 +2149,7 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
1938
2149
|
return {
|
|
1939
2150
|
content: [{
|
|
1940
2151
|
type: "text",
|
|
1941
|
-
text: JSON.stringify({ error
|
|
2152
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "bulk_copy_locale") })
|
|
1942
2153
|
}],
|
|
1943
2154
|
isError: true
|
|
1944
2155
|
};
|
|
@@ -2022,7 +2233,7 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2022
2233
|
return {
|
|
2023
2234
|
content: [{
|
|
2024
2235
|
type: "text",
|
|
2025
|
-
text: JSON.stringify({ error
|
|
2236
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "bulk_update_status") })
|
|
2026
2237
|
}],
|
|
2027
2238
|
isError: true
|
|
2028
2239
|
};
|
|
@@ -2090,7 +2301,7 @@ function registerBulkTools(server, _provider, projectRoot) {
|
|
|
2090
2301
|
return {
|
|
2091
2302
|
content: [{
|
|
2092
2303
|
type: "text",
|
|
2093
|
-
text: JSON.stringify({ error
|
|
2304
|
+
text: JSON.stringify({ ...normalizeOperationError(error, "bulk_delete_entries") })
|
|
2094
2305
|
}],
|
|
2095
2306
|
isError: true
|
|
2096
2307
|
};
|
|
@@ -2122,7 +2333,7 @@ function registerDoctorTools(server, _provider, projectRoot) {
|
|
|
2122
2333
|
}
|
|
2123
2334
|
//#endregion
|
|
2124
2335
|
//#region package.json
|
|
2125
|
-
var version = "1.
|
|
2336
|
+
var version = "1.5.1";
|
|
2126
2337
|
//#endregion
|
|
2127
2338
|
//#region src/server.ts
|
|
2128
2339
|
function createServer(input) {
|
|
@@ -2163,4 +2374,4 @@ function resolveServerContext(input) {
|
|
|
2163
2374
|
//#endregion
|
|
2164
2375
|
export { createServer as t };
|
|
2165
2376
|
|
|
2166
|
-
//# sourceMappingURL=server-
|
|
2377
|
+
//# sourceMappingURL=server-BpQvNZke.mjs.map
|