sonance-brand-mcp 1.3.24 → 1.3.26

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.
@@ -55,69 +55,63 @@ interface BackupManifest {
55
55
 
56
56
  const BACKUP_ROOT = ".sonance-backups";
57
57
 
58
- const VISION_SYSTEM_PROMPT = `You are a React/Tailwind CSS expert with vision capabilities for the Sonance brand system.
58
+ const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
59
59
 
60
- You can see screenshots of web pages and understand their visual layout, then modify code to implement requested changes.
60
+ ═══════════════════════════════════════════════════════════════════════════════
61
+ UNDERSTAND THE USER'S REQUEST NATURALLY
62
+ ═══════════════════════════════════════════════════════════════════════════════
63
+
64
+ Read the user's request and do exactly what they ask. You know React, TypeScript, Tailwind CSS, and JSX - use that knowledge naturally.
65
+
66
+ - If they say "add X", ADD X to the code (don't replace or remove anything else)
67
+ - If they say "change X to Y", find X and change it to Y
68
+ - If they say "make X bigger/smaller/different color", adjust the relevant properties
69
+ - If they say "remove X", remove X
70
+ - If they say "wrap X with Y", add Y as a parent around X
71
+
72
+ For any change:
73
+ 1. Read the code context provided
74
+ 2. Understand what the user wants
75
+ 3. Generate patches that accomplish exactly that
76
+ 4. If adding a component/icon requires an import, include a patch for the import too
77
+
78
+ Don't overthink - just make the change the user requested.
61
79
 
62
80
  ═══════════════════════════════════════════════════════════════════════════════
63
- ABSOLUTE RULES - VIOLATION WILL BREAK THE APPLICATION
81
+ PATCH FORMAT
64
82
  ═══════════════════════════════════════════════════════════════════════════════
65
83
 
66
- **SURGICAL EDITING (MOST CRITICAL):**
67
- 1. You are making SURGICAL EDITS, not rewriting files
68
- 2. Change ONLY the specific lines that need modification
69
- 3. The modified file MUST have approximately the same line count as the original
70
- 4. If original file has 200 lines, modified file should have ~195-210 lines
71
- 5. NEVER rewrite a file from scratch - this destroys the application
72
-
73
- **FILE RULES:**
74
- 6. You may ONLY edit files that are provided in the PAGE CONTEXT section
75
- 7. NEVER create new files - only modify existing ones shown to you
76
- 8. The filePath in your response MUST exactly match one of the provided file paths
77
-
78
- **PRESERVATION RULES (ZERO TOLERANCE):**
79
- 9. NEVER delete or remove existing functions, hooks, state, or logic
80
- 10. NEVER remove imports - you may only ADD imports if needed
81
- 11. NEVER remove useEffect, useState, useCallback, or other React hooks
82
- 12. NEVER remove API calls, fetch requests, or data loading logic
83
- 13. NEVER remove error handling, loading states, or conditional rendering
84
- 14. NEVER remove data-sonance-* attributes
85
- 15. NEVER change component structure unless specifically requested
86
- 16. NEVER modify TypeScript types or interfaces unless specifically requested
87
-
88
- **CHANGE RULES:**
89
- 17. Make ONLY the changes requested by the user
90
- 18. Modify the MINIMUM amount of code necessary
91
- 19. Keep all existing className values and ADD to them if needed
92
- 20. Use semantic Tailwind classes (bg-primary, text-foreground, etc.)
93
- 21. Maintain dark mode compatibility with CSS variables
94
- 22. Keep the cn() utility for className merging
95
- 23. CRITICAL: You MUST return the FULL file content in "modifiedContent". Do NOT use comments like "// ... existing code ..." or "// ... rest of file ...". Return every single line of code, even if unchanged.
84
+ Return search/replace patches (NOT full files). The system applies your patches to the original.
85
+
86
+ **Patch Rules:**
87
+ - "search" must match the original code EXACTLY (including whitespace/indentation)
88
+ - "replace" contains your modified version
89
+ - Include 2-4 lines of context in "search" to make it unique
90
+ - You may ONLY edit files provided in the PAGE CONTEXT section
96
91
 
97
92
  **SONANCE BRAND COLORS:**
98
- - Charcoal: #333F48, #343D46 (primary)
99
- - Silver: #E2E2E2, #D1D1D6 (secondary)
93
+ - Charcoal: #333F48 (primary text)
100
94
  - IPORT Orange: #FC4C02
101
- - IPORT Dark: #0F161D
102
95
  - Blaze Blue: #00A3E1
103
- - Blaze Red: #C02B0A
104
96
 
105
97
  **RESPONSE FORMAT:**
106
- Return ONLY a valid JSON object. Do not include any conversational text before or after the JSON.
107
- The JSON must include:
108
- - "reasoning": Brief explanation of what you see in the screenshot and your plan
109
- - "modifications": Array of file modifications, each with:
110
- - "filePath": Path to the file
111
- - "modifiedContent": Complete updated file content (MUST BE FULL CONTENT, NO TRUNCATION)
112
- - "explanation": What changed in this file
113
- - "explanation": Overall summary of changes
114
-
115
- **EXAMPLE OF CORRECT EDIT:**
116
- If user asks to "make buttons smaller", you should:
117
- - Find the button elements in the code
118
- - Change ONLY the size-related classes (e.g., h-10 -> h-8, px-4 -> px-3)
119
- - Keep ALL other code exactly the same
120
- - Return the FULL file with this tiny change`;
98
+ Return ONLY valid JSON:
99
+ {
100
+ "reasoning": "What you understood from the request and your plan",
101
+ "modifications": [
102
+ {
103
+ "filePath": "path/to/file.tsx",
104
+ "patches": [
105
+ {
106
+ "search": "exact code to find",
107
+ "replace": "the replacement code",
108
+ "explanation": "what this patch does"
109
+ }
110
+ ]
111
+ }
112
+ ],
113
+ "explanation": "summary of changes made"
114
+ }`;
121
115
 
122
116
  export async function POST(request: Request) {
123
117
  // Only allow in development
@@ -236,14 +230,38 @@ ${pageContext.pageContent ? `\`\`\`tsx\n${pageContext.pageContent}\n\`\`\`` : ""
236
230
  `;
237
231
 
238
232
  if (pageContext.componentSources.length > 0) {
239
- textContent += `IMPORTED COMPONENTS:\n`;
240
- for (const comp of pageContext.componentSources) {
233
+ // Smart truncation: prioritize first components (direct imports) and limit total context
234
+ const MAX_TOTAL_CONTEXT = 80000; // ~80k chars to stay well under Claude's limit
235
+ const MAX_PER_FILE_PRIORITY = 4000; // First 10 files get more space
236
+ const MAX_PER_FILE_SECONDARY = 1500; // Remaining files get less
237
+ const MAX_FILES = 30; // Limit total number of files
238
+
239
+ let usedContext = pageContext.pageContent.length + pageContext.globalsCSS.length;
240
+ const truncatedComponents = pageContext.componentSources.slice(0, MAX_FILES);
241
+
242
+ textContent += `IMPORTED COMPONENTS (${truncatedComponents.length} files, ${pageContext.componentSources.length > MAX_FILES ? `${pageContext.componentSources.length - MAX_FILES} omitted` : 'complete'}):\n`;
243
+
244
+ for (let i = 0; i < truncatedComponents.length; i++) {
245
+ const comp = truncatedComponents[i];
246
+ const isPriority = i < 10; // First 10 files are priority (direct imports)
247
+ const maxSize = isPriority ? MAX_PER_FILE_PRIORITY : MAX_PER_FILE_SECONDARY;
248
+
249
+ // Stop if we've used too much context
250
+ if (usedContext > MAX_TOTAL_CONTEXT) {
251
+ textContent += `\n// ... (${truncatedComponents.length - i} more files omitted to stay within context limits)\n`;
252
+ break;
253
+ }
254
+
255
+ const truncatedContent = comp.content.substring(0, maxSize);
256
+ const wasTruncated = comp.content.length > maxSize;
257
+
241
258
  textContent += `
242
- File: ${comp.path}
259
+ File: ${comp.path}${isPriority ? '' : ' (nested)'}
243
260
  \`\`\`tsx
244
- ${comp.content.substring(0, 3000)}${comp.content.length > 3000 ? "\n// ... (truncated)" : ""}
261
+ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
245
262
  \`\`\`
246
263
  `;
