@contentrain/mcp 1.9.0 → 1.10.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.
Files changed (58) hide show
  1. package/README.md +10 -3
  2. package/dist/{annotations-DSzewhL4.mjs → annotations-D3tlsF38.mjs} +39 -4
  3. package/dist/annotations-D3tlsF38.mjs.map +1 -0
  4. package/dist/{apply-manager-BnPbPXFG.mjs → apply-manager-UXUTYFZn.mjs} +3 -3
  5. package/dist/{apply-manager-BnPbPXFG.mjs.map → apply-manager-UXUTYFZn.mjs.map} +1 -1
  6. package/dist/{availability-Bj9zOfEn.mjs → availability-sWWRz2W0.mjs} +8 -2
  7. package/dist/availability-sWWRz2W0.mjs.map +1 -0
  8. package/dist/branch-lifecycle-AwBJIheA.d.mts.map +1 -1
  9. package/dist/{branch-lifecycle-D7tXOE4f.mjs → branch-lifecycle-Dd84lx37.mjs} +61 -3
  10. package/dist/branch-lifecycle-Dd84lx37.mjs.map +1 -0
  11. package/dist/{content-manager-BExZy6Eu.d.mts → content-manager-DcmNwvmq.d.mts} +2 -2
  12. package/dist/{content-manager-BExZy6Eu.d.mts.map → content-manager-DcmNwvmq.d.mts.map} +1 -1
  13. package/dist/core/apply-manager.mjs +3 -3
  14. package/dist/core/config.d.mts +1 -1
  15. package/dist/core/content-manager.d.mts +1 -1
  16. package/dist/core/context.d.mts +1 -1
  17. package/dist/core/contracts/index.d.mts +2 -2
  18. package/dist/core/doctor.mjs +2 -2
  19. package/dist/core/model-manager.d.mts +1 -1
  20. package/dist/core/ops/index.d.mts +2 -2
  21. package/dist/core/overlay-reader.d.mts +1 -1
  22. package/dist/core/validator/index.d.mts +1 -1
  23. package/dist/{doctor-BTToHt0y.mjs → doctor-BwSJW1sS.mjs} +2 -2
  24. package/dist/{doctor-BTToHt0y.mjs.map → doctor-BwSJW1sS.mjs.map} +1 -1
  25. package/dist/git/branch-lifecycle.mjs +1 -1
  26. package/dist/git/transaction.d.mts.map +1 -1
  27. package/dist/git/transaction.mjs +2 -2
  28. package/dist/index-DDX-qYNw.d.mts +2 -0
  29. package/dist/index.mjs +7 -7
  30. package/dist/{local-BZEUWBoC.mjs → local-BIE23GuI.mjs} +3 -3
  31. package/dist/{local-BZEUWBoC.mjs.map → local-BIE23GuI.mjs.map} +1 -1
  32. package/dist/providers/github/index.d.mts +1 -1
  33. package/dist/providers/gitlab/index.d.mts +1 -1
  34. package/dist/providers/local/index.d.mts +1 -1
  35. package/dist/providers/local/index.mjs +3 -3
  36. package/dist/server/http/index.d.mts +1 -1
  37. package/dist/server/http/index.mjs +7 -7
  38. package/dist/{server-B6_fzRmK.mjs → server-BkDuFvVt.mjs} +139 -9
  39. package/dist/server-BkDuFvVt.mjs.map +1 -0
  40. package/dist/{server-B1KfvywF.d.mts → server-vA1tM7DA.d.mts} +2 -2
  41. package/dist/{server-B1KfvywF.d.mts.map → server-vA1tM7DA.d.mts.map} +1 -1
  42. package/dist/server.d.mts +1 -1
  43. package/dist/server.mjs +7 -7
  44. package/dist/tools/annotations.d.mts +3 -3
  45. package/dist/tools/annotations.d.mts.map +1 -1
  46. package/dist/tools/annotations.mjs +1 -1
  47. package/dist/tools/availability.d.mts +3 -0
  48. package/dist/tools/availability.d.mts.map +1 -1
  49. package/dist/tools/availability.mjs +1 -1
  50. package/dist/{transaction-qliKvBRL.mjs → transaction-DhVoGigz.mjs} +4 -22
  51. package/dist/transaction-DhVoGigz.mjs.map +1 -0
  52. package/package.json +4 -4
  53. package/dist/annotations-DSzewhL4.mjs.map +0 -1
  54. package/dist/availability-Bj9zOfEn.mjs.map +0 -1
  55. package/dist/branch-lifecycle-D7tXOE4f.mjs.map +0 -1
  56. package/dist/index-6yasXNLQ.d.mts +0 -2
  57. package/dist/server-B6_fzRmK.mjs.map +0 -1
  58. package/dist/transaction-qliKvBRL.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { t as readConfig } from "./config-DlFcTxFG.mjs";
2
2
  import { r as writeContext } from "./context-CYN__o3Q.mjs";
3
- import { a as deleteRemoteBranch } from "./branch-lifecycle-D7tXOE4f.mjs";
3
+ import { a as deleteRemoteBranch, l as authorConfig } from "./branch-lifecycle-Dd84lx37.mjs";
4
4
  import { t as branchTimestamp } from "./id-DV_T9Ic8.mjs";
5
5
  import { join } from "node:path";
6
6
  import { CONTENTRAIN_BRANCH } from "@contentrain/types";
@@ -79,24 +79,6 @@ async function ensureContentBranch(projectRoot) {
79
79
  ]);
80
80
  } catch {}
81
81
  }
82
- /**
83
- * Commit identity for worktree operations, supplied via environment instead
84
- * of two `git config` spawns per transaction. Git honors GIT_AUTHOR_* /
85
- * GIT_COMMITTER_* for both the sync merges and the feature-branch commit, so
86
- * a worktree git built with this env needs no `git config user.*` calls.
87
- * `process.env` is spread so PATH/HOME and any GIT_* already set survive.
88
- */
89
- function authorEnv() {
90
- const name = process.env["CONTENTRAIN_AUTHOR_NAME"] ?? "Contentrain";
91
- const email = process.env["CONTENTRAIN_AUTHOR_EMAIL"] ?? "ai@contentrain.io";
92
- return {
93
- ...process.env,
94
- GIT_AUTHOR_NAME: name,
95
- GIT_AUTHOR_EMAIL: email,
96
- GIT_COMMITTER_NAME: name,
97
- GIT_COMMITTER_EMAIL: email
98
- };
99
- }
100
82
  async function selectiveSync(projectRoot, _worktreePath, contentrainTip, _previousBaseRef, dirtyFilesBeforeUpdate) {
101
83
  const git = simpleGit(projectRoot);
102
84
  const synced = [];
@@ -222,7 +204,7 @@ async function createTransaction(projectRoot, branchName, options) {
222
204
  worktreePath,
223
205
  CONTENTRAIN_BRANCH
224
206
  ]);
225
- const wtGit = simpleGit(worktreePath).env(authorEnv());
207
+ const wtGit = simpleGit(worktreePath, { config: authorConfig() });
226
208
  try {
227
209
  await wtGit.merge([baseBranch, "--no-edit"]);
228
210
  } catch {
@@ -363,7 +345,7 @@ async function mergeBranch(projectRoot, branchName) {
363
345
  worktreePath,
364
346
  CONTENTRAIN_BRANCH
365
347
  ]);
