@contentrain/mcp 3.2.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -96,7 +96,7 @@ All write operations are designed around git-backed safety:
96
96
  | `contentrain_branch_delete` | Delete a stale/failed `cr/*` branch locally and on the remote (contentrain branch protected) | — | **Yes** |
97
97
  | `contentrain_scan` | Graph- and candidate-based hardcoded string scan | Yes | — |
98
98
  | `contentrain_apply` | Normalize extract/reuse execution with dry-run support | — | — |
99
- | `contentrain_bulk` | Bulk locale copy, status updates, and deletes | — | — |
99
+ | `contentrain_bulk` | Bulk locale copy, status updates, and deletes (`dry_run` previews) | — | — |
100
100
  | `contentrain_media_list` | List media assets (search, tag filter, cursor pagination) | Yes | — |
101
101
  | `contentrain_media_get` | Get one media asset by id | Yes | — |
102
102
  | `contentrain_media_ingest` | Ingest an asset from a source URL (provider fetches server-side) | — | — |
package/dist/index.mjs CHANGED
@@ -15,7 +15,7 @@ import "./transaction-BR2jg4uj.mjs";
15
15
  import "./local-DbPlR0Br.mjs";
16
16
  import "./detect-CI32ynpq.mjs";
17
17
  import "./annotations-CXHPnS-M.mjs";
18
- import { n as createServer } from "./server-ChpsLHlF.mjs";
18
+ import { n as createServer } from "./server-BwICwJYA.mjs";
19
19
  import "./errors-BmLE-JSC.mjs";
20
20
  import "./reconcile-EANDIp_T.mjs";
21
21
  import "./scan-config-C5zzygeH.mjs";
@@ -14,7 +14,7 @@ import "../../transaction-BR2jg4uj.mjs";
14
14
  import "../../local-DbPlR0Br.mjs";
15
15
  import "../../detect-CI32ynpq.mjs";
16
16
  import "../../annotations-CXHPnS-M.mjs";
17
- import { n as createServer } from "../../server-ChpsLHlF.mjs";
17
+ import { n as createServer } from "../../server-BwICwJYA.mjs";
18
18
  import "../../errors-BmLE-JSC.mjs";
19
19
  import "../../reconcile-EANDIp_T.mjs";
20
20
  import "../../scan-config-C5zzygeH.mjs";