264
+ usedContext += truncatedContent.length;
247
265
  }
248
266
  }
249
267
 
@@ -296,13 +314,15 @@ CRITICAL: Your modified file should have approximately the same number of lines
296
314
  );
297
315
  }
298
316
 
299
- // Parse AI response
317
+ // Parse AI response - now expecting patches instead of full file content
300
318
  let aiResponse: {
301
319
  reasoning?: string;
302
320
  modifications: Array<{
303
321
  filePath: string;
304
- modifiedContent: string;
305
- explanation: string;
322
+ patches?: Patch[];
323
+ // Legacy support for modifiedContent (will be deprecated)
324
+ modifiedContent?: string;
325
+ explanation?: string;
306
326
  }>;
307
327
  explanation?: string;
308
328
  };
@@ -344,54 +364,104 @@ CRITICAL: Your modified file should have approximately the same number of lines
344
364
  });
345
365
  }
346
366
 
347
- // Read original content and prepare modifications
367
+ // Build set of valid file paths from page context
368
+ const validFilePaths = new Set<string>();
369
+ if (pageContext.pageFile) {
370
+ validFilePaths.add(pageContext.pageFile);
371
+ }
372
+ for (const comp of pageContext.componentSources) {
373
+ validFilePaths.add(comp.path);
374
+ }
375
+
376
+ // Process modifications - apply patches to get modified content
348
377
  const modifications: VisionFileModification[] = [];