366
- const wtGit = simpleGit(worktreePath).env(authorEnv());
348
+ const wtGit = simpleGit(worktreePath, { config: authorConfig() });
367
349
  try {
368
350
  try {
369
351
  await wtGit.merge([branchName, "--no-edit"]);
@@ -501,4 +483,4 @@ async function regenerateContextOnContentrain(wtGit, worktreePath, contextUpdate
501
483
  //#endregion
502
484
  export { mergeBranch as i, createTransaction as n, ensureContentBranch as r, buildBranchName as t };
503
485
 
504
- //# sourceMappingURL=transaction-qliKvBRL.mjs.map
486
+ //# sourceMappingURL=transaction-DhVoGigz.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction-DhVoGigz.mjs","names":["removeFile"],"sources":["../src/providers/local/migration.ts","../src/git/transaction.ts"],"sourcesContent":["import type { SimpleGit } from 'simple-git'\n\n/**\n * Migration: the first MCP release used `contentrain/*` feature branches.\n * Once we introduced the singleton `contentrain` branch (tracking the\n * committed content state), those old feature branches became a ref-\n * namespace conflict — git cannot hold both `contentrain` (a leaf ref)\n * and `contentrain/foo` (implying a directory) simultaneously.\n *\n * `migrateLegacyBranches` removes the old-prefix branches so the\n * singleton `contentrain` ref can be created. It is idempotent and\n * safe to call before every `ensureContentBranch` run.\n *\n * Strategy:\n * 1. Delete merged `contentrain/*` branches first (`-d`). Their commits\n * are already on the base branch via the old auto-merge flow.\n * 2. Force-delete whatever remains (`-D`). Any unmerged leftover is\n * from an abandoned or partially-committed legacy branch — content\n * on `main` always wins, and the singleton `contentrain` branch is\n * about to be created from `main`/`baseBranch` anyway.\n *\n * Returns the number of branches that were deleted. Callers may log it;\n * the git transaction layer does not need the count for correctness.\n */\nexport async function migrateLegacyBranches(\n git: SimpleGit,\n baseBranch: string,\n): Promise<number> {\n const branches = await git.branchLocal()\n const oldPrefixBranches = branches.all.filter(b => b.startsWith('contentrain/'))\n if (oldPrefixBranches.length === 0) return 0\n\n let deleted = 0\n\n // 1) Delete merged legacy branches first — the safe path.\n let mergedLegacy: string[] = []\n try {\n const mergedOutput = await git.raw(['branch', '--merged', baseBranch])\n mergedLegacy = mergedOutput.split('\\n')\n .map(b => b.trim().replace(/^\\*\\s*/, ''))\n .filter(b => b.startsWith('contentrain/'))\n } catch {\n // `branch --merged` fails before baseBranch exists — fall through.\n }\n\n for (const b of mergedLegacy) {\n try {\n await git.raw(['branch', '-d', b])\n deleted++\n } catch {\n // Branch may be protected or already gone — safe to skip.\n }\n }\n\n // 2) Force-delete any unmerged legacy branches still present.\n const remaining = (await git.branchLocal()).all.filter(b => b.startsWith('contentrain/'))\n for (const b of remaining) {\n try {\n await git.raw(['branch', '-D', b])\n deleted++\n } catch {\n // Skip — best-effort cleanup.\n }\n }\n\n return deleted\n}\n","import { simpleGit, type SimpleGit } from 'simple-git'\nimport { join } from 'node:path'\nimport { rm as removeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { readConfig } from '../core/config.js'\nimport { writeContext } from '../core/context.js'\nimport { deleteRemoteBranch, type RemoteDeleteResult } from './branch-lifecycle.js'\nimport { authorConfig } from './identity.js'\nimport { branchTimestamp } from '../util/id.js'\nimport { migrateLegacyBranches } from '../providers/local/migration.js'\nimport type { SyncResult, WorkflowMode } from '@contentrain/types'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\n\nexport interface ContextUpdate {\n tool: string\n model: string\n locale?: string\n entries?: string[]\n}\n\nexport interface GitTransaction {\n worktree: string\n branch: string\n write(callback: (worktreePath: string) => Promise<void>): Promise<void>\n commit(message: string, contextUpdate?: ContextUpdate): Promise<string>\n complete(): Promise<{ action: 'auto-merged' | 'pending-review'; commit: string; sync?: SyncResult; warning?: string }>\n cleanup(): Promise<void>\n}\n\nexport async function ensureContentBranch(projectRoot: string): Promise<void> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n\n // Check if contentrain branch exists locally\n const branches = await git.branchLocal()\n if (branches.all.includes(CONTENTRAIN_BRANCH)) return\n\n // Detect base branch\n const baseBranch = config?.repository?.default_branch\n || (await git.raw(['branch', '--show-current'])).trim()\n || 'main'\n\n // Clean up legacy `contentrain/*` feature branches so the singleton\n // `contentrain` ref can be created. Idempotent — safe to call even\n // when no legacy branches exist.\n await migrateLegacyBranches(git, baseBranch)\n\n // Create contentrain branch from base\n await git.branch([CONTENTRAIN_BRANCH, baseBranch])\n\n // Push to remote if exists\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n try {\n const remotes = await git.getRemotes()\n if (remotes.some(r => r.name === remoteName)) {\n await git.push(['-u', remoteName, CONTENTRAIN_BRANCH])\n }\n } catch {\n // Remote push is best-effort\n }\n}\n\nasync function selectiveSync(\n projectRoot: string,\n _worktreePath: string,\n contentrainTip: string,\n _previousBaseRef?: string,\n dirtyFilesBeforeUpdate?: Set<string>,\n): Promise<SyncResult> {\n const git = simpleGit(projectRoot)\n const synced: string[] = []\n const skipped: string[] = []\n\n // Use git plumbing to find ALL files that differ between old and new commits.\n // diff-tree is fast and ignores working tree / index state entirely.\n // Not limited to .contentrain/ — some ops also modify .gitignore, etc.\n const compareRef = _previousBaseRef ?? contentrainTip\n let changedFiles: string[] = []\n try {\n const diffOutput = await git.raw([\n 'diff-tree', '--name-only', '-r', '--no-commit-id',\n compareRef, contentrainTip,\n ])\n changedFiles = diffOutput.split('\\n').filter(f => f.trim().length > 0)\n } catch {\n // Fallback: list .contentrain/ files from the contentrainTip commit\n try {\n const lsOutput = await git.raw(['ls-tree', '-r', '--name-only', contentrainTip, '.contentrain/'])\n changedFiles = lsOutput.split('\\n').filter(f => f.trim().length > 0)\n } catch {\n return { synced, skipped }\n }\n }\n\n if (changedFiles.length === 0) return { synced, skipped }\n\n // Use pre-captured dirty files (before update-ref) to avoid false positives.\n // After update-ref, files appear as \"modified\" in status even though the developer\n // didn't touch them. We use the pre-update state to know what was truly dirty.\n const dirtyFiles = dirtyFilesBeforeUpdate ?? new Set<string>()\n\n // Which changed files still exist in contentrainTip (HEAD after update-ref)?\n // ONE `ls-tree` over the changed paths lists exactly the survivors, instead\n // of a `cat-file -e` spawn per file. Falls back to per-file probing if\n // ls-tree fails so behavior is preserved on any edge.\n const filesInTip = new Set<string>()\n try {\n const lsOutput = await git.raw(['ls-tree', '-r', '--name-only', contentrainTip, '--', ...changedFiles])\n for (const f of lsOutput.split('\\n')) {\n const trimmed = f.trim()\n if (trimmed) filesInTip.add(trimmed)\n }\n } catch {\n for (const file of changedFiles) {\n try {\n await git.raw(['cat-file', '-e', `${contentrainTip}:${file}`])\n filesInTip.add(file)\n } catch {\n // File does not exist in tip (was deleted)\n }\n }\n }\n\n // Partition: dirty developer files are skipped; survivors get checked out\n // from HEAD; the rest were deleted in the new HEAD and are removed on disk.\n const toCheckout: string[] = []\n const toRemove: string[] = []\n for (const file of changedFiles) {\n if (dirtyFiles.has(file)) skipped.push(file)\n else if (filesInTip.has(file)) toCheckout.push(file)\n else toRemove.push(file)\n }\n\n // ONE `git checkout HEAD -- f1 f2 …` restores every clean survivor at once.\n // On failure, fall back to per-file so a single unresolvable path still\n // yields precise skip accounting (dirty files were already excluded).\n if (toCheckout.length > 0) {\n try {\n await git.checkout(['HEAD', '--', ...toCheckout])\n synced.push(...toCheckout)\n } catch {\n for (const file of toCheckout) {\n try {\n await git.checkout(['HEAD', '--', file])\n synced.push(file)\n } catch {\n skipped.push(file)\n }\n }\n }\n }\n\n // Deletions are working-tree fs removals — no git spawn, safe to parallelize.\n await Promise.all(toRemove.map(async (file) => {\n try {\n await removeFile(join(projectRoot, file), { force: true })\n synced.push(file)\n } catch {\n skipped.push(file)\n }\n }))\n\n const warning = skipped.length > 0\n ? `${skipped.length} file(s) skipped due to local changes: ${skipped.join(', ')}. Commit your changes, then run: git checkout HEAD -- ${skipped.join(' ')}`\n : undefined\n\n return { synced, skipped, warning }\n}\n\nexport async function createTransaction(\n projectRoot: string,\n branchName: string,\n options?: { workflowOverride?: WorkflowMode },\n): Promise<GitTransaction> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n const workflow = options?.workflowOverride ?? config?.workflow ?? 'auto-merge'\n\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n // Detect base branch + current branch + remote in ONE batch\n // (reduces subprocess spawns from 4 to 2)\n let baseBranch = process.env['CONTENTRAIN_BRANCH'] ?? config?.repository?.default_branch ?? ''\n let currentBranch = ''\n let hasRemote = false\n\n const [branchResult, remotes] = await Promise.all([\n git.raw(['branch', '--show-current']).catch(() => ''),\n git.getRemotes().catch(() => []),\n ])\n currentBranch = branchResult.trim()\n if (!baseBranch) baseBranch = currentBranch || 'main'\n hasRemote = (remotes as { name: string }[]).some(r => r.name === remoteName)\n\n // Check if developer is on contentrain branch\n if (currentBranch === CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `The '${CONTENTRAIN_BRANCH}' branch is checked out in your working directory. `\n + `Contentrain manages this branch automatically. `\n + `Switch to your working branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_CHECKED_OUT',\n agent_hint: 'Ask the developer to switch to their working branch (e.g., main or a feature branch), then retry the operation.',\n developer_action: `git checkout ${baseBranch}`,\n })\n }\n\n // Ensure contentrain branch exists (with migration for old contentrain/* branches)\n await ensureContentBranch(projectRoot)\n\n // Fetch latest from remote (parallel fetch for both branches)\n if (hasRemote) {\n await Promise.all([\n git.fetch(remoteName, baseBranch).catch(() => {}),\n git.fetch(remoteName, CONTENTRAIN_BRANCH).catch(() => {}),\n ])\n }\n\n const worktreePath = join(tmpdir(), `cr-${randomUUID()}`)\n const branch = branchName\n\n // Create worktree on contentrain branch\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\n\n // Commit identity comes from `-c user.*` config (see authorConfig) — passed\n // as args, never via `.env()`, so simple-git's block-unsafe guard is never\n // triggered by an inherited EDITOR/GIT_ASKPASS/etc.\n const wtGit = simpleGit(worktreePath, { config: authorConfig() })\n\n // Sync contentrain with base branch (bring main changes into contentrain)\n try {\n await wtGit.merge([baseBranch, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n if (hasRemote) {\n try {\n await wtGit.merge([`${remoteName}/${baseBranch}`, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* ignore */ }\n }\n }\n }\n\n // Sync with remote contentrain if exists\n if (hasRemote) {\n try {\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* ignore */ }\n }\n }\n\n // Create feature branch from contentrain\n await wtGit.checkout(['-b', branch])\n\n let commitHash = ''\n let pendingReview = false\n let savedContextUpdate: ContextUpdate | undefined\n\n return {\n worktree: worktreePath,\n branch,\n\n async write(callback) {\n await callback(worktreePath)\n },\n\n async commit(message, contextUpdate?) {\n // context.json is intentionally NOT committed on the feature branch — it\n // is regenerated on the contentrain branch after the merge (see\n // complete()). Committing it per-branch caused cross-branch merge\n // conflicts on a single mutable file. `--no-verify` keeps the repo's\n // commit-msg / pre-commit hooks (commitlint, lefthook, husky) from\n // rejecting these machine-generated infra commits.\n savedContextUpdate = contextUpdate\n await wtGit.add('.')\n const result = await wtGit.commit(message, { '--allow-empty': null, '--no-verify': null })\n commitHash = result.commit || ''\n return commitHash\n },\n\n async complete() {\n if (workflow === 'review') {\n if (hasRemote) {\n await git.push(remoteName, branch)\n }\n // Pending-review branches must survive for a later contentrain_merge.\n pendingReview = true\n return { action: 'pending-review', commit: commitHash }\n }\n\n // auto-merge: merge feature branch into contentrain, then advance base\n\n // Switch to contentrain branch in worktree\n await wtGit.checkout(CONTENTRAIN_BRANCH)\n\n // Merge feature branch into contentrain\n try {\n await wtGit.merge([branch, '--no-edit'])\n } catch {\n try {\n await wtGit.merge(['--abort'])\n } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Merge conflict when merging branch \"${branch}\" into \"${CONTENTRAIN_BRANCH}\". `\n + `The branch still exists with your changes intact. `\n + `Resolve the conflict manually, or delete the branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_MERGE_CONFLICT',\n agent_hint: 'The feature branch could not be merged into the contentrain branch. Ask the developer to resolve the conflict.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${branch}`,\n })\n }\n\n // Regenerate context.json on the contentrain branch (post-merge,\n // single-threaded) and fold it into the tip before advancing the base.\n if (savedContextUpdate) {\n await regenerateContextOnContentrain(wtGit, worktreePath, savedContextUpdate)\n }\n\n // Get contentrain tip + old base ref + dirty files in parallel\n const [contentrainTip, previousBaseRef, statusBeforeUpdate] = await Promise.all([\n wtGit.raw(['rev-parse', 'HEAD']).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n git.status(),\n ])\n const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map(f => f.path))\n\n // Verify fast-forward: baseBranch must be an ancestor of contentrainTip\n // (guaranteed by the merge above, but verify for safety).\n // `rev-list --count` instead of `merge-base --is-ancestor`: the latter\n // signals via exit code with empty stderr, which simple-git reports as\n // success — the guard would silently pass on divergence.\n if (!(await isAncestor(git, previousBaseRef, contentrainTip))) {\n throw Object.assign(new Error(\n `Cannot fast-forward \"${baseBranch}\" to contentrain tip. `\n + `The base branch has diverged. Merge \"${baseBranch}\" into \"${CONTENTRAIN_BRANCH}\" first.`,\n ), {\n code: 'BASE_UPDATE_FAILED',\n agent_hint: `The base branch has commits not in contentrain. Merge ${baseBranch} into ${CONTENTRAIN_BRANCH} first.`,\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${baseBranch} && git checkout ${baseBranch}`,\n })\n }\n\n // Advance base branch to contentrain tip via update-ref\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n\n // Refresh index to match new HEAD.\n // update-ref moves the branch pointer but leaves the index stale.\n // read-tree updates the index to match HEAD without touching the working tree.\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n // fallback: try reset for older git versions\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n\n // Selective sync: copy .contentrain/ files to developer's working tree\n const sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate)\n\n // Push to remote (best-effort with retry)\n if (hasRemote) {\n // Push contentrain branch\n try {\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n // Retry: fetch, merge, push\n try {\n await wtGit.fetch(remoteName, CONTENTRAIN_BRANCH)\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n // Push failed after retry — continue, local state is fine\n }\n }\n\n // Push base branch\n try {\n await git.push(remoteName, baseBranch)\n } catch {\n // push may fail, local merge succeeded\n }\n }\n\n return {\n action: 'auto-merged' as const,\n commit: commitHash,\n sync,\n ...(sync.warning ? { warning: sync.warning } : {}),\n }\n },\n\n async cleanup() {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n // Prune the feature branch unless it is a pending-review branch that must\n // survive for a later contentrain_merge. Auto-merged branches (already in\n // contentrain) and failed/empty branches are both safe to delete, so\n // failed saves and merged saves no longer leak dangling cr/* refs.\n if (!pendingReview) {\n await safeDeleteBranch(git, branch)\n }\n },\n }\n}\n\nexport async function mergeBranch(\n projectRoot: string,\n branchName: string,\n): Promise<{ action: 'merged'; commit: string; sync: SyncResult; remote?: RemoteDeleteResult }> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n // Detect base branch\n const baseBranch = process.env['CONTENTRAIN_BRANCH']\n ?? config?.repository?.default_branch\n ?? ((await git.raw(['branch', '--show-current'])).trim() || 'main')\n\n // Ensure contentrain branch exists\n await ensureContentBranch(projectRoot)\n\n // Check remote\n let hasRemote = false\n try {\n const remotes = await git.getRemotes()\n hasRemote = remotes.some(r => r.name === remoteName)\n } catch {\n hasRemote = false\n }\n\n // Create temp worktree on contentrain branch\n const worktreePath = join(tmpdir(), `cr-merge-${randomUUID()}`)\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\n\n // Commit identity from `-c user.*` config (see authorConfig) — guard-safe,\n // no `.env()` spread.\n const wtGit = simpleGit(worktreePath, { config: authorConfig() })\n\n try {\n // Merge the feature branch into contentrain\n try {\n await wtGit.merge([branchName, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Merge conflict when merging branch \"${branchName}\" into \"${CONTENTRAIN_BRANCH}\". `\n + `The branch still exists with your changes intact. `\n + `Resolve the conflict manually, or delete the branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_MERGE_CONFLICT',\n agent_hint: 'The feature branch could not be merged into the contentrain branch. Ask the developer to resolve the conflict.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${branchName}`,\n })\n }\n\n // Regenerate context.json on contentrain post-merge (deterministic,\n // single-threaded) so review-mode branches — which carry no context.json —\n // still produce up-to-date stats once landed.\n await regenerateContextOnContentrain(wtGit, worktreePath, { tool: 'contentrain_merge', model: '*' })\n\n // Get contentrain tip + old base ref + dirty files in parallel\n const [contentrainTip, previousBaseRef, statusBeforeUpdate] = await Promise.all([\n wtGit.raw(['rev-parse', 'HEAD']).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n git.status(),\n ])\n const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map(f => f.path))\n\n // Verify fast-forward: baseBranch must be an ancestor of contentrainTip.\n // (See complete() — merge-base --is-ancestor is unusable via simple-git.)\n if (!(await isAncestor(git, previousBaseRef, contentrainTip))) {\n throw Object.assign(new Error(\n `Cannot fast-forward \"${baseBranch}\" to contentrain tip. `\n + `The base branch has diverged. Merge \"${baseBranch}\" into \"${CONTENTRAIN_BRANCH}\" first.`,\n ), {\n code: 'BASE_UPDATE_FAILED',\n agent_hint: `The base branch has commits not in contentrain. Merge ${baseBranch} into ${CONTENTRAIN_BRANCH} first.`,\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${baseBranch} && git checkout ${baseBranch}`,\n })\n }\n\n // Advance base branch to contentrain tip via update-ref\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n\n // Refresh index to match new HEAD\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n\n // Selective sync: copy .contentrain/ files to developer's working tree\n const sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate)\n\n // Push to remote (best-effort)\n if (hasRemote) {\n try {\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n try {\n await wtGit.fetch(remoteName, CONTENTRAIN_BRANCH)\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n await git.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n // Push failed after retry — continue, local state is fine\n }\n }\n\n try {\n await git.push(remoteName, baseBranch)\n } catch {\n // push may fail, local merge succeeded\n }\n }\n\n // Prune the now-merged feature branch so merged cr/* refs don't accumulate.\n await safeDeleteBranch(git, branchName)\n\n // Delete the remote copy too (review-mode branches were pushed on save).\n // Best-effort and config-gated inside the helper: a failure surfaces as\n // `remote.warning`, never as a failed merge.\n let remote: RemoteDeleteResult | undefined\n if (hasRemote) {\n remote = await deleteRemoteBranch(projectRoot, branchName, { config })\n }\n\n return {\n action: 'merged' as const,\n commit: contentrainTip,\n sync,\n ...(remote ? { remote } : {}),\n }\n } finally {\n // Cleanup worktree\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n }\n}\n\nexport function buildBranchName(scope: string, target: string, locale?: string): string {\n const ts = branchTimestamp()\n const parts = ['cr', scope, target]\n if (locale) parts.push(locale)\n parts.push(ts)\n return parts.join('/')\n}\n\n/**\n * True when `ancestor` is an ancestor of (or equal to) `descendant`.\n * Implemented with `rev-list --count` because `merge-base --is-ancestor`\n * signals via exit code with empty stderr — simple-git reports that as\n * success, so it cannot express a negative verdict.\n */\nasync function isAncestor(git: SimpleGit, ancestor: string, descendant: string): Promise<boolean> {\n try {\n const count = Number((await git.raw(['rev-list', '--count', ancestor, `^${descendant}`])).trim())\n return count === 0\n } catch {\n return false\n }\n}\n\n/**\n * Force-delete a local branch, swallowing all errors. Never deletes the\n * singleton `contentrain` branch. Used to prune feature branches after they\n * are merged (auto-merge / contentrain_merge) or when a transaction fails\n * before completing — so failed/merged `cr/*` refs do not accumulate.\n */\nasync function safeDeleteBranch(git: SimpleGit, branch: string): Promise<void> {\n if (!branch || branch === CONTENTRAIN_BRANCH) return\n try {\n await git.raw(['branch', '-D', branch])\n } catch {\n // Branch may not exist, be checked out, or already be deleted — ignore.\n }\n}\n\n/**\n * Regenerate `.contentrain/context.json` deterministically inside a worktree\n * that is currently on the `contentrain` branch, then commit it (hooks\n * bypassed). Called AFTER a feature branch is merged so context.json is only\n * ever written on `contentrain`, single-threaded — eliminating the per-branch\n * merge conflicts that came from committing it on every feature branch.\n */\nasync function regenerateContextOnContentrain(\n wtGit: SimpleGit,\n worktreePath: string,\n contextUpdate: ContextUpdate,\n): Promise<void> {\n await writeContext(worktreePath, contextUpdate)\n await wtGit.add('.contentrain/context.json')\n try {\n await wtGit.commit('[contentrain] context: update', { '--no-verify': null })\n } catch {\n // Nothing staged (context.json unchanged) — fine.\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@contentrain/mcp",
3
- "version": "1.9.0",
3
+ "version": "1.10.1",
4
4
  "mcpName": "io.github.Contentrain/contentrain",
5
5
  "license": "MIT",
6
- "description": "Local-first MCP server for AI-generated content governance — 19 deterministic tools, stdio + HTTP transports, Local / GitHub / GitLab providers",
6
+ "description": "Local-first MCP server for AI-generated content governance — 24 deterministic tools (19 core + 5 media), stdio + HTTP transports, Local / GitHub / GitLab providers",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -155,10 +155,10 @@
155
155
  ],
156
156
  "dependencies": {
157
157
  "@modelcontextprotocol/sdk": "^1.12.0",
158
- "simple-git": "^3.27.0",
158
+ "simple-git": "^3.36.0",
159
159
  "typescript": "^5.7.0",
160
160
  "zod": "^3.24.0",
161
- "@contentrain/types": "0.7.0"
161
+ "@contentrain/types": "0.8.0"
162
162
  },
163
163
  "optionalDependencies": {
164
164
  "@astrojs/compiler": "^2.10.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"annotations-DSzewhL4.mjs","names":[],"sources":["../src/tools/annotations.ts"],"sourcesContent":["import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'\n\n/**\n * Centralized tool annotations registry.\n * MCP clients use these hints to distinguish read-only vs. write vs. destructive tools.\n * `openWorldHint` is `false` across the board: every tool operates on the\n * configured content repository only — none reaches out to arbitrary\n * external systems.\n *\n * Also serves as the **single source of truth for the tool name list**. Consumers\n * that need to enumerate every registered tool (e.g. parity tests in\n * `@contentrain/rules` / `@contentrain/skills`) should import `TOOL_NAMES` below\n * rather than hardcoding the list.\n */\nexport const TOOL_ANNOTATIONS: Record<string, ToolAnnotations> = {\n // ─── Context (read-only) ───\n contentrain_status: {\n title: 'Project Status',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n contentrain_describe: {\n title: 'Describe Model',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n contentrain_describe_format: {\n title: 'Describe Format',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n contentrain_doctor: {\n title: 'Project Health Report',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n\n // ─── Setup (write + git) ───\n contentrain_init: {\n title: 'Initialize Project',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n },\n contentrain_scaffold: {\n title: 'Scaffold Template',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n },\n\n // ─── Model (write + git) ───\n contentrain_model_save: {\n title: 'Save Model',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n contentrain_model_delete: {\n title: 'Delete Model',\n readOnlyHint: false,\n destructiveHint: true,\n idempotentHint: false,\n openWorldHint: false,\n },\n\n // ─── Content (mixed) ───\n contentrain_content_save: {\n title: 'Save Content',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n contentrain_content_delete: {\n title: 'Delete Content',\n readOnlyHint: false,\n destructiveHint: true,\n idempotentHint: false,\n openWorldHint: false,\n },\n contentrain_content_list: {\n title: 'List Content',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n\n // ─── Workflow (mixed) ───\n contentrain_validate: {\n title: 'Validate Project',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n contentrain_submit: {\n title: 'Submit Branches',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n contentrain_merge: {\n title: 'Merge Branch',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n },\n contentrain_branch_list: {\n title: 'List Branches',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n contentrain_branch_delete: {\n title: 'Delete Branch',\n readOnlyHint: false,\n destructiveHint: true,\n idempotentHint: false,\n openWorldHint: false,\n },\n\n // ─── Normalize (mixed) ───\n contentrain_scan: {\n title: 'Scan Source Code',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n contentrain_apply: {\n title: 'Apply Normalize',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n },\n\n // ─── Bulk (write + git) ───\n contentrain_bulk: {\n title: 'Bulk Operations',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n },\n}\n\n/**\n * Canonical list of every registered MCP tool name, derived from the\n * single source of truth above. Re-exported here with a stable name so\n * parity tests in sibling packages (`@contentrain/rules`,\n * `@contentrain/skills`) can assert against it without depending on\n * `TOOL_ANNOTATIONS` internals.\n */\nexport const TOOL_NAMES: readonly string[] = Object.freeze(Object.keys(TOOL_ANNOTATIONS))\n"],"mappings":";;;;;;;;;;;;;AAcA,MAAa,mBAAoD;CAE/D,oBAAoB;EAClB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACD,sBAAsB;EACpB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACD,6BAA6B;EAC3B,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACD,oBAAoB;EAClB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CAGD,kBAAkB;EAChB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACD,sBAAsB;EACpB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CAGD,wBAAwB;EACtB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACD,0BAA0B;EACxB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CAGD,0BAA0B;EACxB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACD,4BAA4B;EAC1B,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACD,0BAA0B;EACxB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CAGD,sBAAsB;EACpB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACD,oBAAoB;EAClB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACD,mBAAmB;EACjB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACD,yBAAyB;EACvB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACD,2BAA2B;EACzB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CAGD,kBAAkB;EAChB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACD,mBAAmB;EACjB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CAGD,kBAAkB;EAChB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB,gBAAgB;EAChB,eAAe;EAChB;CACF;;;;;;;;AASD,MAAa,aAAgC,OAAO,OAAO,OAAO,KAAK,iBAAiB,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"availability-Bj9zOfEn.mjs","names":[],"sources":["../src/tools/availability.ts"],"sourcesContent":["import type { ProviderCapabilities } from '@contentrain/types'\n\n/**\n * Declarative availability requirements for capability-gated tools.\n *\n * This is the single source of truth `createServer` uses to decide which\n * tools to register for a given provider + projectRoot combination. Tools\n * absent from this map are always available. The requirements mirror the\n * call-time guards inside each tool handler (`capabilityError`) — those\n * guards remain as defense in depth, but a client talking to a remote\n * provider no longer sees tools that could never succeed in `tools/list`.\n *\n * `contentrain_validate` is intentionally NOT listed: read-only validate\n * works over any provider; only `fix: true` needs a local worktree and\n * keeps its call-time guard.\n */\nexport interface ToolRequirements {\n /** Tool needs a local project root on disk (worktree git transactions, IDE config, AST scans). */\n projectRoot?: boolean\n /** Provider capability flags that must all be `true`. */\n capabilities?: readonly (keyof ProviderCapabilities)[]\n}\n\nexport const TOOL_REQUIREMENTS: Readonly<Record<string, ToolRequirements>> = {\n contentrain_init: { projectRoot: true },\n contentrain_scaffold: { projectRoot: true },\n contentrain_doctor: { projectRoot: true },\n contentrain_bulk: { projectRoot: true },\n contentrain_submit: { projectRoot: true, capabilities: ['localWorktree', 'pushRemote'] },\n contentrain_merge: { projectRoot: true, capabilities: ['localWorktree'] },\n contentrain_branch_list: { projectRoot: true, capabilities: ['localWorktree'] },\n contentrain_branch_delete: { projectRoot: true, capabilities: ['localWorktree'] },\n contentrain_scan: { projectRoot: true, capabilities: ['astScan'] },\n // Registration requires the extract path (sourceRead); reuse mode\n // additionally needs sourceWrite, which stays a call-time check because\n // it depends on the `mode` input.\n contentrain_apply: { projectRoot: true, capabilities: ['sourceRead'] },\n}\n\n/**\n * Whether a tool can ever succeed for this provider + projectRoot pair.\n * Used by `createServer` to filter registration; also exported for\n * embedders (e.g. Studio) that want to reason about the effective tool\n * surface without spinning up a server.\n */\nexport function isToolAvailable(\n name: string,\n provider: { capabilities: ProviderCapabilities },\n projectRoot: string | undefined,\n): boolean {\n const req = TOOL_REQUIREMENTS[name]\n if (!req) return true\n if (req.projectRoot && !projectRoot) return false\n return (req.capabilities ?? []).every(cap => provider.capabilities[cap])\n}\n"],"mappings":";AAuBA,MAAa,oBAAgE;CAC3E,kBAAkB,EAAE,aAAa,MAAM;CACvC,sBAAsB,EAAE,aAAa,MAAM;CAC3C,oBAAoB,EAAE,aAAa,MAAM;CACzC,kBAAkB,EAAE,aAAa,MAAM;CACvC,oBAAoB;EAAE,aAAa;EAAM,cAAc,CAAC,iBAAiB,aAAa;EAAE;CACxF,mBAAmB;EAAE,aAAa;EAAM,cAAc,CAAC,gBAAgB;EAAE;CACzE,yBAAyB;EAAE,aAAa;EAAM,cAAc,CAAC,gBAAgB;EAAE;CAC/E,2BAA2B;EAAE,aAAa;EAAM,cAAc,CAAC,gBAAgB;EAAE;CACjF,kBAAkB;EAAE,aAAa;EAAM,cAAc,CAAC,UAAU;EAAE;CAIlE,mBAAmB;EAAE,aAAa;EAAM,cAAc,CAAC,aAAa;EAAE;CACvE;;;;;;;AAQD,SAAgB,gBACd,MACA,UACA,aACS;CACT,MAAM,MAAM,kBAAkB;AAC9B,KAAI,CAAC,IAAK,QAAO;AACjB,KAAI,IAAI,eAAe,CAAC,YAAa,QAAO;AAC5C,SAAQ,IAAI,gBAAgB,EAAE,EAAE,OAAM,QAAO,SAAS,aAAa,KAAK"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"branch-lifecycle-D7tXOE4f.mjs","names":[],"sources":["../src/git/branch-lifecycle.ts"],"sourcesContent":["import { simpleGit, type SimpleGit } from 'simple-git'\nimport { CONTENTRAIN_BRANCH, type ContentrainConfig } from '@contentrain/types'\nimport { readConfig } from '../core/config.js'\n\nexport interface CleanupResult {\n deleted: number\n remaining: number\n deletedBranches: string[]\n}\n\nexport interface BranchHealthCheck {\n total: number\n merged: number\n unmerged: number\n warning: boolean\n blocked: boolean\n message?: string\n}\n\n/**\n * Lists all local contentrain/* branches, deletes those already merged\n * into the base branch, and returns the count of remaining unmerged ones.\n */\nexport async function cleanupMergedBranches(projectRoot: string): Promise<CleanupResult> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n\n // Determine base branch\n const baseBranch = config?.repository?.default_branch\n ?? process.env['CONTENTRAIN_BRANCH']\n ?? ((await git.raw(['branch', '--show-current'])).trim() || 'main')\n\n // Get all local branches (exclude the dedicated contentrain branch itself)\n const branchSummary = await git.branchLocal()\n const contentrainBranches = branchSummary.all\n .filter(b => b.startsWith('cr/'))\n .filter(b => b !== CONTENTRAIN_BRANCH)\n\n if (contentrainBranches.length === 0) {\n return { deleted: 0, remaining: 0, deletedBranches: [] }\n }\n\n // Check merged into the dedicated contentrain branch, falling back to baseBranch\n let mergedSet: Set<string>\n try {\n mergedSet = await classifyMergedBranches(projectRoot, contentrainBranches, CONTENTRAIN_BRANCH)\n } catch {\n // Contentrain branch may not exist yet (pre-init); fall back to baseBranch\n try {\n mergedSet = await classifyMergedBranches(projectRoot, contentrainBranches, baseBranch)\n } catch {\n // Base branch may not exist either; nothing is merged\n return { deleted: 0, remaining: contentrainBranches.length, deletedBranches: [] }\n }\n }\n\n const mergedContentrain = contentrainBranches.filter(b => mergedSet.has(b))\n const deletedBranches: string[] = []\n\n // Determine retention period (days). Default: 30\n const retentionDays = config?.branchRetention ?? 30\n const retentionMs = retentionDays * 24 * 60 * 60 * 1000\n const now = Date.now()\n\n // Delete merged branches only if older than retention period\n for (const branch of mergedContentrain) {\n try {\n const timestampRaw = (await git.raw(['log', '-1', '--format=%ct', branch])).trim()\n const commitTimestamp = Number(timestampRaw) * 1000\n if (now - commitTimestamp < retentionMs) {\n continue // Branch is within retention period — keep it\n }\n // -D, not -d: classifyMergedBranches already proved merged-ness (including\n // patch-id equivalence after a base-history rewrite, which -d's\n // ancestry-only check would refuse).\n await git.raw(['branch', '-D', branch])\n deletedBranches.push(branch)\n } catch {\n // Branch may be checked out, locked, or log failed — skip\n }\n }\n\n const remaining = contentrainBranches.length - deletedBranches.length\n return { deleted: deletedBranches.length, remaining, deletedBranches }\n}\n\n/**\n * Check branch health: count contentrain/* branches and return warning/blocked status.\n * - 50+ branches: warning\n * - 80+ branches: blocked\n */\nexport async function checkBranchHealth(projectRoot: string): Promise<BranchHealthCheck> {\n const git = simpleGit(projectRoot)\n const config = await readConfig(projectRoot)\n\n const baseBranch = config?.repository?.default_branch\n ?? process.env['CONTENTRAIN_BRANCH']\n ?? ((await git.raw(['branch', '--show-current'])).trim() || 'main')\n\n const branchSummary = await git.branchLocal()\n const contentrainBranches = branchSummary.all\n .filter(b => b.startsWith('cr/'))\n .filter(b => b !== CONTENTRAIN_BRANCH)\n const total = contentrainBranches.length\n\n const warnLimit = config?.branchWarnLimit ?? 50\n const blockLimit = config?.branchBlockLimit ?? 80\n\n // Count merged into contentrain branch, falling back to baseBranch.\n // This gate runs before EVERY write: the patch-id fallback (which rescues\n // rewrite-orphaned merged branches from being counted as unmerged) only\n // matters once the ancestry-unmerged count could trip the warning, so\n // below `warnLimit` the check costs exactly one `branch --merged`.\n let mergedCount = 0\n const classifyOpts = { fallbackThreshold: warnLimit }\n try {\n const mergedSet = await classifyMergedBranches(projectRoot, contentrainBranches, CONTENTRAIN_BRANCH, classifyOpts)\n mergedCount = mergedSet.size\n } catch {\n // Contentrain branch may not exist yet (pre-init); fall back to baseBranch\n try {\n const mergedSet = await classifyMergedBranches(projectRoot, contentrainBranches, baseBranch, classifyOpts)\n mergedCount = mergedSet.size\n } catch {\n // ignore — neither branch exists\n }\n }\n const unmerged = total - mergedCount\n const warning = unmerged >= warnLimit\n const blocked = unmerged >= blockLimit\n\n let message: string | undefined\n if (blocked) {\n message = `BLOCKED: ${unmerged} active contentrain branches (limit: ${blockLimit}). Run cleanup or merge/delete old branches before creating new ones.`\n } else if (warning) {\n message = `WARNING: ${unmerged} active contentrain branches. Consider merging or deleting old branches (warning at ${warnLimit}, blocked at ${blockLimit}).`\n }\n\n return { total, merged: mergedCount, unmerged, warning, blocked, message }\n}\n\nexport interface BranchDiffResult {\n /** The feature branch the diff was computed from. */\n branch: string\n /** The base ref the diff was computed against. Defaults to the `contentrain` branch. */\n base: string\n /** `git diff --stat` output — human-readable summary. */\n stat: string\n /** Raw unified diff. */\n patch: string\n /** Number of files touched in the diff. */\n filesChanged: number\n}\n\n/**\n * Compute the diff between a feature branch and its base.\n *\n * Defaults `base` to `CONTENTRAIN_BRANCH` — the singleton content-\n * tracking branch every feature branch forks from. Passing the repo's\n * default branch (e.g. `main`) is almost always a bug: when\n * `contentrain` is ahead of `main`, the diff picks up unrelated\n * historical content changes that the feature branch did not produce.\n *\n * Used by `contentrain serve` (branch detail view), the `contentrain\n * diff` CLI command, and any Studio-side driver that needs to preview\n * a feature branch before approving it.\n */\nexport async function branchDiff(\n projectRoot: string,\n opts: { branch: string, base?: string },\n): Promise<BranchDiffResult> {\n const git = simpleGit(projectRoot)\n const base = opts.base ?? CONTENTRAIN_BRANCH\n const range = `${base}...${opts.branch}`\n\n const [stat, patch, summary] = await Promise.all([\n git.diff([range, '--stat']),\n git.diff([range]),\n git.diffSummary([range]),\n ])\n\n return {\n branch: opts.branch,\n base,\n stat,\n patch,\n filesChanged: summary.changed,\n }\n}\n\n// ─── Merged-state classification (ancestry + patch-id fallback) ───\n\n/**\n * Merged-verdict caches.\n *\n * - `pairVerdictCache` — keyed by `(tipSha, intoTipSha, cap)`: the\n * relationship between two FIXED commits never changes, so entries are\n * permanently valid.\n * - `mergedTipCache` — keyed by `(tipSha, into NAME)`: once a tip is merged\n * into a branch, it stays merged as that branch advances (merged-ness is\n * monotonic), so positives survive `contentrain` moving forward. This is\n * what keeps the per-write branch-health gate cheap in long-lived\n * processes (MCP server, `contentrain serve`).\n */\nconst pairVerdictCache = new Map<string, boolean>()\nconst mergedTipCache = new Map<string, true>()\nconst MERGED_CACHE_LIMIT = 10_000\nconst DEFAULT_MAX_CHERRY_COMMITS = 200\n/** Concurrent `git cherry` subprocesses during classification. */\nconst CHERRY_CONCURRENCY = 8\n\nasync function isTipMerged(\n git: SimpleGit,\n tip: string,\n intoTip: string,\n maxCherryCommits: number,\n intoName?: string,\n): Promise<boolean> {\n const tipKey = intoName ? `${tip}→${intoName}` : undefined\n if (tipKey && mergedTipCache.has(tipKey)) return true\n const pairKey = `${tip}:${intoTip}:${maxCherryCommits}`\n const cached = pairVerdictCache.get(pairKey)\n if (cached !== undefined) return cached\n\n let merged = false\n try {\n // ONE subprocess answers both questions: `git cherry intoTip tip` lists\n // tip's commits missing from intoTip by ancestry — empty output means\n // ancestor (merged); otherwise a line without `+` is patch-id-equivalent\n // to a commit already in intoTip (survives base-history rewrites).\n // Deliberately NOT `merge-base --is-ancestor`: that plumbing signals via\n // exit code with EMPTY stderr, and simple-git reports exit-code-only\n // failures as success, silently inverting the check. Bounded by\n // maxCherryCommits so rewrite-orphaned deep histories cannot stall the\n // hot pre-write gate.\n const lines = (await git.raw(['cherry', intoTip, tip]))\n .split('\\n').map(line => line.trim()).filter(Boolean)\n if (lines.length === 0) {\n merged = true\n } else if (lines.length <= maxCherryCommits) {\n merged = !lines.some(line => line.startsWith('+'))\n }\n } catch {\n merged = false\n }\n\n if (pairVerdictCache.size >= MERGED_CACHE_LIMIT) pairVerdictCache.clear()\n pairVerdictCache.set(pairKey, merged)\n if (merged && tipKey) {\n if (mergedTipCache.size >= MERGED_CACHE_LIMIT) mergedTipCache.clear()\n mergedTipCache.set(tipKey, true)\n }\n return merged\n}\n\n/**\n * Robust merged check for a single ref: ancestry fast-path, then a bounded\n * `git cherry` (patch-id) fallback that survives base-history rewrites.\n * Returns false when either ref cannot be resolved.\n */\nexport async function isRefMerged(\n git: SimpleGit,\n ref: string,\n into: string,\n opts?: { maxCherryCommits?: number },\n): Promise<boolean> {\n let tip: string | undefined\n let intoTip: string | undefined\n try {\n const lines = (await git.raw(['rev-parse', ref, into])).trim().split('\\n')\n tip = lines[0]?.trim()\n intoTip = lines[1]?.trim()\n } catch {\n return false\n }\n if (!tip || !intoTip) return false\n return isTipMerged(git, tip, intoTip, opts?.maxCherryCommits ?? DEFAULT_MAX_CHERRY_COMMITS, into)\n}\n\n/**\n * Classify which of the given local branches are merged into `into`\n * (default: the contentrain branch). One `git branch --merged` call covers\n * the ancestry-merged majority; only the remainder pays the patch-id\n * fallback (bounded concurrency, verdicts cached).\n *\n * `opts.fallbackThreshold` skips the patch-id fallback entirely when fewer\n * than that many branches are ancestry-unmerged — the fallback can only\n * LOWER the unmerged count, so callers that merely compare the count\n * against a limit (the hot pre-write gate) pay nothing in the normal case.\n *\n * Throws when `into` does not resolve — callers use this to fall back to\n * the base branch (mirrors the previous `branch --merged` semantics).\n */\nexport async function classifyMergedBranches(\n projectRoot: string,\n branches: string[],\n into: string = CONTENTRAIN_BRANCH,\n opts?: { fallbackThreshold?: number },\n): Promise<Set<string>> {\n const git = simpleGit(projectRoot)\n if (branches.length === 0) {\n // Preserve the \"throws when into is missing\" contract even for empty input.\n await git.raw(['branch', '--merged', into])\n return new Set()\n }\n const mergedRaw = await git.raw(['branch', '--merged', into])\n const ancestryMerged = new Set(\n mergedRaw.split('\\n').map(b => b.replace(/^\\*?\\s+/, '').trim()).filter(Boolean),\n )\n\n const merged = new Set<string>()\n const rest: string[] = []\n for (const branch of branches) {\n if (ancestryMerged.has(branch)) merged.add(branch)\n else rest.push(branch)\n }\n if (rest.length === 0 || rest.length < (opts?.fallbackThreshold ?? 0)) return merged\n\n // Resolve all remaining tips + into in ONE subprocess, then run the\n // (mostly cached) patch-id fallback in bounded-concurrency chunks. Each\n // task gets its OWN simple-git instance — a shared instance serializes\n // its command queue, which turns 80 branches into 80 sequential spawns.\n let tips: string[]\n try {\n tips = (await git.raw(['rev-parse', into, ...rest])).trim().split('\\n').map(s => s.trim())\n } catch {\n return merged\n }\n const intoTip = tips[0]\n if (!intoTip) return merged\n for (let i = 0; i < rest.length; i += CHERRY_CONCURRENCY) {\n const chunk = rest.slice(i, i + CHERRY_CONCURRENCY)\n const verdicts = await Promise.all(chunk.map((branch, j) => {\n const tip = tips[i + j + 1]\n return tip\n ? isTipMerged(simpleGit(projectRoot), tip, intoTip, DEFAULT_MAX_CHERRY_COMMITS, into)\n : Promise.resolve(false)\n }))\n for (const [j, verdict] of verdicts.entries()) {\n if (verdict) merged.add(chunk[j]!)\n }\n }\n return merged\n}\n\n// ─── Remote cr/* branch lifecycle ───\n\nconst REMOTE_PUSH_TIMEOUT_MS = 10_000\nconst REMOTE_LIST_TIMEOUT_MS = 5_000\n\nfunction contentrainRemoteName(): string {\n return process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n}\n\n/**\n * simple-git instance hardened for network operations: `timeout.block` kills\n * the child after N ms without output (hung SSH passphrase prompts), and\n * `GIT_TERMINAL_PROMPT=0` refuses interactive HTTPS credential prompts.\n */\nfunction networkGit(projectRoot: string, timeoutMs: number): SimpleGit {\n return simpleGit({ baseDir: projectRoot, timeout: { block: timeoutMs } })\n .env({ ...process.env, GIT_TERMINAL_PROMPT: '0' })\n}\n\nasync function resolveRemote(git: SimpleGit): Promise<string | null> {\n try {\n const remotes = await git.getRemotes()\n const name = contentrainRemoteName()\n return remotes.some(r => r.name === name) ? name : null\n } catch {\n return null\n }\n}\n\nexport interface RemoteDeleteResult {\n deleted: boolean\n /** Why nothing was deleted, when that is expected (not a failure). */\n skipped?: 'disabled' | 'no-remote' | 'not-found' | 'protected'\n /** A real failure (offline, auth, protected ref) — surfaced, never thrown. */\n warning?: string\n}\n\n/**\n * Best-effort delete of a cr/* branch on the configured remote. Never\n * throws: expected conditions land in `skipped`, real failures in\n * `warning`. Gated by `config.remoteBranchCleanup` (default: on).\n *\n * Pass `opts.config` when the caller already read it (avoids a re-read);\n * `null` means \"no config\" and applies the default gate.\n */\nexport async function deleteRemoteBranch(\n projectRoot: string,\n branch: string,\n opts?: { config?: ContentrainConfig | null, timeoutMs?: number },\n): Promise<RemoteDeleteResult> {\n if (!branch.startsWith('cr/') || branch === CONTENTRAIN_BRANCH) {\n return { deleted: false, skipped: 'protected' }\n }\n const config = opts?.config === undefined ? await readConfig(projectRoot) : opts.config\n if (!(config?.remoteBranchCleanup ?? true)) {\n return { deleted: false, skipped: 'disabled' }\n }\n const git = networkGit(projectRoot, opts?.timeoutMs ?? REMOTE_PUSH_TIMEOUT_MS)\n const remote = await resolveRemote(git)\n if (!remote) return { deleted: false, skipped: 'no-remote' }\n try {\n await git.push([remote, '--delete', branch])\n return { deleted: true }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n if (/remote ref does not exist|couldn't find remote ref/i.test(message)) {\n return { deleted: false, skipped: 'not-found' }\n }\n return { deleted: false, warning: `Could not delete \"${branch}\" on ${remote}: ${message}` }\n }\n}\n\nexport interface RemoteBranchList {\n remote: string\n branches: { name: string, sha: string }[]\n /** ls-remote failed (offline/timeout) — branches is empty, not authoritative. */\n error?: string\n}\n\n/**\n * Authoritative list of cr/* branches on the configured remote via\n * `ls-remote --heads` (no fetch, no stale remote-tracking refs). Returns\n * null when no remote is configured. Never throws.\n */\nexport async function listRemoteCrBranches(\n projectRoot: string,\n opts?: { timeoutMs?: number },\n): Promise<RemoteBranchList | null> {\n const git = networkGit(projectRoot, opts?.timeoutMs ?? REMOTE_LIST_TIMEOUT_MS)\n const remote = await resolveRemote(git)\n if (!remote) return null\n try {\n const raw = await git.raw(['ls-remote', '--heads', remote, 'refs/heads/cr/*'])\n const branches = raw.split('\\n')\n .map(line => line.trim())\n .filter(Boolean)\n .map((line) => {\n const [sha, ref] = line.split('\\t')\n return { sha: sha?.trim() ?? '', name: ref?.trim().replace(/^refs\\/heads\\//, '') ?? '' }\n })\n .filter(b => b.name.startsWith('cr/') && b.name !== CONTENTRAIN_BRANCH)\n return { remote, branches }\n } catch (error) {\n return { remote, branches: [], error: error instanceof Error ? error.message : String(error) }\n }\n}\n\nexport interface RemotePruneResult {\n /** Branches removed from the remote (in dryRun mode: the candidates). */\n deleted: string[]\n kept: string[]\n errors: string[]\n skipped?: 'disabled' | 'no-remote' | 'offline'\n}\n\nconst PRUNE_PUSH_CHUNK = 50\n\n/**\n * Delete already-merged cr/* branches on the remote in batches. Merged-state\n * uses the same ancestry + patch-id classification as the local cleanup, so\n * branches leaked before a base-history rewrite are still recognised.\n * Ignores `branchRetention` — a merged remote copy only produces phantom\n * reviews. Never throws; gated by `config.remoteBranchCleanup`.\n */\nexport async function pruneMergedRemoteBranches(\n projectRoot: string,\n opts?: { config?: ContentrainConfig | null, max?: number, dryRun?: boolean, timeoutMs?: number },\n): Promise<RemotePruneResult> {\n const config = opts?.config === undefined ? await readConfig(projectRoot) : opts.config\n if (!(config?.remoteBranchCleanup ?? true)) {\n return { deleted: [], kept: [], errors: [], skipped: 'disabled' }\n }\n\n const listed = await listRemoteCrBranches(projectRoot, { timeoutMs: opts?.timeoutMs })\n if (!listed) return { deleted: [], kept: [], errors: [], skipped: 'no-remote' }\n if (listed.error) return { deleted: [], kept: [], errors: [listed.error], skipped: 'offline' }\n if (listed.branches.length === 0) return { deleted: [], kept: [], errors: [] }\n\n const git = networkGit(projectRoot, opts?.timeoutMs ?? REMOTE_PUSH_TIMEOUT_MS)\n\n // Remote tips may predate this clone (leaked long ago, and our fetches are\n // single-refspec) — one scoped fetch backfills any missing objects.\n // `cat-file -t` (not `-e`): -e signals via exit code with empty stderr,\n // which simple-git reports as success.\n const presence = await Promise.all(listed.branches.map(b =>\n git.raw(['cat-file', '-t', b.sha]).then(() => true).catch(() => false),\n ))\n if (presence.some(present => !present)) {\n try {\n await git.fetch(listed.remote, `+refs/heads/cr/*:refs/remotes/${listed.remote}/cr/*`)\n } catch {\n // Best-effort: branches whose objects stay unresolvable are kept below.\n }\n }\n\n const kept: string[] = []\n const candidates: string[] = []\n const verdicts = await Promise.all(listed.branches.map(b => isRefMerged(git, b.sha, CONTENTRAIN_BRANCH)))\n for (const [i, merged] of verdicts.entries()) {\n const name = listed.branches[i]!.name\n if (merged) candidates.push(name)\n else kept.push(name)\n }\n\n const limit = opts?.max ?? Number.POSITIVE_INFINITY\n const toDelete = candidates.slice(0, limit)\n kept.push(...candidates.slice(toDelete.length))\n\n if (opts?.dryRun) return { deleted: toDelete, kept, errors: [] }\n\n const deleted: string[] = []\n const errors: string[] = []\n for (let i = 0; i < toDelete.length; i += PRUNE_PUSH_CHUNK) {\n const chunk = toDelete.slice(i, i + PRUNE_PUSH_CHUNK)\n try {\n await git.push([listed.remote, '--delete', ...chunk])\n deleted.push(...chunk)\n } catch {\n // One missing ref fails the whole multi-refspec push — retry per branch.\n for (const branch of chunk) {\n try {\n await git.push([listed.remote, '--delete', branch])\n deleted.push(branch)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n if (/remote ref does not exist|couldn't find remote ref/i.test(message)) {\n deleted.push(branch) // already gone — net effect is pruned\n } else {\n errors.push(`${branch}: ${message}`)\n }\n }\n }\n }\n }\n return { deleted, kept, errors }\n}\n"],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- import { ApplyPlanInput, Branch, Commit, CommitAuthor, FileChange, FileDiff, LOCAL_CAPABILITIES, MergeResult, ProviderCapabilities as ProviderCapabilities$1, RepoProvider, RepoReader, RepoWriter } from "@contentrain/types";
2
- export { FileChange as a, MergeResult as c, RepoReader as d, RepoWriter as f, CommitAuthor as i, ProviderCapabilities$1 as l, Branch as n, FileDiff as o, Commit as r, LOCAL_CAPABILITIES as s, ApplyPlanInput as t, RepoProvider as u };