@contentrain/mcp 3.7.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +1 -0
  2. package/dist/{content-manager-wKsAkhly.d.mts → content-manager-2g0pkLFh.d.mts} +2 -2
  3. package/dist/{content-manager-wKsAkhly.d.mts.map → content-manager-2g0pkLFh.d.mts.map} +1 -1
  4. package/dist/core/config.d.mts +1 -1
  5. package/dist/core/content-manager.d.mts +1 -1
  6. package/dist/core/context.d.mts +1 -1
  7. package/dist/core/contracts/index.d.mts +2 -2
  8. package/dist/core/model-manager.d.mts +1 -1
  9. package/dist/core/ops/index.d.mts +3 -3
  10. package/dist/core/overlay-reader.d.mts +1 -1
  11. package/dist/core/validator/index.d.mts +1 -1
  12. package/dist/git/reconcile.d.mts +1 -1
  13. package/dist/{index-CkhXTFdk.d.mts → index-BHpAYYFF.d.mts} +2 -2
  14. package/dist/{index-CkhXTFdk.d.mts.map → index-BHpAYYFF.d.mts.map} +1 -1
  15. package/dist/index-v0N9d4GT.d.mts +2 -0
  16. package/dist/index.mjs +2 -2
  17. package/dist/{local-BySg4Ib-.mjs → local-BXTVAAr-.mjs} +311 -14
  18. package/dist/local-BXTVAAr-.mjs.map +1 -0
  19. package/dist/providers/github/index.d.mts +1 -1
  20. package/dist/providers/gitlab/index.d.mts +1 -1
  21. package/dist/providers/local/index.d.mts +4 -1
  22. package/dist/providers/local/index.d.mts.map +1 -1
  23. package/dist/providers/local/index.mjs +1 -1
  24. package/dist/server/http/index.d.mts +1 -1
  25. package/dist/server/http/index.mjs +2 -2
  26. package/dist/{server-D1c-79hA.d.mts → server-24UHN9Ll.d.mts} +2 -2
  27. package/dist/server-24UHN9Ll.d.mts.map +1 -0
  28. package/dist/{server-BdGsBjPT.mjs → server-Cl3LMiU3.mjs} +54 -31
  29. package/dist/server-Cl3LMiU3.mjs.map +1 -0
  30. package/dist/server.d.mts +1 -1
  31. package/dist/server.mjs +2 -2
  32. package/dist/testing/memory-provider.d.mts +1 -1
  33. package/package.json +2 -2
  34. package/dist/index-PCQQBpIH.d.mts +0 -2
  35. package/dist/local-BySg4Ib-.mjs.map +0 -1
  36. package/dist/server-BdGsBjPT.mjs.map +0 -1
  37. 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 { _ as RepoReader } from "./index-PCQQBpIH.mjs";
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-wKsAkhly.d.mts.map
76
+ //# sourceMappingURL=content-manager-2g0pkLFh.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"content-manager-wKsAkhly.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"}
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"}
@@ -1,4 +1,4 @@
1
- import { _ as RepoReader } from "../index-PCQQBpIH.mjs";
1
+ import { v as RepoReader } from "../index-v0N9d4GT.mjs";
2
2
  import { ContentrainConfig, Vocabulary } from "@contentrain/types";
3
3
 
4
4
  //#region src/core/config.d.ts
@@ -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-wKsAkhly.mjs";
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 };
@@ -1,4 +1,4 @@
1
- import { _ as RepoReader, a as FileChange } from "../index-PCQQBpIH.mjs";
1
+ import { o as FileChange, v as RepoReader } from "../index-v0N9d4GT.mjs";
2
2
  import { ContextJson, ContextSource } from "@contentrain/types";
3
3
 
4
4
  //#region src/core/context.d.ts
@@ -1,2 +1,2 @@
1
- import { _ as RepoReader, a as FileChange, c as MediaAsset, d as MediaListResult, f as MediaProvider, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, l as MediaIngestInput, m as MergeResult, n as Branch, o as FileDiff, p as MediaUpdateInput, r as Commit, s as LOCAL_CAPABILITIES, t as ApplyPlanInput, u as MediaListOptions, v as RepoWriter, y as WriteReadiness } from "../../index-PCQQBpIH.mjs";
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,4 +1,4 @@
1
- import { _ as RepoReader } from "../index-PCQQBpIH.mjs";
1
+ import { v as RepoReader } from "../index-v0N9d4GT.mjs";
2
2
  import { MODEL_FIELD_ORDER, ModelDefinition, ModelSummary, ModelSummary as ModelSummary$1 } from "@contentrain/types";
3
3
  import { z } from "zod";
4
4
 
@@ -1,6 +1,6 @@
1
- import { _ as RepoReader, a as FileChange } from "../../index-PCQQBpIH.mjs";
2
- import { t as ContentEntry } from "../../content-manager-wKsAkhly.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-CkhXTFdk.mjs";
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 { _ as RepoReader, a as FileChange } from "../index-PCQQBpIH.mjs";
1
+ import { o as FileChange, v as RepoReader } from "../index-v0N9d4GT.mjs";
2
2
 
3
3
  //#region src/core/overlay-reader.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { _ as RepoReader } from "../../index-PCQQBpIH.mjs";
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
@@ -1,4 +1,4 @@
1
- import { r as ReconcilePlan } from "../index-CkhXTFdk.mjs";
1
+ import { r as ReconcilePlan } from "../index-BHpAYYFF.mjs";
2
2
  import { BaseAdvance, ConflictResolution, ContextSource, RemotePush, SyncResult } from "@contentrain/types";
3
3
 
4
4
  //#region src/git/reconcile.d.ts
@@ -1,4 +1,4 @@
1
- import { _ as RepoReader, a as FileChange } from "./index-PCQQBpIH.mjs";
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-CkhXTFdk.d.mts.map
85
+ //# sourceMappingURL=index-BHpAYYFF.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index-CkhXTFdk.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"}
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-BySg4Ib-.mjs";
16
+ import "./local-BXTVAAr-.mjs";
17
17
  import "./detect-B1QHYA0u.mjs";
18
18
  import "./annotations-BYkjoj2d.mjs";
19
- import { n as createServer } from "./server-BdGsBjPT.mjs";
19
+ import { n as createServer } from "./server-Cl3LMiU3.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
- return this.reader.readFile(path, ref);
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
- listDirectory(path, ref) {
228
- return this.reader.listDirectory(path, ref);
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
- fileExists(path, ref) {
231
- return this.reader.fileExists(path, ref);
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 basis = await Promise.all(input.changes.map((c) => readOrNull(join(this.projectRoot, c.path))));
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 { isMerged as n, LocalProvider as t };
689
+ export { withReadScope as n, isMerged as r, LocalProvider as t };
393
690
 
394
- //# sourceMappingURL=local-BySg4Ib-.mjs.map
691
+ //# sourceMappingURL=local-BXTVAAr-.mjs.map