349
- const validationErrors: string[] = [];
350
- const validationWarnings: string[] = [];
378
+ const patchErrors: string[] = [];
351
379
 
352
380
  for (const mod of aiResponse.modifications) {
381
+ // Validate that the file path is in the page context
382
+ // This prevents the AI from creating new files
383
+ if (!validFilePaths.has(mod.filePath)) {
384
+ console.warn(`[Apply-First] Rejected modification to unknown file: ${mod.filePath}`);
385
+ console.warn(`[Apply-First] Valid files are: ${Array.from(validFilePaths).join(", ")}`);
386
+ patchErrors.push(`${mod.filePath}: This file was not found in the page context. The AI can only modify existing files that are part of the current page.`);
387
+ continue;
388
+ }
389
+
353
390
  const fullPath = path.join(projectRoot, mod.filePath);
354
391
  let originalContent = "";
355
392
  if (fs.existsSync(fullPath)) {
356
393
  originalContent = fs.readFileSync(fullPath, "utf-8");
357
394
  }
358
395
 
359
- // Validate the modification
360
- const validation = validateModification(originalContent, mod.modifiedContent, mod.filePath);
361
-
362
- if (!validation.valid) {
363
- validationErrors.push(validation.error || "Unknown validation error");
364
- continue; // Skip this modification
365
- }
366
-
367
- if (validation.warnings.length > 0) {
368
- validationWarnings.push(...validation.warnings.map(w => `${mod.filePath}: ${w}`));
396
+ let modifiedContent: string;
397
+ let explanation = mod.explanation || "";
398
+
399
+ // Check if AI returned patches (new format) or modifiedContent (legacy)
400
+ if (mod.patches && mod.patches.length > 0) {
401
+ // New patch-based approach
402
+ console.log(`[Apply-First] Applying ${mod.patches.length} patches to ${mod.filePath}`);
403
+
404
+ const patchResult = applyPatches(originalContent, mod.patches);
405
+
406
+ if (!patchResult.success) {
407
+ const failedMessages = patchResult.failedPatches.map(
408
+ (f) => `Patch failed: ${f.error}`
409
+ ).join("\n");
410
+ patchErrors.push(`${mod.filePath}:\n${failedMessages}`);
411
+
412
+ // If some patches succeeded, use partial result
413
+ if (patchResult.appliedPatches > 0) {
414
+ console.warn(`[Apply-First] ${patchResult.appliedPatches}/${mod.patches.length} patches applied to ${mod.filePath}`);
415
+ modifiedContent = patchResult.modifiedContent;
416
+ explanation += ` (${patchResult.appliedPatches}/${mod.patches.length} patches applied)`;
417
+ } else {
418
+ continue; // Skip this file entirely if no patches worked
419
+ }
420
+ } else {
421
+ modifiedContent = patchResult.modifiedContent;
422
+ console.log(`[Apply-First] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
423
+ }
424
+ } else if (mod.modifiedContent) {
425
+ // Legacy: AI returned full file content
426
+ console.warn(`[Apply-First] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
427
+ modifiedContent = mod.modifiedContent;
428
+
429
+ // Validate the modification using legacy validation
430
+ const validation = validateModification(originalContent, modifiedContent, mod.filePath);
431
+ if (!validation.valid) {
432
+ patchErrors.push(`${mod.filePath}: ${validation.error}`);
433
+ continue;
434
+ }
435
+ } else {
436
+ // No patches and no modifiedContent - skip
437
+ console.warn(`[Apply-First] No patches or modifiedContent for ${mod.filePath}`);
438
+ continue;
369
439
  }
370
440
 
371
441
  modifications.push({
372
442
  filePath: mod.filePath,
373
443
  originalContent,
374
- modifiedContent: mod.modifiedContent,
375
- diff: generateSimpleDiff(originalContent, mod.modifiedContent),
376
- explanation: mod.explanation,
444
+ modifiedContent,
445
+ diff: generateSimpleDiff(originalContent, modifiedContent),
446
+ explanation,
377
447
  });
378
448
  }
379
449
 
380
- // If all modifications failed validation, return error
381
- if (validationErrors.length > 0 && modifications.length === 0) {
382
- console.error("All AI modifications failed validation:", validationErrors);
450
+ // If all modifications failed, return error
451
+ if (patchErrors.length > 0 && modifications.length === 0) {
452
+ console.error("All AI patches failed:", patchErrors);
383
453
  return NextResponse.json(
384
454
  {
385
455
  success: false,
386
- error: validationErrors.join("\n\n"),
456
+ error: `Patch application failed:\n\n${patchErrors.join("\n\n")}`,
387
457
  },
388
458
  { status: 400 }
389
459
  );
390
460
  }
391
461
 
392
- // Log warnings for review
393
- if (validationWarnings.length > 0) {
394
- console.warn("Vision apply validation warnings:", validationWarnings);
462
+ // Log patch errors as warnings if some modifications succeeded
463
+ if (patchErrors.length > 0) {
464
+ console.warn("Some patches failed:", patchErrors);
395
465
  }
396
466
 
397
467
  // Create backups and apply changes atomically
@@ -575,8 +645,109 @@ async function revertFromBackups(
575
645
  }
576
646
  }
577
647
 
648
+ /**
649
+ * Recursively gather all imports from a file up to a max depth
650
+ * This builds a complete component graph for the AI to understand
651
+ */
652
+ function gatherAllImports(
653
+ filePath: string,
654
+ projectRoot: string,
655
+ visited: Set<string> = new Set(),
656
+ maxDepth: number = 4
657
+ ): { path: string; content: string }[] {
658
+ // Prevent infinite loops and limit total files
659
+ if (visited.has(filePath) || visited.size > 50) return [];
660
+ visited.add(filePath);
661
+
662
+ const results: { path: string; content: string }[] = [];
663
+ const fullPath = path.join(projectRoot, filePath);
664
+
665
+ if (!fs.existsSync(fullPath)) return results;
666
+
667
+ try {
668
+ const content = fs.readFileSync(fullPath, "utf-8");
669
+ results.push({ path: filePath, content });
670
+
671
+ // Continue recursing if we haven't hit max depth
672
+ if (maxDepth > 0) {
673
+ const imports = extractImports(content);
674
+ for (const imp of imports) {
675
+ const resolved = resolveImportPath(imp, filePath, projectRoot);
676
+ if (resolved && !visited.has(resolved)) {
677
+ const nestedImports = gatherAllImports(resolved, projectRoot, visited, maxDepth - 1);
678
+ results.push(...nestedImports);
679
+ }
680
+ }
681
+ }
682
+ } catch {
683
+ // Skip files that can't be read
684
+ }
685
+
686
+ return results;
687
+ }
688
+
689
+ /**
690
+ * Discover layout files that wrap the page
691
+ * App Router: layout.tsx in same and parent directories
692
+ * Pages Router: _app.tsx and _document.tsx
693
+ */
694
+ function discoverLayoutFiles(pageFile: string | null, projectRoot: string): string[] {
695
+ const layoutFiles: string[] = [];
696
+
697
+ if (!pageFile) return layoutFiles;
698
+
699
+ // Determine if App Router or Pages Router
700
+ const isAppRouter = pageFile.includes("/app/") || pageFile.startsWith("app/");
701
+ const isPagesRouter = pageFile.includes("/pages/") || pageFile.startsWith("pages/");
702
+
703
+ if (isAppRouter) {
704
+ // App Router: Check for layout.tsx in same directory and parent directories
705
+ let currentDir = path.dirname(pageFile);
706
+ const appRoot = pageFile.includes("src/app") ? "src/app" : "app";
707
+
708
+ while (currentDir.includes(appRoot)) {
709
+ const layoutPatterns = [
710
+ path.join(currentDir, "layout.tsx"),
711
+ path.join(currentDir, "layout.jsx"),
712
+ ];
713
+
714
+ for (const layoutPath of layoutPatterns) {
715
+ if (fs.existsSync(path.join(projectRoot, layoutPath))) {
716
+ layoutFiles.push(layoutPath);
717
+ }
718
+ }
719
+
720
+ // Move to parent directory
721
+ const parentDir = path.dirname(currentDir);
722
+ if (parentDir === currentDir) break;
723
+ currentDir = parentDir;
724
+ }
725
+ }
726
+
727
+ if (isPagesRouter) {
728
+ // Pages Router: Check for _app.tsx and _document.tsx
729
+ const pagesRoot = pageFile.includes("src/pages") ? "src/pages" : "pages";
730
+
731
+ const pagesRouterLayouts = [
732
+ `${pagesRoot}/_app.tsx`,
733
+ `${pagesRoot}/_app.jsx`,
734
+ `${pagesRoot}/_document.tsx`,
735
+ `${pagesRoot}/_document.jsx`,
736
+ ];
737
+
738
+ for (const layoutPath of pagesRouterLayouts) {
739
+ if (fs.existsSync(path.join(projectRoot, layoutPath))) {
740
+ layoutFiles.push(layoutPath);
741
+ }
742
+ }
743
+ }
744
+
745
+ return layoutFiles;
746
+ }
747
+
578
748
  /**
579
749
  * Gather context about the current page for AI analysis
750
+ * Uses recursive import resolution to build complete component graph
580
751
  */
581
752
  function gatherPageContext(
582
753
  pageRoute: string,
@@ -590,50 +761,102 @@ function gatherPageContext(
590
761
  const pageFile = discoverPageFile(pageRoute, projectRoot);
591
762
  let pageContent = "";
592
763
  const componentSources: { path: string; content: string }[] = [];
764
+ const visited = new Set<string>();
593
765
 
594
766
  if (pageFile) {
595
767
  const fullPath = path.join(projectRoot, pageFile);
596
768
  if (fs.existsSync(fullPath)) {
597
769
  pageContent = fs.readFileSync(fullPath, "utf-8");
770
+ visited.add(pageFile);
598
771
 
772
+ // Recursively gather all imported components (up to 4 levels deep)
599
773
  const imports = extractImports(pageContent);
600
774
  for (const importPath of imports) {
601
775
  const resolvedPath = resolveImportPath(importPath, pageFile, projectRoot);
602
- if (resolvedPath && fs.existsSync(path.join(projectRoot, resolvedPath))) {
603
- try {
604
- const content = fs.readFileSync(path.join(projectRoot, resolvedPath), "utf-8");
605
- componentSources.push({ path: resolvedPath, content });
606
- } catch {
607
- // Skip files that can't be read
608
- }
776
+ if (resolvedPath && !visited.has(resolvedPath)) {
777
+ const nestedComponents = gatherAllImports(resolvedPath, projectRoot, visited, 3);
778
+ componentSources.push(...nestedComponents);
779
+ }
780
+ }
781
+
782
+ // Also include layout files
783
+ const layoutFiles = discoverLayoutFiles(pageFile, projectRoot);
784
+ for (const layoutFile of layoutFiles) {
785
+ if (!visited.has(layoutFile)) {
786
+ const layoutComponents = gatherAllImports(layoutFile, projectRoot, visited, 2);
787
+ componentSources.push(...layoutComponents);
609
788
  }
610
789
  }
611
790
  }
612
791
  }
613
792
 
614
793
  let globalsCSS = "";
615
- const globalsPath = path.join(projectRoot, "src/app/globals.css");
616
- if (fs.existsSync(globalsPath)) {
617
- globalsCSS = fs.readFileSync(globalsPath, "utf-8");
794
+ const globalsCSSPatterns = [
795
+ "src/app/globals.css",
796
+ "app/globals.css",
797
+ "src/styles/globals.css",
798
+ "styles/globals.css",
799
+ "src/styles/global.css",
800
+ "styles/global.css",
801
+ ];
802
+
803
+ for (const cssPattern of globalsCSSPatterns) {
804
+ const globalsPath = path.join(projectRoot, cssPattern);
805
+ if (fs.existsSync(globalsPath)) {
806
+ globalsCSS = fs.readFileSync(globalsPath, "utf-8");
807
+ break;
808
+ }
618
809
  }
619
810
 
620
811
  return { pageFile, pageContent, componentSources, globalsCSS };
621
812
  }
622
813
 
623
814
  function discoverPageFile(route: string, projectRoot: string): string | null {
815
+ // Handle root route
624
816
  if (route === "/" || route === "") {
625
- const rootPage = "src/app/page.tsx";
626
- if (fs.existsSync(path.join(projectRoot, rootPage))) {
627
- return rootPage;
817
+ const rootPatterns = [
818
+ // App Router patterns
819
+ "src/app/page.tsx",
820
+ "src/app/page.jsx",
821
+ "app/page.tsx",
822
+ "app/page.jsx",
823
+ // Pages Router patterns
824
+ "src/pages/index.tsx",
825
+ "src/pages/index.jsx",
826
+ "pages/index.tsx",
827
+ "pages/index.jsx",
828
+ ];
829
+
830
+ for (const pattern of rootPatterns) {
831
+ if (fs.existsSync(path.join(projectRoot, pattern))) {
832
+ return pattern;
833
+ }
628
834
  }
629
835
  return null;
630
836
  }
631
837
 
632
838
  const cleanRoute = route.replace(/^\//, "");
839
+
840
+ // First, try exact match patterns
633
841
  const patterns = [
842
+ // App Router patterns (with src)
634
843
  `src/app/${cleanRoute}/page.tsx`,
635
844
  `src/app/${cleanRoute}/page.jsx`,
636
- `src/app/${cleanRoute}.tsx`,
845
+ // App Router patterns (without src)
846
+ `app/${cleanRoute}/page.tsx`,
847
+ `app/${cleanRoute}/page.jsx`,
848
+ // Pages Router patterns (with src) - file-based
849
+ `src/pages/${cleanRoute}.tsx`,
850
+ `src/pages/${cleanRoute}.jsx`,
851
+ // Pages Router patterns (with src) - folder-based
852
+ `src/pages/${cleanRoute}/index.tsx`,
853
+ `src/pages/${cleanRoute}/index.jsx`,
854
+ // Pages Router patterns (without src) - file-based
855
+ `pages/${cleanRoute}.tsx`,
856
+ `pages/${cleanRoute}.jsx`,
857
+ // Pages Router patterns (without src) - folder-based
858
+ `pages/${cleanRoute}/index.tsx`,
859
+ `pages/${cleanRoute}/index.jsx`,
637
860
  ];
638
861
 
639
862
  for (const pattern of patterns) {
@@ -642,6 +865,99 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
642
865
  }
643
866
  }
644
867
 
868
+ // If exact match not found, try dynamic route matching
869
+ // e.g., /processes/123 -> src/pages/processes/[id].tsx
870
+ const dynamicResult = findDynamicRoute(cleanRoute, projectRoot);
871
+ if (dynamicResult) {
872
+ return dynamicResult;
873
+ }
874
+
875
+ return null;
876
+ }
877
+
878
+ /**
879
+ * Find dynamic route files when exact match fails
880
+ * Maps runtime routes like "/processes/123" to file paths like "src/pages/processes/[id].tsx"
881
+ */
882
+ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | null {
883
+ const segments = cleanRoute.split("/");
884
+
885
+ // Try replacing the last segment with dynamic patterns
886
+ const baseDirs = [
887
+ "src/app",
888
+ "app",
889
+ "src/pages",
890
+ "pages",
891
+ ];
892
+
893
+ const dynamicPatterns = ["[id]", "[slug]", "[...slug]", "[[...slug]]"];
894
+ const extensions = [".tsx", ".jsx"];
895
+
896
+ for (const baseDir of baseDirs) {
897
+ const basePath = path.join(projectRoot, baseDir);
898
+ if (!fs.existsSync(basePath)) continue;
899
+
900
+ // Build path with all segments except the last one
901
+ const parentSegments = segments.slice(0, -1);
902
+ const parentPath = parentSegments.length > 0
903
+ ? path.join(basePath, ...parentSegments)
904
+ : basePath;
905
+
906
+ if (!fs.existsSync(parentPath)) continue;
907
+
908
+ // Check for dynamic route files
909
+ for (const dynPattern of dynamicPatterns) {
910
+ for (const ext of extensions) {
911
+ // App Router: parent/[id]/page.tsx
912
+ if (baseDir.includes("app")) {
913
+ const appRouterPath = path.join(parentPath, dynPattern, `page${ext}`);
914
+ if (fs.existsSync(appRouterPath)) {
915
+ return path.relative(projectRoot, appRouterPath);
916
+ }
917
+ }
918
+
919
+ // Pages Router: parent/[id].tsx
920
+ const pagesRouterFile = path.join(parentPath, `${dynPattern}${ext}`);
921
+ if (fs.existsSync(pagesRouterFile)) {
922
+ return path.relative(projectRoot, pagesRouterFile);
923
+ }
924
+
925
+ // Pages Router: parent/[id]/index.tsx
926
+ const pagesRouterDir = path.join(parentPath, dynPattern, `index${ext}`);
927
+ if (fs.existsSync(pagesRouterDir)) {
928
+ return path.relative(projectRoot, pagesRouterDir);
929
+ }
930
+ }
931
+ }
932
+
933
+ // Also scan directory for any dynamic segment pattern [...]
934
+ try {
935
+ const entries = fs.readdirSync(parentPath, { withFileTypes: true });
936
+ for (const entry of entries) {
937
+ if (entry.name.startsWith("[") && entry.name.includes("]")) {
938
+ for (const ext of extensions) {
939
+ if (entry.isDirectory()) {
940
+ // App Router or Pages Router with folder
941
+ const pagePath = path.join(parentPath, entry.name, `page${ext}`);
942
+ const indexPath = path.join(parentPath, entry.name, `index${ext}`);
943
+ if (fs.existsSync(pagePath)) {
944
+ return path.relative(projectRoot, pagePath);
945
+ }
946
+ if (fs.existsSync(indexPath)) {
947
+ return path.relative(projectRoot, indexPath);
948
+ }
949
+ } else if (entry.isFile() && entry.name.endsWith(ext)) {
950
+ // Pages Router file-based
951
+ return path.relative(projectRoot, path.join(parentPath, entry.name));
952
+ }
953
+ }
954
+ }
955
+ }
956
+ } catch {
957
+ // Skip if directory can't be read
958
+ }
959
+ }
960
+
645
961
  return null;
646
962
  }
647
963
 
@@ -709,6 +1025,76 @@ function generateSimpleDiff(original: string, modified: string): string {
709
1025
  return diff.join("\n");
710
1026
  }
711
1027
 
1028
+ /**
1029
+ * Patch interface for search/replace operations
1030
+ */
1031
+ interface Patch {
1032
+ search: string;
1033
+ replace: string;
1034
+ explanation: string;
1035
+ }
1036
+
1037
+ /**
1038
+ * Result of applying patches to a file
1039
+ */
1040
+ interface ApplyPatchesResult {
1041
+ success: boolean;
1042
+ modifiedContent: string;
1043
+ appliedPatches: number;
1044
+ failedPatches: { patch: Patch; error: string }[];
1045
+ }
1046
+
1047
+ /**
1048
+ * Apply search/replace patches to file content
1049
+ * This is the core of the patch-based editing system
1050
+ */
1051
+ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesResult {
1052
+ let content = originalContent;
1053
+ let appliedPatches = 0;
1054
+ const failedPatches: { patch: Patch; error: string }[] = [];
1055
+
1056
+ for (const patch of patches) {
1057
+ // Normalize line endings for matching
1058
+ const normalizedSearch = patch.search.replace(/\\n/g, "\n");
1059
+ const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
1060
+
1061
+ // Check if search string exists in content
1062
+ if (!content.includes(normalizedSearch)) {
1063
+ // Try with different whitespace normalization
1064
+ const flexibleSearch = normalizedSearch.replace(/\s+/g, "\\s+");
1065
+ const regex = new RegExp(flexibleSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\s\+/g, "\\s+"));
1066
+
1067
+ if (!regex.test(content)) {
1068
+ failedPatches.push({
1069
+ patch,
1070
+ error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
1071
+ });
1072
+ continue;
1073
+ }
1074
+
1075
+ // If regex matched, use regex replace
1076
+ content = content.replace(regex, normalizedReplace);
1077
+ appliedPatches++;
1078
+ } else {
1079
+ // Exact match found - apply the replacement
1080
+ // Only replace the first occurrence to be safe
1081
+ const index = content.indexOf(normalizedSearch);
1082
+ content =
1083
+ content.substring(0, index) +
1084
+ normalizedReplace +
1085
+ content.substring(index + normalizedSearch.length);
1086
+ appliedPatches++;
1087
+ }
1088
+ }
1089
+
1090
+ return {
1091
+ success: failedPatches.length === 0,
1092
+ modifiedContent: content,
1093
+ appliedPatches,
1094
+ failedPatches,
1095
+ };
1096
+ }
1097
+
712
1098
  /**
713
1099
  * Validate that AI modifications are surgical edits, not complete rewrites
714
1100
  */