@contentrain/mcp 3.7.0 → 3.8.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 +1 -0
- package/dist/{content-manager-wKsAkhly.d.mts → content-manager-2g0pkLFh.d.mts} +2 -2
- package/dist/{content-manager-wKsAkhly.d.mts.map → content-manager-2g0pkLFh.d.mts.map} +1 -1
- package/dist/core/config.d.mts +1 -1
- package/dist/core/content-manager.d.mts +1 -1
- package/dist/core/context.d.mts +1 -1
- package/dist/core/contracts/index.d.mts +2 -2
- package/dist/core/model-manager.d.mts +1 -1
- package/dist/core/ops/index.d.mts +3 -3
- package/dist/core/overlay-reader.d.mts +1 -1
- package/dist/core/validator/index.d.mts +1 -1
- package/dist/git/reconcile.d.mts +1 -1
- package/dist/{index-CkhXTFdk.d.mts → index-BHpAYYFF.d.mts} +2 -2
- package/dist/{index-CkhXTFdk.d.mts.map → index-BHpAYYFF.d.mts.map} +1 -1
- package/dist/index-v0N9d4GT.d.mts +2 -0
- package/dist/index.mjs +2 -2
- package/dist/{local-BySg4Ib-.mjs → local-BXTVAAr-.mjs} +311 -14
- package/dist/local-BXTVAAr-.mjs.map +1 -0
- package/dist/providers/github/index.d.mts +1 -1
- package/dist/providers/gitlab/index.d.mts +1 -1
- package/dist/providers/local/index.d.mts +4 -1
- package/dist/providers/local/index.d.mts.map +1 -1
- package/dist/providers/local/index.mjs +1 -1
- package/dist/server/http/index.d.mts +1 -1
- package/dist/server/http/index.mjs +2 -2
- package/dist/{server-D1c-79hA.d.mts → server-24UHN9Ll.d.mts} +2 -2
- package/dist/server-24UHN9Ll.d.mts.map +1 -0
- package/dist/{server-BdGsBjPT.mjs → server-Sedx3tOQ.mjs} +54 -31
- package/dist/server-Sedx3tOQ.mjs.map +1 -0
- package/dist/server.d.mts +1 -1
- package/dist/server.mjs +2 -2
- package/dist/testing/memory-provider.d.mts +1 -1
- package/package.json +2 -2
- package/dist/index-PCQQBpIH.d.mts +0 -2
- package/dist/local-BySg4Ib-.mjs.map +0 -1
- package/dist/server-BdGsBjPT.mjs.map +0 -1
- package/dist/server-D1c-79hA.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -64,6 +64,7 @@ All write operations are designed around git-backed safety:
|
|
|
64
64
|
- auto-merge: feature merges into `contentrain`, baseBranch advanced via update-ref, `.contentrain/` files selectively synced to developer's working tree when the base branch is the one checked out
|
|
65
65
|
- baseBranch = `CONTENTRAIN_BRANCH` env → `repository.default_branch` → remote HEAD → `main` → `master` → checked-out branch (last resort). A write made on a feature branch never merges, moves or pushes that branch
|
|
66
66
|
- plans are read from the working tree but committed on the fetched `contentrain` tip: JSON changes are carried over key by key, so entries another writer pushed are kept; a same-value conflict writes nothing and returns `CONTENT_WORKING_TREE_STALE`
|
|
67
|
+
- on a checked-out feature branch, which writes do not update, `.contentrain/**` is read from the `contentrain` ref instead (source files still from the working tree); read tools report it as `content_source`
|
|
67
68
|
- review: feature branch pushed to remote for team review; once merged (or deleted), its remote copy is removed too — best-effort, opt out with `remoteBranchCleanup: false` in config.json
|
|
68
69
|
- merged-branch detection survives base-history rewrites (ancestry check with a patch-id fallback), so rebases/squashes don't strand stale branches
|
|
69
70
|
- developer's working tree is never mutated during MCP git operations (no stash, no checkout, no merge)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { v as RepoReader } from "./index-v0N9d4GT.mjs";
|
|
2
2
|
import { ContentrainConfig, LocaleStrategy, ModelDefinition, Vocabulary, parseMarkdownFrontmatter as parseFrontmatter, serializeMarkdownFrontmatter as serializeFrontmatter, validateEntryId as validateEntryId$1, validateLocale as validateLocale$1, validateSlug as validateSlug$1 } from "@contentrain/types";
|
|
3
3
|
|
|
4
4
|
//#region src/core/content-manager.d.ts
|
|
@@ -73,4 +73,4 @@ declare function readContent(projectRoot: string, model: ModelDefinition, opts:
|
|
|
73
73
|
}): Promise<unknown>;
|
|
74
74
|
//#endregion
|
|
75
75
|
export { validateEntryId$1 as _, ListOpts as a, writeContent as b, deleteContent as c, readContent as d, resolveContentDir as f, serializeFrontmatter as g, resolveMdFilePath as h, DeleteOpts as i, listContent as l, resolveLocaleStrategy as m, ContentFileModel as n, LocaleStrategyModel as o, resolveJsonFilePath as p, ContentPathModel as r, WriteResult as s, ContentEntry as t, parseFrontmatter as u, validateLocale$1 as v, validateSlug$1 as y };
|
|
76
|
-
//# sourceMappingURL=content-manager-
|
|
76
|
+
//# sourceMappingURL=content-manager-2g0pkLFh.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-manager-
|
|
1
|
+
{"version":3,"file":"content-manager-2g0pkLFh.d.mts","names":[],"sources":["../src/core/content-manager.ts"],"mappings":";;;;KAyBY,gBAAA,GAAmB,IAAA,CAAK,eAAA;AAAA,KACxB,mBAAA,GAAsB,IAAA,CAAK,eAAA;AAAA,KAC3B,gBAAA,GAAmB,IAAA,CAAK,eAAA;AAAA,iBAEpB,iBAAA,CAAkB,WAAA,UAAqB,KAAA,EAAO,gBAAA;AAAA,iBAO9C,qBAAA,CAAsB,KAAA,EAAO,mBAAA,GAAsB,cAAA;;iBAKnD,mBAAA,CAAoB,GAAA,UAAa,KAAA,EAAO,gBAAA,EAAkB,MAAA;;iBAc1D,iBAAA,CAAkB,GAAA,UAAa,KAAA,EAAO,IAAA,CAAK,eAAA,+BAA8C,MAAA,UAAgB,IAAA;AAAA,UAexG,YAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA,EAAM,MAAA;EA7CQ;;;;;EAmDd,UAAA;EAnDqD;EAqDrD,SAAA;AAAA;AAAA,UAGe,WAAA;EACf,MAAA;EACA,EAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;AAAA;AAAA,UAGe,UAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EAxDsE;EA0DtE,aAAA;AAAA;AAAA,UAGe,QAAA;EACf,MAAA;EACA,MAAA,GAAS,MAAA;EACT,OAAA;EACA,KAAA;EACA,MAAA;AAAA;AAAA,iBAKoB,YAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,OAAA,EAAS,YAAA,IACT,MAAA,EAAQ,iBAAA,EACR,UAAA,GAAa,UAAA,UACZ,OAAA,CAAQ,WAAA;AAAA,iBAuJW,aAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,UAAA,GACL,OAAA;;;;;;;;AA3MH;;;;;;;;iBAwVgB,WAAA,CACd,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBACa,WAAA,CACd,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBA0QmB,WAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA;EAAQ,MAAA;EAAgB,OAAA;EAAkB,IAAA;AAAA,IACzC,OAAA"}
|
package/dist/core/config.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as validateEntryId, a as ListOpts, b as writeContent, c as deleteContent, d as readContent, f as resolveContentDir, g as serializeFrontmatter, h as resolveMdFilePath, i as DeleteOpts, l as listContent, m as resolveLocaleStrategy, n as ContentFileModel, o as LocaleStrategyModel, p as resolveJsonFilePath, r as ContentPathModel, s as WriteResult, t as ContentEntry, u as parseFrontmatter, v as validateLocale, y as validateSlug } from "../content-manager-
|
|
1
|
+
import { _ as validateEntryId, a as ListOpts, b as writeContent, c as deleteContent, d as readContent, f as resolveContentDir, g as serializeFrontmatter, h as resolveMdFilePath, i as DeleteOpts, l as listContent, m as resolveLocaleStrategy, n as ContentFileModel, o as LocaleStrategyModel, p as resolveJsonFilePath, r as ContentPathModel, s as WriteResult, t as ContentEntry, u as parseFrontmatter, v as validateLocale, y as validateSlug } from "../content-manager-2g0pkLFh.mjs";
|
|
2
2
|
export { ContentEntry, ContentFileModel, ContentPathModel, DeleteOpts, ListOpts, LocaleStrategyModel, WriteResult, deleteContent, listContent, parseFrontmatter, readContent, resolveContentDir, resolveJsonFilePath, resolveLocaleStrategy, resolveMdFilePath, serializeFrontmatter, validateEntryId, validateLocale, validateSlug, writeContent };
|
package/dist/core/context.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
export { ApplyPlanInput, Branch, Commit, CommitAuthor, FileChange, FileDiff, LOCAL_CAPABILITIES, MediaAsset, MediaIngestInput, MediaListOptions, MediaListResult, MediaProvider, MediaUpdateInput, MergeResult, ProviderCapabilities, RepoProvider, RepoReader, RepoWriter, WriteReadiness };
|
|
1
|
+
import { _ as RepoProvider, a as ContentReadSource, b as WriteReadiness, c as LOCAL_CAPABILITIES, d as MediaListOptions, f as MediaListResult, g as ProviderCapabilities, h as MergeResult, i as CommitAuthor, l as MediaAsset, m as MediaUpdateInput, n as Branch, o as FileChange, p as MediaProvider, r as Commit, s as FileDiff, t as ApplyPlanInput, u as MediaIngestInput, v as RepoReader, y as RepoWriter } from "../../index-v0N9d4GT.mjs";
|
|
2
|
+
export { ApplyPlanInput, Branch, Commit, CommitAuthor, ContentReadSource, FileChange, FileDiff, LOCAL_CAPABILITIES, MediaAsset, MediaIngestInput, MediaListOptions, MediaListResult, MediaProvider, MediaUpdateInput, MergeResult, ProviderCapabilities, RepoProvider, RepoReader, RepoWriter, WriteReadiness };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as ContentEntry } from "../../content-manager-
|
|
3
|
-
import { a as ContentSaveEntryResult, i as ReconcileResult, n as ReconcileInput, o as ContentSavePlan, r as ReconcilePlan, s as OpPlan, t as planReconcile } from "../../index-
|
|
1
|
+
import { o as FileChange, v as RepoReader } from "../../index-v0N9d4GT.mjs";
|
|
2
|
+
import { t as ContentEntry } from "../../content-manager-2g0pkLFh.mjs";
|
|
3
|
+
import { a as ContentSaveEntryResult, i as ReconcileResult, n as ReconcileInput, o as ContentSavePlan, r as ReconcilePlan, s as OpPlan, t as planReconcile } from "../../index-BHpAYYFF.mjs";
|
|
4
4
|
import { ContentrainConfig, ModelDefinition, Vocabulary } from "@contentrain/types";
|
|
5
5
|
|
|
6
6
|
//#region src/core/ops/apply-to-worktree.d.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { v as RepoReader } from "../../index-v0N9d4GT.mjs";
|
|
2
2
|
import { EntryMeta, FieldDef, ModelDefinition, ValidationError, ValidationResult } from "@contentrain/types";
|
|
3
3
|
|
|
4
4
|
//#region src/core/validator/entry.d.ts
|
package/dist/git/reconcile.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { o as FileChange, v as RepoReader } from "./index-v0N9d4GT.mjs";
|
|
2
2
|
import { ConflictItem, ConflictResolution, ContextSource } from "@contentrain/types";
|
|
3
3
|
|
|
4
4
|
//#region src/core/ops/types.d.ts
|
|
@@ -82,4 +82,4 @@ interface ReconcilePlan extends OpPlan<ReconcileResult> {
|
|
|
82
82
|
declare function planReconcile(input: ReconcileInput): Promise<ReconcilePlan>;
|
|
83
83
|
//#endregion
|
|
84
84
|
export { ContentSaveEntryResult as a, ReconcileResult as i, ReconcileInput as n, ContentSavePlan as o, ReconcilePlan as r, OpPlan as s, planReconcile as t };
|
|
85
|
-
//# sourceMappingURL=index-
|
|
85
|
+
//# sourceMappingURL=index-BHpAYYFF.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index-
|
|
1
|
+
{"version":3,"file":"index-BHpAYYFF.d.mts","names":[],"sources":["../src/core/ops/types.ts","../src/core/ops/reconcile/types.ts","../src/core/ops/reconcile/plan.ts"],"mappings":";;;;;;;AAUA;;;;;UAAiB,MAAA;EAEN;EAAT,OAAA,EAAS,UAAA;EAED;EAAR,MAAA,EAAQ,OAAA;EAEE;EAAV,UAAA;AAAA;AAAA,UAGe,sBAAA;EACf,MAAA;EACA,EAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;AAAA;AAAA,KAGU,eAAA,GAAkB,MAAA,CAAO,sBAAA;;;AAjBrC;;;;;;;;AAAA,UCGiB,cAAA;EACf,IAAA,EAAM,UAAA;EACN,IAAA,EAAM,UAAA;EACN,MAAA,EAAQ,UAAA;EDGO;ECDf,WAAA,GAAc,kBAAA;;EAEd,MAAA,GAAS,aAAA;AAAA;AAAA,UAGM,eAAA;EDDf;ECGA,YAAA;EDDA;ECGA,kBAAA;EACA,oBAAA;EDDU;ECGV,oBAAA;;EAEA,WAAA;AAAA;;;;AAnBF;;;UA4BiB,aAAA,SAAsB,MAAA,CAAO,eAAA;EAC5C,SAAA,EAAW,YAAA;AAAA;;;;;;ADhCb;;;;;;;;;iBEkDsB,aAAA,CAAc,KAAA,EAAO,cAAA,GAAiB,OAAA,CAAQ,aAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { ApplyPlanInput as ApplyPlanInput$1, Branch, Commit as Commit$1, CommitAuthor, ContentReadSource, FileChange, FileDiff, LOCAL_CAPABILITIES, MediaAsset, MediaIngestInput, MediaListOptions, MediaListResult, MediaProvider, MediaUpdateInput, MergeResult, ProviderCapabilities as ProviderCapabilities$1, RepoProvider as RepoProvider$1, RepoReader, RepoWriter as RepoWriter$1, WriteReadiness } from "@contentrain/types";
|
|
2
|
+
export { RepoProvider$1 as _, ContentReadSource as a, WriteReadiness as b, LOCAL_CAPABILITIES as c, MediaListOptions as d, MediaListResult as f, ProviderCapabilities$1 as g, MergeResult as h, CommitAuthor as i, MediaAsset as l, MediaUpdateInput as m, Branch as n, FileChange as o, MediaProvider as p, Commit$1 as r, FileDiff as s, ApplyPlanInput$1 as t, MediaIngestInput as u, RepoReader as v, RepoWriter$1 as y };
|
package/dist/index.mjs
CHANGED
|
@@ -13,10 +13,10 @@ import "./base-branch-Cw9DWvCC.mjs";
|
|
|
13
13
|
import "./branch-lifecycle-CcsSyfSw.mjs";
|
|
14
14
|
import "./id-jrHWdtqn.mjs";
|
|
15
15
|
import "./transaction-DfrvGIfk.mjs";
|
|
16
|
-
import "./local-
|
|
16
|
+
import "./local-BXTVAAr-.mjs";
|
|
17
17
|
import "./detect-B1QHYA0u.mjs";
|
|
18
18
|
import "./annotations-BYkjoj2d.mjs";
|
|
19
|
-
import { n as createServer } from "./server-
|
|
19
|
+
import { n as createServer } from "./server-Sedx3tOQ.mjs";
|
|
20
20
|
import "./errors-De8Noahk.mjs";
|
|
21
21
|
import "./reconcile-BWbckvbv.mjs";
|
|
22
22
|
import "./scan-config-S5fkzNhL.mjs";
|
|
@@ -3,15 +3,17 @@ import { n as applyChangesToWorktree } from "./reconcile-CPDGFQ8H.mjs";
|
|
|
3
3
|
import { o as canonicalStringify$1, s as sortKeys$1 } from "./context-fJCKH_qv.mjs";
|
|
4
4
|
import { k as LocalReader } from "./model-manager-DPXulg7z.mjs";
|
|
5
5
|
import { t as readConfig } from "./config-BLwZtwlI.mjs";
|
|
6
|
-
import { r as createGit } from "./identity-CxAB9Qb8.mjs";
|
|
6
|
+
import { i as gitBinary, r as createGit } from "./identity-CxAB9Qb8.mjs";
|
|
7
7
|
import { t as resolveBaseBranch } from "./base-branch-Cw9DWvCC.mjs";
|
|
8
8
|
import { n as checkBranchHealth, o as deleteRemoteBranch, r as classifyMergedBranches } from "./branch-lifecycle-CcsSyfSw.mjs";
|
|
9
9
|
import { o as mergeBranch$1, r as createTransaction } from "./transaction-DfrvGIfk.mjs";
|
|
10
|
-
import { join } from "node:path";
|
|
11
|
-
import { readFile } from "node:fs/promises";
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
|
+
import { readFile, stat } from "node:fs/promises";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
import { CONTENTRAIN_BRANCH, MODEL_FIELD_ORDER } from "@contentrain/types";
|
|
15
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
15
17
|
//#region src/providers/local/branch-ops.ts
|
|
16
18
|
/**
|
|
17
19
|
* Branch/merge/diff helpers backed by the local simple-git worktree flow.
|
|
@@ -92,6 +94,274 @@ async function isMerged(projectRoot, branch, into) {
|
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
//#endregion
|
|
97
|
+
//#region src/git/ref-snapshot.ts
|
|
98
|
+
/**
|
|
99
|
+
* RefSnapshotReader — a `RepoReader` over one subtree of one commit, read
|
|
100
|
+
* with two git spawns in total however many files the caller touches.
|
|
101
|
+
*
|
|
102
|
+
* `GitRefReader` spawns `git show` per file, which is right for a planner
|
|
103
|
+
* reading a handful of paths and wrong for a tool that walks every model and
|
|
104
|
+
* every locale file (`content_list`, `status`, validation): a few hundred
|
|
105
|
+
* spawns per call (#229). Here one `ls-tree -r` lists the subtree — which
|
|
106
|
+
* answers `listDirectory` and `fileExists` from memory — and the first
|
|
107
|
+
* `readFile` pulls every content blob under it through a single
|
|
108
|
+
* `cat-file --batch`. Only content is batched ({@link batched}): media under
|
|
109
|
+
* `assets_path`, and any large or non-text file, is read on demand, one
|
|
110
|
+
* `cat-file blob` each, so a list or a save never loads the project's images.
|
|
111
|
+
*
|
|
112
|
+
* Bound to a commit sha, not a branch name, so a snapshot never changes
|
|
113
|
+
* under its reader; {@link loadRefSnapshot} caches one per (repo, sha).
|
|
114
|
+
*
|
|
115
|
+
* Paths are repo-relative with `/` separators; the caller maps its own paths
|
|
116
|
+
* (see LocalProvider). Only paths under `root` are in the snapshot — anything
|
|
117
|
+
* else reads as missing.
|
|
118
|
+
*/
|
|
119
|
+
var RefSnapshotReader = class {
|
|
120
|
+
blobs = null;
|
|
121
|
+
single = /* @__PURE__ */ new Map();
|
|
122
|
+
constructor(repoDir, commit, root, files, dirs) {
|
|
123
|
+
this.repoDir = repoDir;
|
|
124
|
+
this.commit = commit;
|
|
125
|
+
this.root = root;
|
|
126
|
+
this.files = files;
|
|
127
|
+
this.dirs = dirs;
|
|
128
|
+
}
|
|
129
|
+
async readFile(path, _ref) {
|
|
130
|
+
const p = trim(path);
|
|
131
|
+
const file = this.files.get(p);
|
|
132
|
+
if (!file) throw Object.assign(/* @__PURE__ */ new Error(`ENOENT: ${path} is not in ${this.commit.slice(0, 8)}:${this.root}`), { code: "ENOENT" });
|
|
133
|
+
if (!batched(p, file.size)) {
|
|
134
|
+
let text = this.single.get(file.oid);
|
|
135
|
+
if (!text) {
|
|
136
|
+
text = runGit(this.repoDir, [
|
|
137
|
+
"cat-file",
|
|
138
|
+
"blob",
|
|
139
|
+
file.oid
|
|
140
|
+
]).then((buf) => buf.toString("utf-8"));
|
|
141
|
+
text.catch(() => this.single.delete(file.oid));
|
|
142
|
+
this.single.set(file.oid, text);
|
|
143
|
+
}
|
|
144
|
+
return text;
|
|
145
|
+
}
|
|
146
|
+
this.blobs ??= readBlobs(this.repoDir, [...new Set([...this.files].filter(([f, { size }]) => batched(f, size)).map(([, { oid }]) => oid))]);
|
|
147
|
+
const text = (await this.blobs).get(file.oid);
|
|
148
|
+
if (text === void 0) throw new Error(`blob ${file.oid} for ${path} could not be read`);
|
|
149
|
+
return text;
|
|
150
|
+
}
|
|
151
|
+
async listDirectory(path, _ref) {
|
|
152
|
+
return [...this.dirs.get(trim(path)) ?? []];
|
|
153
|
+
}
|
|
154
|
+
async fileExists(path, _ref) {
|
|
155
|
+
const p = trim(path);
|
|
156
|
+
return this.files.has(p) || this.dirs.has(p);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
/** Largest blob the batch loads; a content file is far smaller. */
|
|
160
|
+
const BATCH_MAX_BYTES = 1024 * 1024;
|
|
161
|
+
/**
|
|
162
|
+
* Whether the first `readFile` loads this file with the rest: a content file —
|
|
163
|
+
* JSON (config, models, content, meta) or Markdown (documents) — no larger
|
|
164
|
+
* than {@link BATCH_MAX_BYTES}. Media never matches by extension, wherever
|
|
165
|
+
* `assets_path` points.
|
|
166
|
+
*/
|
|
167
|
+
function batched(path, size) {
|
|
168
|
+
return size <= BATCH_MAX_BYTES && /\.(?:json|md|mdx)$/i.test(path);
|
|
169
|
+
}
|
|
170
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
171
|
+
const MAX_SNAPSHOTS = 8;
|
|
172
|
+
/**
|
|
173
|
+
* The snapshot of `root` at `commit`, listed once per (repo, commit) for the
|
|
174
|
+
* life of the process: a commit's tree never changes, so a later tool call on
|
|
175
|
+
* an unmoved branch costs no spawn at all.
|
|
176
|
+
*/
|
|
177
|
+
function loadRefSnapshot(repoDir, commit, root) {
|
|
178
|
+
const key = `${repoDir}\0${commit}\0${root}`;
|
|
179
|
+
let hit = snapshots.get(key);
|
|
180
|
+
if (!hit) {
|
|
181
|
+
hit = listTree(repoDir, commit, root);
|
|
182
|
+
hit.catch(() => snapshots.delete(key));
|
|
183
|
+
snapshots.set(key, hit);
|
|
184
|
+
if (snapshots.size > MAX_SNAPSHOTS) snapshots.delete(snapshots.keys().next().value);
|
|
185
|
+
}
|
|
186
|
+
return hit;
|
|
187
|
+
}
|
|
188
|
+
async function listTree(repoDir, commit, root) {
|
|
189
|
+
const raw = await runGit(repoDir, [
|
|
190
|
+
"ls-tree",
|
|
191
|
+
"-r",
|
|
192
|
+
"-l",
|
|
193
|
+
"-z",
|
|
194
|
+
"--full-tree",
|
|
195
|
+
commit,
|
|
196
|
+
"--",
|
|
197
|
+
root
|
|
198
|
+
]);
|
|
199
|
+
const files = /* @__PURE__ */ new Map();
|
|
200
|
+
const children = /* @__PURE__ */ new Map();
|
|
201
|
+
for (const record of raw.toString("utf-8").split("\0")) {
|
|
202
|
+
if (!record) continue;
|
|
203
|
+
const tab = record.indexOf(" ");
|
|
204
|
+
const [, type, oid, size] = record.slice(0, tab).split(/ +/);
|
|
205
|
+
if (type !== "blob" || !oid) continue;
|
|
206
|
+
const path = record.slice(tab + 1);
|
|
207
|
+
files.set(path, {
|
|
208
|
+
oid,
|
|
209
|
+
size: Number(size)
|
|
210
|
+
});
|
|
211
|
+
const parts = path.split("/");
|
|
212
|
+
for (let i = 0; i < parts.length; i++) {
|
|
213
|
+
const dir = parts.slice(0, i).join("/");
|
|
214
|
+
let set = children.get(dir);
|
|
215
|
+
if (!set) children.set(dir, set = /* @__PURE__ */ new Set());
|
|
216
|
+
set.add(parts[i]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return new RefSnapshotReader(repoDir, commit, root, files, new Map([...children].map(([dir, names]) => [dir, [...names].toSorted()])));
|
|
220
|
+
}
|
|
221
|
+
/** The given blobs in one `cat-file --batch` process. */
|
|
222
|
+
async function readBlobs(repoDir, oids) {
|
|
223
|
+
const out = /* @__PURE__ */ new Map();
|
|
224
|
+
if (oids.length === 0) return out;
|
|
225
|
+
const buf = await runGit(repoDir, ["cat-file", "--batch"], `${oids.join("\n")}\n`);
|
|
226
|
+
let at = 0;
|
|
227
|
+
while (at < buf.length) {
|
|
228
|
+
const eol = buf.indexOf(10, at);
|
|
229
|
+
if (eol < 0) break;
|
|
230
|
+
const [oid, , size] = buf.subarray(at, eol).toString("utf-8").split(" ");
|
|
231
|
+
const bytes = Number(size);
|
|
232
|
+
if (!oid || !Number.isFinite(bytes)) break;
|
|
233
|
+
out.set(oid, buf.subarray(eol + 1, eol + 1 + bytes).toString("utf-8"));
|
|
234
|
+
at = eol + 1 + bytes + 1;
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
function runGit(cwd, args, stdin) {
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
const child = spawn(gitBinary(), args, {
|
|
241
|
+
cwd,
|
|
242
|
+
stdio: [
|
|
243
|
+
"pipe",
|
|
244
|
+
"pipe",
|
|
245
|
+
"pipe"
|
|
246
|
+
]
|
|
247
|
+
});
|
|
248
|
+
const chunks = [];
|
|
249
|
+
let stderr = "";
|
|
250
|
+
child.stdout.on("data", (c) => chunks.push(c));
|
|
251
|
+
child.stderr.on("data", (c) => {
|
|
252
|
+
stderr += c.toString();
|
|
253
|
+
});
|
|
254
|
+
child.on("error", reject);
|
|
255
|
+
child.stdin.on("error", () => {});
|
|
256
|
+
child.on("close", (code) => {
|
|
257
|
+
if (code === 0) resolve(Buffer.concat(chunks));
|
|
258
|
+
else reject(/* @__PURE__ */ new Error(`git ${args[0]} exited ${code}: ${stderr.trim()}`));
|
|
259
|
+
});
|
|
260
|
+
child.stdin.end(stdin ?? "");
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function trim(path) {
|
|
264
|
+
let p = path.replaceAll("\\", "/");
|
|
265
|
+
while (p.startsWith("./")) p = p.slice(2);
|
|
266
|
+
while (p.endsWith("/")) p = p.slice(0, -1);
|
|
267
|
+
return p === "." ? "" : p;
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/providers/local/content-view.ts
|
|
271
|
+
/** The `.contentrain` paths that are never committed: always the working tree. */
|
|
272
|
+
const UNTRACKED = [".contentrain/.cache", ".contentrain/client"];
|
|
273
|
+
const scope = new AsyncLocalStorage();
|
|
274
|
+
/**
|
|
275
|
+
* Run `fn` with one content view per project for its whole duration: every
|
|
276
|
+
* read inside answers from the same snapshot, resolved once. The MCP server
|
|
277
|
+
* wraps each tool call in this, so a call costs a fixed handful of spawns
|
|
278
|
+
* however many files it reads.
|
|
279
|
+
*/
|
|
280
|
+
function withReadScope(fn) {
|
|
281
|
+
return scope.run(/* @__PURE__ */ new Map(), fn);
|
|
282
|
+
}
|
|
283
|
+
function contentView(projectRoot) {
|
|
284
|
+
const memo = scope.getStore();
|
|
285
|
+
if (!memo) return resolveView(projectRoot);
|
|
286
|
+
let view = memo.get(projectRoot);
|
|
287
|
+
if (!view) {
|
|
288
|
+
view = resolveView(projectRoot);
|
|
289
|
+
memo.set(projectRoot, view);
|
|
290
|
+
}
|
|
291
|
+
return view;
|
|
292
|
+
}
|
|
293
|
+
function describeSource(view) {
|
|
294
|
+
return view.source === "working_tree" ? { source: "working_tree" } : {
|
|
295
|
+
source: "ref",
|
|
296
|
+
ref: view.ref,
|
|
297
|
+
commit: view.commit,
|
|
298
|
+
checked_out: view.branch
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The repo-relative path a snapshot answers for, or null when `path` must come
|
|
303
|
+
* from the working tree (outside `.contentrain/`, or never committed).
|
|
304
|
+
*/
|
|
305
|
+
function refPath(view, projectRoot, path) {
|
|
306
|
+
const abs = resolve(projectRoot, path);
|
|
307
|
+
const root = resolve(projectRoot);
|
|
308
|
+
if (abs !== root && !abs.startsWith(`${root}/`) && !abs.startsWith(`${root}\\`)) return null;
|
|
309
|
+
const rel = abs.slice(root.length + 1).replaceAll("\\", "/");
|
|
310
|
+
if (rel !== ".contentrain" && !rel.startsWith(".contentrain/")) return null;
|
|
311
|
+
if (UNTRACKED.some((p) => rel === p || rel.startsWith(`${p}/`))) return null;
|
|
312
|
+
return view.prefix + rel;
|
|
313
|
+
}
|
|
314
|
+
async function resolveView(projectRoot) {
|
|
315
|
+
try {
|
|
316
|
+
const head = await headBranch(projectRoot);
|
|
317
|
+
if (!head || head === CONTENTRAIN_BRANCH || head.startsWith("cr/")) return { source: "working_tree" };
|
|
318
|
+
const git = createGit(projectRoot);
|
|
319
|
+
if (head === await resolveBaseBranch(git, await readConfig(projectRoot), { currentBranch: head })) return { source: "working_tree" };
|
|
320
|
+
const [prefix, commit] = (await git.raw([
|
|
321
|
+
"rev-parse",
|
|
322
|
+
"--show-prefix",
|
|
323
|
+
`refs/heads/${CONTENTRAIN_BRANCH}^{commit}`
|
|
324
|
+
])).split("\n").map((line) => line.trim());
|
|
325
|
+
if (!commit) return { source: "working_tree" };
|
|
326
|
+
return {
|
|
327
|
+
source: "ref",
|
|
328
|
+
ref: CONTENTRAIN_BRANCH,
|
|
329
|
+
commit,
|
|
330
|
+
branch: head,
|
|
331
|
+
reader: await loadRefSnapshot(projectRoot, commit, `${prefix ?? ""}.contentrain`),
|
|
332
|
+
prefix: prefix ?? ""
|
|
333
|
+
};
|
|
334
|
+
} catch {
|
|
335
|
+
return { source: "working_tree" };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* The checked-out branch, or null when HEAD is detached. Read from the HEAD
|
|
340
|
+
* file when `.git` sits at the project root (no spawn — this runs once per
|
|
341
|
+
* tool call on every local project); asked of git otherwise (a linked
|
|
342
|
+
* worktree, or a project in a monorepo subdirectory).
|
|
343
|
+
*/
|
|
344
|
+
async function headBranch(projectRoot) {
|
|
345
|
+
const dotGit = join(projectRoot, ".git");
|
|
346
|
+
try {
|
|
347
|
+
let gitDir = dotGit;
|
|
348
|
+
if ((await stat(dotGit)).isFile()) {
|
|
349
|
+
const pointer = (await readFile(dotGit, "utf-8")).match(/^gitdir:\s*(.+)$/m)?.[1]?.trim();
|
|
350
|
+
if (!pointer) throw new Error("unreadable .git file");
|
|
351
|
+
gitDir = resolve(projectRoot, pointer);
|
|
352
|
+
}
|
|
353
|
+
const head = (await readFile(join(gitDir, "HEAD"), "utf-8")).trim();
|
|
354
|
+
return head.startsWith("ref: refs/heads/") ? head.slice(16) : null;
|
|
355
|
+
} catch {
|
|
356
|
+
return (await createGit(projectRoot).raw([
|
|
357
|
+
"symbolic-ref",
|
|
358
|
+
"-q",
|
|
359
|
+
"--short",
|
|
360
|
+
"HEAD"
|
|
361
|
+
]).catch(() => "")).trim() || null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
//#endregion
|
|
95
365
|
//#region src/providers/local/rebase-changes.ts
|
|
96
366
|
function rebaseChange(change, basis, current) {
|
|
97
367
|
if (current === basis || current === change.content) return {
|
|
@@ -221,14 +491,29 @@ var LocalProvider = class {
|
|
|
221
491
|
this.projectRoot = projectRoot;
|
|
222
492
|
this.reader = new LocalReader(projectRoot);
|
|
223
493
|
}
|
|
224
|
-
readFile(path, ref) {
|
|
225
|
-
|
|
494
|
+
async readFile(path, ref) {
|
|
495
|
+
const at = await this.routed(path);
|
|
496
|
+
return at ? at.view.reader.readFile(at.path) : this.reader.readFile(path, ref);
|
|
497
|
+
}
|
|
498
|
+
async listDirectory(path, ref) {
|
|
499
|
+
const at = await this.routed(path);
|
|
500
|
+
return at ? at.view.reader.listDirectory(at.path) : this.reader.listDirectory(path, ref);
|
|
226
501
|
}
|
|
227
|
-
|
|
228
|
-
|
|
502
|
+
async fileExists(path, ref) {
|
|
503
|
+
const at = await this.routed(path);
|
|
504
|
+
return at ? at.view.reader.fileExists(at.path) : this.reader.fileExists(path, ref);
|
|
229
505
|
}
|
|
230
|
-
|
|
231
|
-
return this.
|
|
506
|
+
async contentSource() {
|
|
507
|
+
return describeSource(await contentView(this.projectRoot));
|
|
508
|
+
}
|
|
509
|
+
async routed(path) {
|
|
510
|
+
const view = await contentView(this.projectRoot);
|
|
511
|
+
if (view.source !== "ref") return null;
|
|
512
|
+
const inRef = refPath(view, this.projectRoot, path);
|
|
513
|
+
return inRef === null ? null : {
|
|
514
|
+
view,
|
|
515
|
+
path: inRef
|
|
516
|
+
};
|
|
232
517
|
}
|
|
233
518
|
/**
|
|
234
519
|
* Branch pressure gate. Unmerged `cr/*` branches accumulate in a local repo
|
|
@@ -243,14 +528,15 @@ var LocalProvider = class {
|
|
|
243
528
|
} : { blocked: false };
|
|
244
529
|
}
|
|
245
530
|
async applyPlan(input) {
|
|
246
|
-
const
|
|
531
|
+
const view = await contentView(this.projectRoot);
|
|
532
|
+
const basis = await Promise.all(input.changes.map((c) => this.readBasis(view, c.path)));
|
|
247
533
|
const tx = await createTransaction(this.projectRoot, input.branch, { workflowOverride: input.workflowOverride });
|
|
248
534
|
try {
|
|
249
535
|
await tx.write(async (wt) => {
|
|
250
536
|
const current = await Promise.all(input.changes.map((c) => readOrNull(join(wt, c.path))));
|
|
251
537
|
const results = input.changes.map((change, i) => rebaseChange(change, basis[i], current[i]));
|
|
252
538
|
const conflicts = results.filter((r) => !r.ok);
|
|
253
|
-
if (conflicts.length > 0) throw Object.assign(/* @__PURE__ */ new Error(`Nothing was written: the content branch moved since your working tree was last updated, and this change cannot be applied on top of it. ` + conflicts.map((c) => `${c.path}: ${c.reason}.`).join(" ")), staleTreeGuidance(tx.baseBranch, tx.baseCheckedOut));
|
|
539
|
+
if (conflicts.length > 0) throw Object.assign(/* @__PURE__ */ new Error(`Nothing was written: the content branch moved since your working tree was last updated, and this change cannot be applied on top of it. ` + conflicts.map((c) => `${c.path}: ${c.reason}.`).join(" ")), staleTreeGuidance(tx.baseBranch, tx.baseCheckedOut, view.source === "ref"));
|
|
254
540
|
await applyChangesToWorktree(wt, results.map((r) => r.change));
|
|
255
541
|
});
|
|
256
542
|
await tx.commit(input.message, input.context);
|
|
@@ -273,6 +559,13 @@ var LocalProvider = class {
|
|
|
273
559
|
await tx.cleanup();
|
|
274
560
|
}
|
|
275
561
|
}
|
|
562
|
+
async readBasis(view, path) {
|
|
563
|
+
if (view.source === "ref") {
|
|
564
|
+
const inRef = refPath(view, this.projectRoot, path);
|
|
565
|
+
if (inRef !== null) return view.reader.readFile(inRef).catch(() => null);
|
|
566
|
+
}
|
|
567
|
+
return readOrNull(join(this.projectRoot, path));
|
|
568
|
+
}
|
|
276
569
|
listBranches(prefix) {
|
|
277
570
|
return listBranches(this.projectRoot, prefix);
|
|
278
571
|
}
|
|
@@ -381,7 +674,11 @@ async function readOrNull(path) {
|
|
|
381
674
|
* advance leaves a non-base checkout alone), so a pull fetches nothing
|
|
382
675
|
* useful — merging the base is what brings it in.
|
|
383
676
|
*/
|
|
384
|
-
function staleTreeGuidance(baseBranch, baseCheckedOut) {
|
|
677
|
+
function staleTreeGuidance(baseBranch, baseCheckedOut, readFromRef) {
|
|
678
|
+
if (readFromRef) return {
|
|
679
|
+
code: "CONTENT_WORKING_TREE_STALE",
|
|
680
|
+
agent_hint: `Another writer (Studio, a teammate, CI) changed the same value on the contentrain branch after this write read it. Re-read the content and retry — do not retry unchanged. No git action is needed.`
|
|
681
|
+
};
|
|
385
682
|
return {
|
|
386
683
|
code: "CONTENT_WORKING_TREE_STALE",
|
|
387
684
|
agent_hint: `The content on the contentrain branch differs from the working tree this write was planned from — another writer (Studio, a teammate, CI) or an earlier write changed the same value. ${baseCheckedOut ? "Ask the developer to update their working tree (git pull)" : `The developer is on a branch other than "${baseBranch}", which content writes do not update. Ask them to merge "${baseBranch}" into it (or rebase onto it)`}, then re-read the content and retry — do not retry unchanged.`,
|
|
@@ -389,6 +686,6 @@ function staleTreeGuidance(baseBranch, baseCheckedOut) {
|
|
|
389
686
|
};
|
|
390
687
|
}
|
|
391
688
|
//#endregion
|
|
392
|
-
export {
|
|
689
|
+
export { withReadScope as n, isMerged as r, LocalProvider as t };
|
|
393
690
|
|
|
394
|
-
//# sourceMappingURL=local-
|
|
691
|
+
//# sourceMappingURL=local-BXTVAAr-.mjs.map
|