@nikelyh/metamorph 2.1.0 → 2.1.2

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
@@ -16,16 +16,19 @@
16
16
 
17
17
  Metamorph is a CLI tool powered by [Mozaik](https://github.com/jigjoy-ai/mozaik) that automates complex architectural shifts and framework migrations in your codebase. Instead of doing it manually, Metamorph spawns a swarm of specialized AI agents that safely refactor your code in an isolated "Shadow Workspace".
18
18
 
19
- 📚 **[Read the Full Documentation](https://metamorph.nikelyh.tech/docs)**
19
+ **[Read the Full Documentation](https://metamorph.nikelyh.tech/docs)** | **[System Architecture](https://github.com/yohanvillarp/metamorph/blob/main/docs/architecture.md)**
20
20
 
21
- ## 🚀 Features
21
+ ## Features
22
22
 
23
- - **Multi-Agent Concurrency**: Utilizes Mapper, Worker, Reviewer, and PackageManager agents to migrate code.
24
- - **Zero Risk**: All migrations happen in a `.metamorph/shadow` workspace. Your original code is completely untouched until you explicitly approve and integrate the changes.
25
- - **Visual Dashboard**: Watch your agents work in real-time through the built-in local dashboard UI.
26
- - **Extensive Framework Support**: Easily migrate between popular frameworks automatically.
23
+ - **Project Intelligence Engine (PIE)** -- Autonomously detects project architecture, frameworks, and monorepo workspace topologies (Turborepo, pnpm workspaces, npm/yarn workspaces, Lerna).
24
+ - **Polymorphic Package Manager (PPME)** -- Automatically detects and adapts to your chosen package manager (`npm`, `pnpm`, `yarn`, `bun`) without host process leakage.
25
+ - **Multi-Agent Concurrency** -- Utilizes Mapper, Worker, Reviewer, Integration, and Coordinator agents to migrate code in parallel.
26
+ - **Zero Risk (Shadow Workspace)** -- All migrations happen in an isolated `.metamorph/shadow` workspace. Your original code is completely untouched until you explicitly approve and apply the changes.
27
+ - **Shadow Build Verification** -- Every migration is validated with your package manager (`npm`, `pnpm`, `yarn`, `bun`) and verification build inside the shadow workspace before completion.
28
+ - **Visual Dashboard** -- Watch your agents work in real-time through the built-in local dashboard UI.
29
+ - **Extensive Framework Support** -- Easily migrate between popular frontend and backend frameworks.
27
30
 
28
- ## 📦 Installation
31
+ ## Installation
29
32
 
30
33
  Install Metamorph globally via NPM:
31
34
 
@@ -33,66 +36,90 @@ Install Metamorph globally via NPM:
33
36
  npm install -g @nikelyh/metamorph
34
37
  ```
35
38
 
36
- ## 🛠️ Typical Workflow
39
+ ### Prerequisites
37
40
 
38
- You can run the entire migration lifecycle using just your terminal.
41
+ - Node.js >= 20
42
+ - An LLM provider API key (OpenAI, Anthropic, or compatible). Set it as an environment variable:
43
+ ```bash
44
+ export OPENAI_API_KEY=sk-...
45
+ ```
39
46
 
40
- ### 1. Help & Options
41
- To see all available commands and options at any time, run:
42
- ```bash
43
- metamorph --help
44
- ```
47
+ ## Typical Workflow
48
+
49
+ ### 1. Start a Migration
45
50
 
46
- ### 2. Start a Migration
47
51
  Navigate to the root of the project you want to migrate and run:
52
+
48
53
  ```bash
49
54
  metamorph run
50
55
  ```
51
- Metamorph will automatically detect your current stack (e.g. Express, React) and prompt you to select the target framework. Alternatively, you can bypass the interactive prompt by passing the source explicitly:
56
+
57
+ Metamorph automatically detects your current stack (e.g., Express, React) and prompts you to select the target framework. You can bypass the interactive prompt by passing the source explicitly:
58
+
52
59
  ```bash
53
60
  metamorph run --from express
54
61
  ```
55
62
 
56
- ### 3. Monitor the Agents (Dashboard UI)
57
- Once a migration is running, the AI swarm begins working in the background. To see their real-time progress, open a **new terminal tab** in the same folder and run:
63
+ ### 2. Monitor the Agents (Dashboard UI)
64
+
65
+ Once a migration is running, the AI swarm begins working in the background. To see their real-time progress, open a new terminal in the same directory and run:
66
+
58
67
  ```bash
59
68
  metamorph ui
60
69
  ```
61
- This will start the internal dashboard server and open a Neo-Brutalist interface in your default browser. You can specify a port with `--port 8080`.
62
70
 
63
- ### 4. Review and Apply
64
- After the AI finishes the migration in the isolated Shadow Workspace, you will receive a `runId`. You can apply the completed migration to your actual repository (which automatically creates a new git branch for safety):
71
+ This starts the internal dashboard server and opens a monitoring interface in your default browser. You can specify a port with `--port 8080`.
72
+
73
+ ### 3. Review and Apply
74
+
75
+ After the AI finishes the migration in the isolated Shadow Workspace, you will receive a `runId`. Apply the completed migration to your actual repository (which automatically creates a new Git branch for safety):
76
+
65
77
  ```bash
66
78
  metamorph apply <runId> .
67
79
  ```
68
80
 
69
- ### 5. Managing Migrations
70
- If you want to discard a run without applying it:
81
+ ### 4. Managing Migrations
82
+
83
+ Discard a run without applying it:
84
+
71
85
  ```bash
72
86
  metamorph rollback <runId>
73
87
  ```
74
- To see a history of all migrations:
88
+
89
+ View migration history:
90
+
75
91
  ```bash
76
92
  metamorph list
77
93
  ```
78
- To clear the local database and reset all events:
94
+
95
+ Clear the local database and reset all events:
96
+
79
97
  ```bash
80
98
  metamorph reset
81
99
  ```
82
100
 
83
101
  ### Utility Commands
84
- To test the technology detection on your repository without starting a migration:
102
+
103
+ Test the technology detection on your repository without starting a migration:
104
+
85
105
  ```bash
86
106
  metamorph detect .
87
107
  ```
88
108
 
89
- ## 🔄 Supported Migrations
109
+ View all available commands and options:
90
110
 
91
- As Metamorph's AI capabilities are constantly expanding, the list of supported architectural shifts is updated frequently.
111
+ ```bash
112
+ metamorph --help
113
+ ```
92
114
 
93
- To see the complete, up-to-date list of all supported frontend and backend framework migrations, please visit our official documentation:
115
+ ## Supported Migrations
94
116
 
95
- 👉 **[View Supported Migrations](https://metamorph.nikelyh.tech/docs/migrations)**
117
+ The list of supported architectural shifts is updated frequently as new migration catalogs are added.
118
+
119
+ See the complete, up-to-date list of all supported frontend and backend framework migrations in the official documentation:
120
+
121
+ **[View Supported Migrations](https://metamorph.nikelyh.tech/docs/migrations)**
96
122
 
97
123
  ---
98
- *Built with ❤️ using Mozaik.*
124
+
125
+ *Built with [Mozaik](https://github.com/jigjoy-ai/mozaik).* 💖
package/bin/metamorph.js CHANGED
File without changes
@@ -0,0 +1,32 @@
1
+ import {
2
+ analyzeCallbackPropMismatches,
3
+ analyzeReactToNextComposition,
4
+ analyzeReactToNextStructure,
5
+ analyzeUnresolvedLocalImports,
6
+ findShadowRoot,
7
+ hasNextAppRoutes,
8
+ isFsdPageComponent,
9
+ isHollowView,
10
+ isSpaPagesTreeFile,
11
+ isSpaRootComponent,
12
+ isStubAppRouterPage,
13
+ listAppRouterFiles,
14
+ nextMigrationFileHint,
15
+ unresolvedLocalImports
16
+ } from "./chunk-H2LZEPNI.js";
17
+ export {
18
+ analyzeCallbackPropMismatches,
19
+ analyzeReactToNextComposition,
20
+ analyzeReactToNextStructure,
21
+ analyzeUnresolvedLocalImports,
22
+ findShadowRoot,
23
+ hasNextAppRoutes,
24
+ isFsdPageComponent,
25
+ isHollowView,
26
+ isSpaPagesTreeFile,
27
+ isSpaRootComponent,
28
+ isStubAppRouterPage,
29
+ listAppRouterFiles,
30
+ nextMigrationFileHint,
31
+ unresolvedLocalImports
32
+ };
@@ -0,0 +1,16 @@
1
+ import {
2
+ detectMonorepo,
3
+ detectTechnologies,
4
+ inspectManifest,
5
+ inspectProject,
6
+ inspectStructure,
7
+ resolveProjectRoot
8
+ } from "./chunk-XLD7NXYU.js";
9
+ export {
10
+ detectMonorepo,
11
+ detectTechnologies,
12
+ inspectManifest,
13
+ inspectProject,
14
+ inspectStructure,
15
+ resolveProjectRoot
16
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
- findMigrationCatalogEntry
3
- } from "./chunk-VEAYUIOZ.js";
2
+ resolveMigrationCatalog
3
+ } from "./chunk-WLHSZWET.js";
4
4
 
5
5
  // ../infrastructure/src/db/SQLiteStateStore.ts
6
6
  import { DatabaseSync } from "node:sqlite";
@@ -50,6 +50,10 @@ var SQLiteStateStore = class {
50
50
  this.db.exec("ALTER TABLE plans ADD COLUMN phase TEXT");
51
51
  } catch (e) {
52
52
  }
53
+ try {
54
+ this.db.exec("ALTER TABLE plans ADD COLUMN outcome TEXT");
55
+ } catch (e) {
56
+ }
53
57
  try {
54
58
  this.db.exec("ALTER TABLE plans ADD COLUMN applied_at TEXT");
55
59
  } catch (e) {
@@ -58,6 +62,10 @@ var SQLiteStateStore = class {
58
62
  this.db.exec("ALTER TABLE plans ADD COLUMN applied_branch TEXT");
59
63
  } catch (e) {
60
64
  }
65
+ try {
66
+ this.db.exec("ALTER TABLE plans ADD COLUMN package_manager TEXT DEFAULT 'npm'");
67
+ } catch (e) {
68
+ }
61
69
  this.db.exec(`
62
70
  CREATE TABLE IF NOT EXISTS tasks (
63
71
  plan_id TEXT,
@@ -80,8 +88,8 @@ var SQLiteStateStore = class {
80
88
  }
81
89
  async savePlan(plan) {
82
90
  const stmtPlan = this.db.prepare(`
83
- INSERT OR REPLACE INTO plans (id, run_id, target_path, source_framework, target_framework, rules_json, created_at, integration_rounds, phase, applied_at, applied_branch)
84
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
91
+ INSERT OR REPLACE INTO plans (id, run_id, target_path, source_framework, target_framework, rules_json, created_at, integration_rounds, phase, outcome, applied_at, applied_branch, package_manager)
92
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
85
93
  `);
86
94
  stmtPlan.run(
87
95
  plan.id,
@@ -93,8 +101,10 @@ var SQLiteStateStore = class {
93
101
  plan.createdAt.toISOString(),
94
102
  plan.integrationRounds ?? 0,
95
103
  plan.phase || "files",
104
+ plan.outcome || null,
96
105
  plan.appliedAt ? plan.appliedAt.toISOString() : null,
97
- plan.appliedBranch || null
106
+ plan.appliedBranch || null,
107
+ plan.packageManager || "npm"
98
108
  );
99
109
  const stmtTask = this.db.prepare(`
100
110
  INSERT OR REPLACE INTO tasks (plan_id, file_path, status, dependencies_json, error)
@@ -115,6 +125,7 @@ var SQLiteStateStore = class {
115
125
  id: planRow.id,
116
126
  runId: planRow.run_id,
117
127
  targetPath: planRow.target_path,
128
+ packageManager: planRow.package_manager || "npm",
118
129
  profile: {
119
130
  source: planRow.source_framework,
120
131
  target: planRow.target_framework,
@@ -123,6 +134,7 @@ var SQLiteStateStore = class {
123
134
  createdAt: new Date(planRow.created_at),
124
135
  integrationRounds: planRow.integration_rounds ?? 0,
125
136
  phase: planRow.phase || "files",
137
+ outcome: planRow.outcome || void 0,
126
138
  appliedAt: planRow.applied_at ? new Date(planRow.applied_at) : void 0,
127
139
  appliedBranch: planRow.applied_branch || void 0,
128
140
  tasks: taskRows.map((row) => ({
@@ -195,7 +207,8 @@ function assertSandbox(filePath, sandboxDir) {
195
207
  const resolved = path.resolve(filePath);
196
208
  if (sandboxDir) {
197
209
  const resolvedSandbox = path.resolve(sandboxDir);
198
- if (!resolved.startsWith(resolvedSandbox)) {
210
+ const isInside = resolved === resolvedSandbox || resolved.startsWith(resolvedSandbox + path.sep);
211
+ if (!isInside) {
199
212
  throw new Error(
200
213
  `BLOCKED: Path "${resolved}" is outside the sandbox "${resolvedSandbox}". All file operations must stay within the shadow workspace.`
201
214
  );
@@ -396,6 +409,16 @@ function createListDirectoryTool(sandboxDir) {
396
409
  }
397
410
  };
398
411
  }
412
+ function createShadowTools(shadowDir) {
413
+ return [
414
+ createReadFileTool(shadowDir),
415
+ createWriteFileTool(shadowDir),
416
+ createRenameFileTool(shadowDir),
417
+ createCreateFileTool(shadowDir),
418
+ createDeleteFileTool(shadowDir),
419
+ createListDirectoryTool(shadowDir)
420
+ ];
421
+ }
399
422
 
400
423
  // ../infrastructure/src/workspace/ShadowWorkspace.ts
401
424
  import fs from "fs-extra";
@@ -454,15 +477,16 @@ var ShadowWorkspace = class {
454
477
  * Cleans up old shadow runs to save disk space, keeping only the specified number of most recent runs.
455
478
  * @param keepCount Number of recent runs to keep (default: 3)
456
479
  */
457
- cleanupOldRuns(keepCount = 3) {
480
+ cleanupOldRuns(keepCount = 3, protectedRunIds = []) {
458
481
  if (!fs.existsSync(this.baseDir)) return;
482
+ const protectedSet = new Set(protectedRunIds);
459
483
  const runs = fs.readdirSync(this.baseDir).filter((f) => f.startsWith("run_") && fs.statSync(join3(this.baseDir, f)).isDirectory());
460
484
  runs.sort();
461
- if (runs.length > keepCount) {
462
- const toDelete = runs.slice(0, runs.length - keepCount);
463
- for (const run of toDelete) {
464
- this.rollback(run);
465
- }
485
+ const unprotected = runs.filter((run) => !protectedSet.has(run));
486
+ if (unprotected.length <= keepCount) return;
487
+ const toDelete = unprotected.slice(0, unprotected.length - keepCount);
488
+ for (const run of toDelete) {
489
+ this.rollback(run);
466
490
  }
467
491
  }
468
492
  };
@@ -472,7 +496,11 @@ import fs2 from "fs-extra";
472
496
  import * as path2 from "path";
473
497
  import { execFileSync } from "child_process";
474
498
  var COPY_IGNORED = ["node_modules", ".git", ".metamorph", "dist", "build", "out", "coverage", ".next"];
475
- var GITIGNORE_ENTRIES = [".metamorph/", ".next/", "node_modules/", "dist/", "build/", "coverage/", ".env"];
499
+ function isCopyIgnored(itemName) {
500
+ if (COPY_IGNORED.includes(itemName)) return true;
501
+ if (itemName === ".env" || itemName.startsWith(".env.")) return true;
502
+ return false;
503
+ }
476
504
  var MigrationIntegrator = class {
477
505
  shadowWorkspace;
478
506
  constructor(shadowWorkspace) {
@@ -494,6 +522,20 @@ var MigrationIntegrator = class {
494
522
  "Metamorph apply requires a Git repository. Run this from a project that already has git (including monorepo packages whose .git lives in a parent folder). Nested git init is not supported."
495
523
  );
496
524
  }
525
+ const rootPkgJson = path2.join(gitRoot, "package.json");
526
+ if (fs2.existsSync(rootPkgJson)) {
527
+ try {
528
+ const pkg = JSON.parse(fs2.readFileSync(rootPkgJson, "utf-8"));
529
+ const normalizedTarget = resolvedTarget.replace(/\\/g, "/");
530
+ if (pkg.name === "metamorph-monorepo" && normalizedTarget.includes("/scratch/")) {
531
+ throw new Error(
532
+ `BLOCKED: Cannot apply to "${resolvedTarget}" because it is inside the development scratch/ directory of the Metamorph repository. Use a standalone git repository for user project migrations.`
533
+ );
534
+ }
535
+ } catch (e) {
536
+ if (e instanceof Error && e.message.startsWith("BLOCKED:")) throw e;
537
+ }
538
+ }
497
539
  if (this.isIgnoredByGit(gitRoot, resolvedTarget)) {
498
540
  throw new Error(
499
541
  `Cannot apply via Git: "${resolvedTarget}" is ignored by the repository at "${gitRoot}" (.gitignore). Metamorph will not create branches on a parent repo for ignored sandboxes. Use a standalone git project, or a tracked package inside a monorepo.`
@@ -536,22 +578,17 @@ var MigrationIntegrator = class {
536
578
  throw new Error(`git ${args.join(" ")} failed: ${detail || "unknown git error"}`);
537
579
  }
538
580
  }
539
- ensureApplyGitignore(gitRoot) {
540
- const gitignorePath = path2.join(gitRoot, ".gitignore");
541
- const existing = fs2.existsSync(gitignorePath) ? fs2.readFileSync(gitignorePath, "utf-8") : "";
542
- const missing = GITIGNORE_ENTRIES.filter((entry) => {
543
- const name = entry.replace(/\/$/, "").replace(".", "\\.");
544
- const pattern = new RegExp(`(^|\\n)${name}(/)?(\\n|$)`);
545
- return !pattern.test(existing.replace(/\r/g, "\n"));
546
- });
547
- if (missing.length === 0) return;
548
- const block = `
549
- # Metamorph apply
550
- ${missing.join("\n")}
551
- `;
552
- fs2.appendFileSync(gitignorePath, block, "utf-8");
553
- }
554
581
  async applyWithGit(runId, shadowDir, targetPath, gitRoot, branchName) {
582
+ const relativeTarget = path2.relative(gitRoot, targetPath).replace(/\\/g, "/");
583
+ const addPath = relativeTarget === "" ? "." : relativeTarget;
584
+ const dirtyStatus = this.git(["status", "--porcelain", "--", addPath], gitRoot).trim();
585
+ if (dirtyStatus) {
586
+ throw new Error(
587
+ `Cannot apply migration: the target directory has uncommitted changes:
588
+ ${dirtyStatus}
589
+ Please commit or stash your changes before applying.`
590
+ );
591
+ }
555
592
  try {
556
593
  this.git(["checkout", "-B", branchName], gitRoot);
557
594
  } catch (e) {
@@ -561,9 +598,6 @@ ${missing.join("\n")}
561
598
  throw new Error(`Failed to create/reset git branch ${branchName}: ${String(e)}`);
562
599
  }
563
600
  this.copyShadowToTarget(shadowDir, targetPath);
564
- this.ensureApplyGitignore(gitRoot);
565
- const relativeTarget = path2.relative(gitRoot, targetPath).replace(/\\/g, "/");
566
- const addPath = relativeTarget === "" ? "." : relativeTarget;
567
601
  try {
568
602
  try {
569
603
  this.git(["-c", "advice.addIgnoredFile=false", "add", "-A", "--", addPath], gitRoot);
@@ -597,13 +631,13 @@ ${missing.join("\n")}
597
631
  if (fs2.existsSync(targetPath)) {
598
632
  const targetItems = fs2.readdirSync(targetPath);
599
633
  for (const item of targetItems) {
600
- if (COPY_IGNORED.includes(item)) continue;
634
+ if (isCopyIgnored(item)) continue;
601
635
  fs2.removeSync(path2.join(targetPath, item));
602
636
  }
603
637
  }
604
638
  const shadowItems = fs2.readdirSync(shadowDir);
605
639
  for (const item of shadowItems) {
606
- if (COPY_IGNORED.includes(item)) continue;
640
+ if (isCopyIgnored(item)) continue;
607
641
  const itemSrc = path2.join(shadowDir, item);
608
642
  const itemDest = path2.join(targetPath, item);
609
643
  fs2.copySync(itemSrc, itemDest, { overwrite: true });
@@ -677,12 +711,16 @@ async function createApiServer(store, migrationRunner) {
677
711
  });
678
712
  app.get("/api/detect", async (req, res) => {
679
713
  try {
680
- const { detectTechnologies } = await import("./TechDetector-5YP44RVR.js");
714
+ const { detectTechnologies: detectTechnologies2, inspectProject: inspectProject2, detectMonorepo } = await import("./ProjectDetector-WU3OD6WT.js");
681
715
  const targetPath = req.query.path ? String(req.query.path) : ".";
682
- const detected = detectTechnologies(targetPath);
716
+ const monorepo = detectMonorepo(targetPath);
717
+ const profiles = inspectProject2(targetPath);
718
+ const detected = detectTechnologies2(targetPath);
683
719
  res.json({
684
720
  detected,
685
- primary: detected.length > 0 ? detected[0].framework : null
721
+ profiles,
722
+ monorepo: monorepo.isMonorepo ? monorepo : null,
723
+ primary: profiles.length > 0 ? profiles[0].framework : null
686
724
  });
687
725
  } catch (error) {
688
726
  if (error instanceof Error) {
@@ -702,7 +740,7 @@ async function createApiServer(store, migrationRunner) {
702
740
  });
703
741
  return;
704
742
  }
705
- const catalogEntry = findMigrationCatalogEntry(from, to);
743
+ const catalogEntry = resolveMigrationCatalog(from, to);
706
744
  if (!catalogEntry) {
707
745
  res.status(400).json({
708
746
  error: `Unsupported migration profile: ${from} -> ${to}. Please check the supported catalog.`
@@ -732,15 +770,29 @@ async function createApiServer(store, migrationRunner) {
732
770
  });
733
771
  return;
734
772
  }
735
- const integrator = new MigrationIntegrator(migrationRunner.workspace);
736
- const result = await integrator.applyMigration(runId, targetPath);
737
773
  const plans = await store.getAllPlans();
738
774
  const plan = plans.find((item) => item.runId === runId);
739
- if (plan) {
740
- plan.appliedAt = /* @__PURE__ */ new Date();
741
- plan.appliedBranch = result.branch;
742
- await store.savePlan(plan);
775
+ if (!plan) {
776
+ res.status(404).json({ error: `Plan for run "${runId}" not found in database.` });
777
+ return;
778
+ }
779
+ if (plan.phase !== "completed" || plan.outcome !== "success") {
780
+ res.status(400).json({
781
+ error: `Cannot apply migration: plan is not completed successfully (phase: "${plan.phase}", outcome: "${plan.outcome || "none"}"). Only successful migrations can be applied.`
782
+ });
783
+ return;
743
784
  }
785
+ if (plan.appliedAt) {
786
+ res.status(400).json({
787
+ error: `Migration for run "${runId}" has already been applied at ${new Date(plan.appliedAt).toISOString()} to branch ${plan.appliedBranch || "unknown"}.`
788
+ });
789
+ return;
790
+ }
791
+ const integrator = new MigrationIntegrator(migrationRunner.workspace);
792
+ const result = await integrator.applyMigration(runId, targetPath);
793
+ plan.appliedAt = /* @__PURE__ */ new Date();
794
+ plan.appliedBranch = result.branch;
795
+ await store.savePlan(plan);
744
796
  await store.logEvent("migration.applied", { runId, branch: result.branch, targetPath });
745
797
  res.json({ ok: true, ...result });
746
798
  } catch (error) {
@@ -918,7 +970,11 @@ function parseImplicatedFiles(output, shadowPath) {
918
970
  const resolved = path4.resolve(abs);
919
971
  const inside = resolved === root || resolved.startsWith(root + path4.sep);
920
972
  if (!inside) return;
921
- if (fs3.existsSync(resolved) && fs3.statSync(resolved).isFile()) found.add(resolved);
973
+ if (fs3.existsSync(resolved)) {
974
+ if (fs3.statSync(resolved).isFile()) found.add(resolved);
975
+ } else {
976
+ found.add(resolved);
977
+ }
922
978
  };
923
979
  for (const re of patterns) {
924
980
  let match;
@@ -953,13 +1009,16 @@ ${result.output}`;
953
1009
 
954
1010
  export {
955
1011
  SQLiteStateStore,
1012
+ assertSandbox,
956
1013
  createReadFileTool,
957
1014
  createWriteFileTool,
958
1015
  createRenameFileTool,
959
1016
  createCreateFileTool,
960
1017
  createDeleteFileTool,
961
1018
  createListDirectoryTool,
1019
+ createShadowTools,
962
1020
  ShadowWorkspace,
1021
+ isCopyIgnored,
963
1022
  MigrationIntegrator,
964
1023
  createApiServer,
965
1024
  createCheckProjectDiagnosticsTool,