@@ -2412,7 +2412,7 @@ function registerNormalizeTools(server, provider, projectRoot) {
2412
2412
  //#endregion
2413
2413
  //#region src/tools/bulk.ts
2414
2414
  function registerBulkTools(server, _provider, projectRoot) {
2415
- server.tool("contentrain_bulk", "Batch operations on content entries. All operations are auto-committed to git.", {
2415
+ server.tool("contentrain_bulk", "Batch operations on content entries. All operations are auto-committed to git. DRY RUN (dry_run:true): runs the whole operation in a throwaway worktree and reports exactly what would change — nothing is committed and no branch survives. Recommended workflow: preview with dry_run:true, review the counts, then call again to execute.", {
2416
2416
  operation: z.enum([
2417
2417
  "copy_locale",
2418
2418
  "update_status",
@@ -2431,7 +2431,8 @@ function registerBulkTools(server, _provider, projectRoot) {
2431
2431
  "rejected",
2432
2432
  "archived"
2433
2433
  ]).optional().describe("New status for update_status"),
2434
- confirm: z.boolean().optional().describe("Must be true for delete_entries")
2434
+ confirm: z.boolean().optional().describe("Must be true for delete_entries (not needed while dry_run:true — a preview deletes nothing)"),
2435
+ dry_run: z.boolean().optional().describe("Preview mode: perform the operation in a throwaway worktree and report what would change, committing nothing. Defaults to false so existing callers keep executing.")
2435
2436
  }, TOOL_ANNOTATIONS["contentrain_bulk"], async (input) => {
2436
2437
  if (!projectRoot) return capabilityError("contentrain_bulk", "localWorktree");
2437
2438
  const config = await readConfig(projectRoot);
@@ -2503,12 +2504,15 @@ function registerBulkTools(server, _provider, projectRoot) {
2503
2504
  const tx = await createTransaction(projectRoot, branch);
2504
2505
  try {
2505
2506
  let copiedCount = 0;
2507
+ let replacedCount = 0;
2506
2508
  await tx.write(async (wt) => {
2507
2509
  const cDir = resolveContentDir(wt, model);
2508
2510
  const sourceFile = resolveJsonFilePath(cDir, model, input.source_locale);
2509
2511
  const targetFile = resolveJsonFilePath(cDir, model, input.target_locale);
2510
2512
  const sourceData = await readJson(sourceFile);
2511
2513
  if (!sourceData) throw new Error(`No content found for locale "${input.source_locale}" in model "${input.model}"`);
2514
+ const targetData = await readJson(targetFile);
2515
+ if (targetData) replacedCount = model.kind === "collection" ? Object.keys(targetData).length : 1;
2512
2516
  await writeJson(targetFile, sourceData);
2513
2517
  if (model.kind === "collection") {
2514
2518
  const entries = sourceData;
@@ -2526,6 +2530,21 @@ function registerBulkTools(server, _provider, projectRoot) {
2526
2530
  copiedCount = 1;
2527
2531
  }
2528
2532
  });
2533
+ if (input.dry_run) {
2534
+ await tx.cleanup();
2535
+ return { content: [{
2536
+ type: "text",
2537
+ text: JSON.stringify({
2538
+ status: "preview",
2539
+ dry_run: true,
2540
+ operation: "copy_locale",
2541
+ message: `Would copy ${copiedCount} entries from ${input.source_locale} to ${input.target_locale}.`,
2542
+ would_copy: copiedCount,
2543
+ would_replace: replacedCount,
2544
+ next_steps: replacedCount > 0 ? [`${replacedCount} record(s) already exist in "${input.target_locale}" and would be REPLACED — confirm that is intended`, "Call contentrain_bulk again without dry_run to execute"] : ["Call contentrain_bulk again without dry_run to execute"]
2545
+ }, null, 2)
2546
+ }] };
2547
+ }
2529
2548
  await tx.commit(`[contentrain] bulk: copy ${input.source_locale} → ${input.target_locale} for ${input.model}`, {
2530
2549
  tool: "contentrain_bulk",
2531
2550
  model: input.model,
@@ -2690,6 +2709,22 @@ function registerBulkTools(server, _provider, projectRoot) {
2690
2709
  isError: true
2691
2710
  };
2692
2711
  }
2712
+ if (input.dry_run) {
2713
+ await tx.cleanup();
2714
+ return { content: [{
2715
+ type: "text",
2716
+ text: JSON.stringify({
2717
+ status: "preview",
2718
+ dry_run: true,
2719
+ operation: "update_status",
2720
+ message: `Would update ${updatedCount} meta record(s) to status "${input.status}" across locales [${Object.keys(updatedPerLocale).join(", ")}].`,
2721
+ would_update: updatedCount,
2722
+ would_update_by_locale: updatedPerLocale,
2723
+ not_found: notFound.length > 0 ? notFound : void 0,
2724
+ next_steps: ["Call contentrain_bulk again without dry_run to execute"]
2725
+ }, null, 2)
2726
+ }] };
2727
+ }
2693
2728
  await tx.commit(`[contentrain] bulk: update status → ${input.status} for ${input.model}`, {
2694
2729
  tool: "contentrain_bulk",
2695
2730
  model: input.model,
@@ -2739,10 +2774,13 @@ function registerBulkTools(server, _provider, projectRoot) {
2739
2774
  }],
2740
2775
  isError: true
2741
2776
  };
2742
- if (input.confirm !== true) return {
2777
+ if (!input.dry_run && input.confirm !== true) return {
2743
2778
  content: [{
2744
2779
  type: "text",
2745
- text: JSON.stringify({ error: "delete_entries requires confirm:true" })
2780
+ text: JSON.stringify({
2781
+ error: "delete_entries requires confirm:true",
2782
+ hint: "Run with dry_run:true first to see which files would be removed — no confirmation needed for a preview."
2783
+ })
2746
2784
  }],
2747
2785
  isError: true
2748
2786
  };
@@ -2766,6 +2804,21 @@ function registerBulkTools(server, _provider, projectRoot) {
2766
2804
  allRemoved.push(...removed);
2767
2805
  }
2768
2806
  });
2807
+ if (input.dry_run) {
2808
+ await tx.cleanup();
2809
+ return { content: [{
2810
+ type: "text",
2811
+ text: JSON.stringify({
2812
+ status: "preview",
2813
+ dry_run: true,
2814
+ operation: "delete_entries",
2815
+ message: `Would delete ${input.entry_ids.length} entries, removing ${allRemoved.length} file(s).`,
2816
+ would_delete: input.entry_ids.length,
2817
+ files_to_remove: allRemoved,
2818
+ next_steps: ["Call contentrain_bulk again with dry_run:false and confirm:true to execute"]
2819
+ }, null, 2)
2820
+ }] };
2821
+ }
2769
2822
  await tx.commit(`[contentrain] bulk: delete ${input.entry_ids.length} entries from ${input.model}`, {
2770
2823
  tool: "contentrain_bulk",
2771
2824
  model: input.model,
@@ -2959,7 +3012,7 @@ function registerMediaTools(server, provider, _projectRoot) {
2959
3012
  }
2960
3013
  //#endregion
2961
3014
  //#region package.json
2962
- var version = "3.2.0";
3015
+ var version = "3.3.1";
2963
3016
  //#endregion
2964
3017
  //#region src/server.ts
2965
3018
  /**
@@ -3034,4 +3087,4 @@ function resolveServerContext(input) {
3034
3087
  //#endregion
3035
3088
  export { createServer as n, DEFAULT_INSTRUCTIONS as t };
3036
3089
 
3037
- //# sourceMappingURL=server-ChpsLHlF.mjs.map
3090
+ //# sourceMappingURL=server-BwICwJYA.mjs.map