sonance-brand-mcp 1.3.25 → 1.3.27

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.
@@ -50,7 +50,11 @@ interface ApplyFirstRequest {
50
50
  interface BackupManifest {
51
51
  sessionId: string;
52
52
  timestamp: number;
53
- files: { original: string; backup: string }[];
53
+ files: {
54
+ original: string;
55
+ backup: string;
56
+ isNewFile: boolean; // Track if file was newly created (needs deletion on revert)
57
+ }[];
54
58
  }
55
59
 
56
60
  const BACKUP_ROOT = ".sonance-backups";
@@ -230,14 +234,38 @@ ${pageContext.pageContent ? `\`\`\`tsx\n${pageContext.pageContent}\n\`\`\`` : ""
230
234
  `;
231
235
 
232
236
  if (pageContext.componentSources.length > 0) {
233
- textContent += `IMPORTED COMPONENTS:\n`;
234
- for (const comp of pageContext.componentSources) {
237
+ // Smart truncation: prioritize first components (direct imports) and limit total context
238
+ const MAX_TOTAL_CONTEXT = 80000; // ~80k chars to stay well under Claude's limit
239
+ const MAX_PER_FILE_PRIORITY = 4000; // First 10 files get more space
240
+ const MAX_PER_FILE_SECONDARY = 1500; // Remaining files get less
241
+ const MAX_FILES = 30; // Limit total number of files
242
+
243
+ let usedContext = pageContext.pageContent.length + pageContext.globalsCSS.length;
244
+ const truncatedComponents = pageContext.componentSources.slice(0, MAX_FILES);
245
+
246
+ textContent += `IMPORTED COMPONENTS (${truncatedComponents.length} files, ${pageContext.componentSources.length > MAX_FILES ? `${pageContext.componentSources.length - MAX_FILES} omitted` : 'complete'}):\n`;
247
+
248
+ for (let i = 0; i < truncatedComponents.length; i++) {
249
+ const comp = truncatedComponents[i];
250
+ const isPriority = i < 10; // First 10 files are priority (direct imports)
251
+ const maxSize = isPriority ? MAX_PER_FILE_PRIORITY : MAX_PER_FILE_SECONDARY;
252
+
253
+ // Stop if we've used too much context
254
+ if (usedContext > MAX_TOTAL_CONTEXT) {
255
+ textContent += `\n// ... (${truncatedComponents.length - i} more files omitted to stay within context limits)\n`;
256
+ break;
257
+ }
258
+
259
+ const truncatedContent = comp.content.substring(0, maxSize);
260
+ const wasTruncated = comp.content.length > maxSize;
261
+
235
262
  textContent += `
236
- File: ${comp.path}
263
+ File: ${comp.path}${isPriority ? '' : ' (nested)'}
237
264
  \`\`\`tsx
238
- ${comp.content.substring(0, 3000)}${comp.content.length > 3000 ? "\n// ... (truncated)" : ""}
265
+ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
239
266
  \`\`\`
240
267
  `;
268
+ usedContext += truncatedContent.length;
241
269
  }
242
270
  }
243
271
 
@@ -340,11 +368,29 @@ CRITICAL: Your modified file should have approximately the same number of lines
340
368
  });
341
369
  }
342
370
 
371
+ // Build set of valid file paths from page context
372
+ const validFilePaths = new Set<string>();
373
+ if (pageContext.pageFile) {
374
+ validFilePaths.add(pageContext.pageFile);
375
+ }
376
+ for (const comp of pageContext.componentSources) {
377
+ validFilePaths.add(comp.path);
378
+ }
379
+
343
380
  // Process modifications - apply patches to get modified content
344
381
  const modifications: VisionFileModification[] = [];
345
382
  const patchErrors: string[] = [];
346
383
 
347
384
  for (const mod of aiResponse.modifications) {
385
+ // Validate that the file path is in the page context
386
+ // This prevents the AI from creating new files
387
+ if (!validFilePaths.has(mod.filePath)) {
388
+ console.warn(`[Apply-First] Rejected modification to unknown file: ${mod.filePath}`);
389
+ console.warn(`[Apply-First] Valid files are: ${Array.from(validFilePaths).join(", ")}`);
390
+ 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.`);
391
+ continue;
392
+ }
393
+
348
394
  const fullPath = path.join(projectRoot, mod.filePath);
349
395
  let originalContent = "";
350
396
  if (fs.existsSync(fullPath)) {
@@ -470,6 +516,15 @@ async function applyChangesWithBackup(
470
516
  const backupDir = path.join(projectRoot, BACKUP_ROOT, sessionId);
471
517
  const backupPaths: string[] = [];
472
518
 
519
+ // Track which files exist before we modify them
520
+ const existingFiles = new Set<string>();
521
+ for (const mod of modifications) {
522
+ const fullPath = path.join(projectRoot, mod.filePath);
523
+ if (fs.existsSync(fullPath)) {
524
+ existingFiles.add(mod.filePath);
525
+ }
526
+ }
527
+
473
528
  try {
474
529
  // Step 1: Create backup directory
475
530
  fs.mkdirSync(backupDir, { recursive: true });
@@ -478,7 +533,7 @@ async function applyChangesWithBackup(
478
533
  for (const mod of modifications) {
479
534
  const fullPath = path.join(projectRoot, mod.filePath);
480
535
 
481
- if (fs.existsSync(fullPath)) {
536
+ if (existingFiles.has(mod.filePath)) {
482
537
  const backupPath = path.join(backupDir, mod.filePath);
483
538
  const backupDirForFile = path.dirname(backupPath);
484
539
 
@@ -488,13 +543,14 @@ async function applyChangesWithBackup(
488
543
  }
489
544
  }
490
545
 
491
- // Step 3: Write manifest
546
+ // Step 3: Write manifest (track which files are new)
492
547
  const manifest: BackupManifest = {
493
548
  sessionId,
494
549
  timestamp: Date.now(),
495
550
  files: modifications.map(m => ({
496
551
  original: m.filePath,
497
552
  backup: path.join(backupDir, m.filePath),
553
+ isNewFile: !existingFiles.has(m.filePath), // Track if file was newly created
498
554
  })),
499
555
  };
500
556
 
@@ -574,24 +630,51 @@ async function revertFromBackups(
574
630
  );
575
631
 
576
632
  let filesReverted = 0;
633
+ let filesDeleted = 0;
577
634
 
578
635
  for (const file of manifest.files) {
579
- const backupPath = path.join(backupDir, file.original);
580
636
  const originalPath = path.join(projectRoot, file.original);
581
637
 
582
- if (fs.existsSync(backupPath)) {
583
- fs.copyFileSync(backupPath, originalPath);
584
- filesReverted++;
638
+ // Handle new files - delete them
639
+ if (file.isNewFile) {
640
+ if (fs.existsSync(originalPath)) {
641
+ fs.unlinkSync(originalPath);
642
+ filesDeleted++;
643
+ console.log(`[Revert] Deleted new file: ${file.original}`);
644
+
645
+ // Clean up empty parent directories
646
+ try {
647
+ const parentDir = path.dirname(originalPath);
648
+ const entries = fs.readdirSync(parentDir);
649
+ if (entries.length === 0) {
650
+ fs.rmdirSync(parentDir);
651
+ console.log(`[Revert] Removed empty directory: ${path.dirname(file.original)}`);
652
+ }
653
+ } catch {
654
+ // Ignore errors when cleaning up directories
655
+ }
656
+ }
657
+ } else {
658
+ // Handle existing files - restore from backup
659
+ const backupPath = path.join(backupDir, file.original);
660
+ if (fs.existsSync(backupPath)) {
661
+ fs.copyFileSync(backupPath, originalPath);
662
+ filesReverted++;
663
+ }
585
664
  }
586
665
  }
587
666
 
588
667
  // Delete backup directory after successful revert
589
668
  fs.rmSync(backupDir, { recursive: true });
590
669
 
670
+ const message = filesDeleted > 0
671
+ ? `Reverted ${filesReverted} file(s), deleted ${filesDeleted} new file(s)`
672
+ : `Reverted ${filesReverted} file(s)`;
673
+
591
674
  return {
592
675
  success: true,
593
- message: `Reverted ${filesReverted} file(s)`,
594
- filesReverted,
676
+ message,
677
+ filesReverted: filesReverted + filesDeleted,
595
678
  };
596
679
  } catch (error) {
597
680
  console.error("Error reverting from backups:", error);
@@ -603,8 +686,109 @@ async function revertFromBackups(
603
686
  }
604
687
  }
605
688
 
689
+ /**
690
+ * Recursively gather all imports from a file up to a max depth
691
+ * This builds a complete component graph for the AI to understand
692
+ */
693
+ function gatherAllImports(
694
+ filePath: string,
695
+ projectRoot: string,
696
+ visited: Set<string> = new Set(),
697
+ maxDepth: number = 4
698
+ ): { path: string; content: string }[] {
699
+ // Prevent infinite loops and limit total files
700
+ if (visited.has(filePath) || visited.size > 50) return [];
701
+ visited.add(filePath);
702
+
703
+ const results: { path: string; content: string }[] = [];
704
+ const fullPath = path.join(projectRoot, filePath);
705
+
706
+ if (!fs.existsSync(fullPath)) return results;
707
+
708
+ try {
709
+ const content = fs.readFileSync(fullPath, "utf-8");
710
+ results.push({ path: filePath, content });
711
+
712
+ // Continue recursing if we haven't hit max depth
713
+ if (maxDepth > 0) {
714
+ const imports = extractImports(content);
715
+ for (const imp of imports) {
716
+ const resolved = resolveImportPath(imp, filePath, projectRoot);
717
+ if (resolved && !visited.has(resolved)) {
718
+ const nestedImports = gatherAllImports(resolved, projectRoot, visited, maxDepth - 1);
719
+ results.push(...nestedImports);
720
+ }
721
+ }
722
+ }
723
+ } catch {
724
+ // Skip files that can't be read
725
+ }
726
+
727
+ return results;
728
+ }
729
+
730
+ /**
731
+ * Discover layout files that wrap the page
732
+ * App Router: layout.tsx in same and parent directories
733
+ * Pages Router: _app.tsx and _document.tsx
734
+ */
735
+ function discoverLayoutFiles(pageFile: string | null, projectRoot: string): string[] {
736
+ const layoutFiles: string[] = [];
737
+
738
+ if (!pageFile) return layoutFiles;
739
+
740
+ // Determine if App Router or Pages Router
741
+ const isAppRouter = pageFile.includes("/app/") || pageFile.startsWith("app/");
742
+ const isPagesRouter = pageFile.includes("/pages/") || pageFile.startsWith("pages/");
743
+
744
+ if (isAppRouter) {
745
+ // App Router: Check for layout.tsx in same directory and parent directories
746
+ let currentDir = path.dirname(pageFile);
747
+ const appRoot = pageFile.includes("src/app") ? "src/app" : "app";
748
+
749
+ while (currentDir.includes(appRoot)) {
750
+ const layoutPatterns = [
751
+ path.join(currentDir, "layout.tsx"),
752
+ path.join(currentDir, "layout.jsx"),
753
+ ];
754
+
755
+ for (const layoutPath of layoutPatterns) {
756
+ if (fs.existsSync(path.join(projectRoot, layoutPath))) {
757
+ layoutFiles.push(layoutPath);
758
+ }
759
+ }
760
+
761
+ // Move to parent directory
762
+ const parentDir = path.dirname(currentDir);
763
+ if (parentDir === currentDir) break;
764
+ currentDir = parentDir;
765
+ }
766
+ }
767
+
768
+ if (isPagesRouter) {
769
+ // Pages Router: Check for _app.tsx and _document.tsx
770
+ const pagesRoot = pageFile.includes("src/pages") ? "src/pages" : "pages";
771
+
772
+ const pagesRouterLayouts = [
773
+ `${pagesRoot}/_app.tsx`,
774
+ `${pagesRoot}/_app.jsx`,
775
+ `${pagesRoot}/_document.tsx`,
776
+ `${pagesRoot}/_document.jsx`,
777
+ ];
778
+
779
+ for (const layoutPath of pagesRouterLayouts) {
780
+ if (fs.existsSync(path.join(projectRoot, layoutPath))) {
781
+ layoutFiles.push(layoutPath);
782
+ }
783
+ }
784
+ }
785
+
786
+ return layoutFiles;
787
+ }
788
+
606
789
  /**
607
790
  * Gather context about the current page for AI analysis
791
+ * Uses recursive import resolution to build complete component graph
608
792
  */
609
793
  function gatherPageContext(
610
794
  pageRoute: string,
@@ -618,50 +802,102 @@ function gatherPageContext(
618
802
  const pageFile = discoverPageFile(pageRoute, projectRoot);
619
803
  let pageContent = "";
620
804
  const componentSources: { path: string; content: string }[] = [];
805
+ const visited = new Set<string>();
621
806
 
622
807
  if (pageFile) {
623
808
  const fullPath = path.join(projectRoot, pageFile);
624
809
  if (fs.existsSync(fullPath)) {
625
810
  pageContent = fs.readFileSync(fullPath, "utf-8");
811
+ visited.add(pageFile);
626
812
 
813
+ // Recursively gather all imported components (up to 4 levels deep)
627
814
  const imports = extractImports(pageContent);
628
815
  for (const importPath of imports) {
629
816
  const resolvedPath = resolveImportPath(importPath, pageFile, projectRoot);
630
- if (resolvedPath && fs.existsSync(path.join(projectRoot, resolvedPath))) {
631
- try {
632
- const content = fs.readFileSync(path.join(projectRoot, resolvedPath), "utf-8");
633
- componentSources.push({ path: resolvedPath, content });
634
- } catch {
635
- // Skip files that can't be read
636
- }
817
+ if (resolvedPath && !visited.has(resolvedPath)) {
818
+ const nestedComponents = gatherAllImports(resolvedPath, projectRoot, visited, 3);
819
+ componentSources.push(...nestedComponents);
820
+ }
821
+ }
822
+
823
+ // Also include layout files
824
+ const layoutFiles = discoverLayoutFiles(pageFile, projectRoot);
825
+ for (const layoutFile of layoutFiles) {
826
+ if (!visited.has(layoutFile)) {
827
+ const layoutComponents = gatherAllImports(layoutFile, projectRoot, visited, 2);
828
+ componentSources.push(...layoutComponents);
637
829
  }
638
830
  }
639
831
  }
640
832
  }
641
833
 
642
834
  let globalsCSS = "";
643
- const globalsPath = path.join(projectRoot, "src/app/globals.css");
644
- if (fs.existsSync(globalsPath)) {
645
- globalsCSS = fs.readFileSync(globalsPath, "utf-8");
835
+ const globalsCSSPatterns = [
836
+ "src/app/globals.css",
837
+ "app/globals.css",
838
+ "src/styles/globals.css",
839
+ "styles/globals.css",
840
+ "src/styles/global.css",
841
+ "styles/global.css",
842
+ ];
843
+
844
+ for (const cssPattern of globalsCSSPatterns) {
845
+ const globalsPath = path.join(projectRoot, cssPattern);
846
+ if (fs.existsSync(globalsPath)) {
847
+ globalsCSS = fs.readFileSync(globalsPath, "utf-8");
848
+ break;
849
+ }
646
850
  }
647
851
 
648
852
  return { pageFile, pageContent, componentSources, globalsCSS };
649
853
  }
650
854
 
651
855
  function discoverPageFile(route: string, projectRoot: string): string | null {
856
+ // Handle root route
652
857
  if (route === "/" || route === "") {
653
- const rootPage = "src/app/page.tsx";
654
- if (fs.existsSync(path.join(projectRoot, rootPage))) {
655
- return rootPage;
858
+ const rootPatterns = [
859
+ // App Router patterns
860
+ "src/app/page.tsx",
861
+ "src/app/page.jsx",
862
+ "app/page.tsx",
863
+ "app/page.jsx",
864
+ // Pages Router patterns
865
+ "src/pages/index.tsx",
866
+ "src/pages/index.jsx",
867
+ "pages/index.tsx",
868
+ "pages/index.jsx",
869
+ ];
870
+
871
+ for (const pattern of rootPatterns) {
872
+ if (fs.existsSync(path.join(projectRoot, pattern))) {
873
+ return pattern;
874
+ }
656
875
  }
657
876
  return null;
658
877
  }
659
878
 
660
879
  const cleanRoute = route.replace(/^\//, "");
880
+
881
+ // First, try exact match patterns
661
882
  const patterns = [
883
+ // App Router patterns (with src)
662
884
  `src/app/${cleanRoute}/page.tsx`,
663
885
  `src/app/${cleanRoute}/page.jsx`,
664
- `src/app/${cleanRoute}.tsx`,
886
+ // App Router patterns (without src)
887
+ `app/${cleanRoute}/page.tsx`,
888
+ `app/${cleanRoute}/page.jsx`,
889
+ // Pages Router patterns (with src) - file-based
890
+ `src/pages/${cleanRoute}.tsx`,
891
+ `src/pages/${cleanRoute}.jsx`,
892
+ // Pages Router patterns (with src) - folder-based
893
+ `src/pages/${cleanRoute}/index.tsx`,
894
+ `src/pages/${cleanRoute}/index.jsx`,
895
+ // Pages Router patterns (without src) - file-based
896
+ `pages/${cleanRoute}.tsx`,
897
+ `pages/${cleanRoute}.jsx`,
898
+ // Pages Router patterns (without src) - folder-based
899
+ `pages/${cleanRoute}/index.tsx`,
900
+ `pages/${cleanRoute}/index.jsx`,
665
901
  ];
666
902
 
667
903
  for (const pattern of patterns) {
@@ -670,6 +906,99 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
670
906
  }
671
907
  }
672
908
 
909
+ // If exact match not found, try dynamic route matching
910
+ // e.g., /processes/123 -> src/pages/processes/[id].tsx
911
+ const dynamicResult = findDynamicRoute(cleanRoute, projectRoot);
912
+ if (dynamicResult) {
913
+ return dynamicResult;
914
+ }
915
+
916
+ return null;
917
+ }
918
+
919
+ /**
920
+ * Find dynamic route files when exact match fails
921
+ * Maps runtime routes like "/processes/123" to file paths like "src/pages/processes/[id].tsx"
922
+ */
923
+ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | null {
924
+ const segments = cleanRoute.split("/");
925
+
926
+ // Try replacing the last segment with dynamic patterns
927
+ const baseDirs = [
928
+ "src/app",
929
+ "app",
930
+ "src/pages",
931
+ "pages",
932
+ ];
933
+
934
+ const dynamicPatterns = ["[id]", "[slug]", "[...slug]", "[[...slug]]"];
935
+ const extensions = [".tsx", ".jsx"];
936
+
937
+ for (const baseDir of baseDirs) {
938
+ const basePath = path.join(projectRoot, baseDir);
939
+ if (!fs.existsSync(basePath)) continue;
940
+
941
+ // Build path with all segments except the last one
942
+ const parentSegments = segments.slice(0, -1);
943
+ const parentPath = parentSegments.length > 0
944
+ ? path.join(basePath, ...parentSegments)
945
+ : basePath;
946
+
947
+ if (!fs.existsSync(parentPath)) continue;
948
+
949
+ // Check for dynamic route files
950
+ for (const dynPattern of dynamicPatterns) {
951
+ for (const ext of extensions) {
952
+ // App Router: parent/[id]/page.tsx
953
+ if (baseDir.includes("app")) {
954
+ const appRouterPath = path.join(parentPath, dynPattern, `page${ext}`);
955
+ if (fs.existsSync(appRouterPath)) {
956
+ return path.relative(projectRoot, appRouterPath);
957
+ }
958
+ }
959
+
960
+ // Pages Router: parent/[id].tsx
961
+ const pagesRouterFile = path.join(parentPath, `${dynPattern}${ext}`);
962
+ if (fs.existsSync(pagesRouterFile)) {
963
+ return path.relative(projectRoot, pagesRouterFile);
964
+ }
965
+
966
+ // Pages Router: parent/[id]/index.tsx
967
+ const pagesRouterDir = path.join(parentPath, dynPattern, `index${ext}`);
968
+ if (fs.existsSync(pagesRouterDir)) {
969
+ return path.relative(projectRoot, pagesRouterDir);
970
+ }
971
+ }
972
+ }
973
+
974
+ // Also scan directory for any dynamic segment pattern [...]
975
+ try {
976
+ const entries = fs.readdirSync(parentPath, { withFileTypes: true });
977
+ for (const entry of entries) {
978
+ if (entry.name.startsWith("[") && entry.name.includes("]")) {
979
+ for (const ext of extensions) {
980
+ if (entry.isDirectory()) {
981
+ // App Router or Pages Router with folder
982
+ const pagePath = path.join(parentPath, entry.name, `page${ext}`);
983
+ const indexPath = path.join(parentPath, entry.name, `index${ext}`);
984
+ if (fs.existsSync(pagePath)) {
985
+ return path.relative(projectRoot, pagePath);
986
+ }
987
+ if (fs.existsSync(indexPath)) {
988
+ return path.relative(projectRoot, indexPath);
989
+ }
990
+ } else if (entry.isFile() && entry.name.endsWith(ext)) {
991
+ // Pages Router file-based
992
+ return path.relative(projectRoot, path.join(parentPath, entry.name));
993
+ }
994
+ }
995
+ }
996
+ }
997
+ } catch {
998
+ // Skip if directory can't be read
999
+ }
1000
+ }
1001
+
673
1002
  return null;
674
1003
  }
675
1004
 
@@ -243,14 +243,38 @@ ${pageContext.pageContent ? `\`\`\`tsx\n${pageContext.pageContent}\n\`\`\`` : ""
243
243
  `;
244
244
 
245
245
  if (pageContext.componentSources.length > 0) {
246
- textContent += `IMPORTED COMPONENTS:\n`;
247
- for (const comp of pageContext.componentSources) {
246
+ // Smart truncation: prioritize first components (direct imports) and limit total context
247
+ const MAX_TOTAL_CONTEXT = 80000; // ~80k chars to stay well under Claude's limit
248
+ const MAX_PER_FILE_PRIORITY = 4000; // First 10 files get more space
249
+ const MAX_PER_FILE_SECONDARY = 1500; // Remaining files get less
250
+ const MAX_FILES = 30; // Limit total number of files
251
+
252
+ let usedContext = pageContext.pageContent.length + pageContext.globalsCSS.length;
253
+ const truncatedComponents = pageContext.componentSources.slice(0, MAX_FILES);
254
+
255
+ textContent += `IMPORTED COMPONENTS (${truncatedComponents.length} files, ${pageContext.componentSources.length > MAX_FILES ? `${pageContext.componentSources.length - MAX_FILES} omitted` : 'complete'}):\n`;
256
+
257
+ for (let i = 0; i < truncatedComponents.length; i++) {
258
+ const comp = truncatedComponents[i];
259
+ const isPriority = i < 10; // First 10 files are priority (direct imports)
260
+ const maxSize = isPriority ? MAX_PER_FILE_PRIORITY : MAX_PER_FILE_SECONDARY;
261
+
262
+ // Stop if we've used too much context
263
+ if (usedContext > MAX_TOTAL_CONTEXT) {
264
+ textContent += `\n// ... (${truncatedComponents.length - i} more files omitted to stay within context limits)\n`;
265
+ break;
266
+ }
267
+
268
+ const truncatedContent = comp.content.substring(0, maxSize);
269
+ const wasTruncated = comp.content.length > maxSize;
270
+
248
271
  textContent += `
249
- File: ${comp.path}
272
+ File: ${comp.path}${isPriority ? '' : ' (nested)'}
250
273
  \`\`\`tsx
251
- ${comp.content.substring(0, 3000)}${comp.content.length > 3000 ? "\n// ... (truncated)" : ""}
274
+ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
252
275
  \`\`\`
253
276
  `;
277
+ usedContext += truncatedContent.length;
254
278
  }
255
279
  }
256
280
 
@@ -551,8 +575,109 @@ function findComponentFileByName(
551
575
  return null;
552
576
  }
553
577
 
578
+ /**
579
+ * Recursively gather all imports from a file up to a max depth
580
+ * This builds a complete component graph for the AI to understand
581
+ */
582
+ function gatherAllImports(
583
+ filePath: string,
584
+ projectRoot: string,
585
+ visited: Set<string> = new Set(),
586
+ maxDepth: number = 4
587
+ ): { path: string; content: string }[] {
588
+ // Prevent infinite loops and limit total files
589
+ if (visited.has(filePath) || visited.size > 50) return [];
590
+ visited.add(filePath);
591
+
592
+ const results: { path: string; content: string }[] = [];
593
+ const fullPath = path.join(projectRoot, filePath);
594
+
595
+ if (!fs.existsSync(fullPath)) return results;
596
+
597
+ try {
598
+ const content = fs.readFileSync(fullPath, "utf-8");
599
+ results.push({ path: filePath, content });
600
+
601
+ // Continue recursing if we haven't hit max depth
602
+ if (maxDepth > 0) {
603
+ const imports = extractImports(content);
604
+ for (const imp of imports) {
605
+ const resolved = resolveImportPath(imp, filePath, projectRoot);
606
+ if (resolved && !visited.has(resolved)) {
607
+ const nestedImports = gatherAllImports(resolved, projectRoot, visited, maxDepth - 1);
608
+ results.push(...nestedImports);
609
+ }
610
+ }
611
+ }
612
+ } catch {
613
+ // Skip files that can't be read
614
+ }
615
+
616
+ return results;
617
+ }
618
+
619
+ /**
620
+ * Discover layout files that wrap the page
621
+ * App Router: layout.tsx in same and parent directories
622
+ * Pages Router: _app.tsx and _document.tsx
623
+ */
624
+ function discoverLayoutFiles(pageFile: string | null, projectRoot: string): string[] {
625
+ const layoutFiles: string[] = [];
626
+
627
+ if (!pageFile) return layoutFiles;
628
+
629
+ // Determine if App Router or Pages Router
630
+ const isAppRouter = pageFile.includes("/app/") || pageFile.startsWith("app/");
631
+ const isPagesRouter = pageFile.includes("/pages/") || pageFile.startsWith("pages/");
632
+
633
+ if (isAppRouter) {
634
+ // App Router: Check for layout.tsx in same directory and parent directories
635
+ let currentDir = path.dirname(pageFile);
636
+ const appRoot = pageFile.includes("src/app") ? "src/app" : "app";
637
+
638
+ while (currentDir.includes(appRoot)) {
639
+ const layoutPatterns = [
640
+ path.join(currentDir, "layout.tsx"),
641
+ path.join(currentDir, "layout.jsx"),
642
+ ];
643
+
644
+ for (const layoutPath of layoutPatterns) {
645
+ if (fs.existsSync(path.join(projectRoot, layoutPath))) {
646
+ layoutFiles.push(layoutPath);
647
+ }
648
+ }
649
+
650
+ // Move to parent directory
651
+ const parentDir = path.dirname(currentDir);
652
+ if (parentDir === currentDir) break;
653
+ currentDir = parentDir;
654
+ }
655
+ }
656
+
657
+ if (isPagesRouter) {
658
+ // Pages Router: Check for _app.tsx and _document.tsx
659
+ const pagesRoot = pageFile.includes("src/pages") ? "src/pages" : "pages";
660
+
661
+ const pagesRouterLayouts = [
662
+ `${pagesRoot}/_app.tsx`,
663
+ `${pagesRoot}/_app.jsx`,
664
+ `${pagesRoot}/_document.tsx`,
665
+ `${pagesRoot}/_document.jsx`,
666
+ ];
667
+
668
+ for (const layoutPath of pagesRouterLayouts) {
669
+ if (fs.existsSync(path.join(projectRoot, layoutPath))) {
670
+ layoutFiles.push(layoutPath);
671
+ }
672
+ }
673
+ }
674
+
675
+ return layoutFiles;
676
+ }
677
+
554
678
  /**
555
679
  * Gather context about the current page for AI analysis
680
+ * Uses recursive import resolution to build complete component graph
556
681
  */
557
682
  function gatherPageContext(
558
683
  pageRoute: string,
@@ -568,23 +693,30 @@ function gatherPageContext(
568
693
  const pageFile = discoverPageFile(pageRoute, projectRoot);
569
694
  let pageContent = "";
570
695
  const componentSources: { path: string; content: string }[] = [];
696
+ const visited = new Set<string>();
571
697
 
572
698
  if (pageFile) {
573
699
  const fullPath = path.join(projectRoot, pageFile);
574
700
  if (fs.existsSync(fullPath)) {
575
701
  pageContent = fs.readFileSync(fullPath, "utf-8");
702
+ visited.add(pageFile);
576
703
 
577
- // Extract imports and read component files
704
+ // Recursively gather all imported components (up to 4 levels deep)
578
705
  const imports = extractImports(pageContent);
579
706
  for (const importPath of imports) {
580
707
  const resolvedPath = resolveImportPath(importPath, pageFile, projectRoot);
581
- if (resolvedPath && fs.existsSync(path.join(projectRoot, resolvedPath))) {
582
- try {
583
- const content = fs.readFileSync(path.join(projectRoot, resolvedPath), "utf-8");
584
- componentSources.push({ path: resolvedPath, content });
585
- } catch {
586
- // Skip files that can't be read
587
- }
708
+ if (resolvedPath && !visited.has(resolvedPath)) {
709
+ const nestedComponents = gatherAllImports(resolvedPath, projectRoot, visited, 3);
710
+ componentSources.push(...nestedComponents);
711
+ }
712
+ }
713
+
714
+ // Also include layout files
715
+ const layoutFiles = discoverLayoutFiles(pageFile, projectRoot);
716
+ for (const layoutFile of layoutFiles) {
717
+ if (!visited.has(layoutFile)) {
718
+ const layoutComponents = gatherAllImports(layoutFile, projectRoot, visited, 2);
719
+ componentSources.push(...layoutComponents);
588
720
  }
589
721
  }
590
722
  }
@@ -595,25 +727,30 @@ function gatherPageContext(
595
727
  for (const el of focusedElements) {
596
728
  // Try to find component file by name
597
729
  const foundPath = findComponentFileByName(el.name, projectRoot);
598
- if (foundPath && !componentSources.some((c) => c.path === foundPath)) {
599
- try {
600
- const content = fs.readFileSync(
601
- path.join(projectRoot, foundPath),
602
- "utf-8"
603
- );
604
- componentSources.push({ path: foundPath, content });
605
- } catch {
606
- /* skip if unreadable */
607
- }
730
+ if (foundPath && !visited.has(foundPath) && !componentSources.some((c) => c.path === foundPath)) {
731
+ const focusedComponents = gatherAllImports(foundPath, projectRoot, visited, 2);
732
+ componentSources.push(...focusedComponents);
608
733
  }
609
734
  }
610
735
  }
611
736
 
612
- // Read globals.css
737
+ // Read globals.css - check multiple possible locations
613
738
  let globalsCSS = "";
614
- const globalsPath = path.join(projectRoot, "src/app/globals.css");
615
- if (fs.existsSync(globalsPath)) {
616
- globalsCSS = fs.readFileSync(globalsPath, "utf-8");
739
+ const globalsCSSPatterns = [
740
+ "src/app/globals.css",
741
+ "app/globals.css",
742
+ "src/styles/globals.css",
743
+ "styles/globals.css",
744
+ "src/styles/global.css",
745
+ "styles/global.css",
746
+ ];
747
+
748
+ for (const cssPattern of globalsCSSPatterns) {
749
+ const globalsPath = path.join(projectRoot, cssPattern);
750
+ if (fs.existsSync(globalsPath)) {
751
+ globalsCSS = fs.readFileSync(globalsPath, "utf-8");
752
+ break;
753
+ }
617
754
  }
618
755
 
619
756
  return { pageFile, pageContent, componentSources, globalsCSS };
@@ -621,13 +758,28 @@ function gatherPageContext(
621
758
 
622
759
  /**
623
760
  * Discover the page file for a given route
761
+ * Supports both App Router (src/app/) and Pages Router (src/pages/, pages/)
624
762
  */
625
763
  function discoverPageFile(route: string, projectRoot: string): string | null {
626
764
  // Handle root route
627
765
  if (route === "/" || route === "") {
628
- const rootPage = "src/app/page.tsx";
629
- if (fs.existsSync(path.join(projectRoot, rootPage))) {
630
- return rootPage;
766
+ const rootPatterns = [
767
+ // App Router patterns
768
+ "src/app/page.tsx",
769
+ "src/app/page.jsx",
770
+ "app/page.tsx",
771
+ "app/page.jsx",
772
+ // Pages Router patterns
773
+ "src/pages/index.tsx",
774
+ "src/pages/index.jsx",
775
+ "pages/index.tsx",
776
+ "pages/index.jsx",
777
+ ];
778
+
779
+ for (const pattern of rootPatterns) {
780
+ if (fs.existsSync(path.join(projectRoot, pattern))) {
781
+ return pattern;
782
+ }
631
783
  }
632
784
  return null;
633
785
  }
@@ -635,11 +787,26 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
635
787
  // Remove leading slash
636
788
  const cleanRoute = route.replace(/^\//, "");
637
789
 
638
- // Try different patterns
790
+ // First, try exact match patterns
639
791
  const patterns = [
792
+ // App Router patterns (with src)
640
793
  `src/app/${cleanRoute}/page.tsx`,
641
794
  `src/app/${cleanRoute}/page.jsx`,
642
- `src/app/${cleanRoute}.tsx`,
795
+ // App Router patterns (without src)
796
+ `app/${cleanRoute}/page.tsx`,
797
+ `app/${cleanRoute}/page.jsx`,
798
+ // Pages Router patterns (with src) - file-based
799
+ `src/pages/${cleanRoute}.tsx`,
800
+ `src/pages/${cleanRoute}.jsx`,
801
+ // Pages Router patterns (with src) - folder-based
802
+ `src/pages/${cleanRoute}/index.tsx`,
803
+ `src/pages/${cleanRoute}/index.jsx`,
804
+ // Pages Router patterns (without src) - file-based
805
+ `pages/${cleanRoute}.tsx`,
806
+ `pages/${cleanRoute}.jsx`,
807
+ // Pages Router patterns (without src) - folder-based
808
+ `pages/${cleanRoute}/index.tsx`,
809
+ `pages/${cleanRoute}/index.jsx`,
643
810
  ];
644
811
 
645
812
  for (const pattern of patterns) {
@@ -648,6 +815,99 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
648
815
  }
649
816
  }
650
817
 
818
+ // If exact match not found, try dynamic route matching
819
+ // e.g., /processes/123 -> src/pages/processes/[id].tsx
820
+ const dynamicResult = findDynamicRoute(cleanRoute, projectRoot);
821
+ if (dynamicResult) {
822
+ return dynamicResult;
823
+ }
824
+
825
+ return null;
826
+ }
827
+
828
+ /**
829
+ * Find dynamic route files when exact match fails
830
+ * Maps runtime routes like "/processes/123" to file paths like "src/pages/processes/[id].tsx"
831
+ */
832
+ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | null {
833
+ const segments = cleanRoute.split("/");
834
+
835
+ // Try replacing the last segment with dynamic patterns
836
+ const baseDirs = [
837
+ "src/app",
838
+ "app",
839
+ "src/pages",
840
+ "pages",
841
+ ];
842
+
843
+ const dynamicPatterns = ["[id]", "[slug]", "[...slug]", "[[...slug]]"];
844
+ const extensions = [".tsx", ".jsx"];
845
+
846
+ for (const baseDir of baseDirs) {
847
+ const basePath = path.join(projectRoot, baseDir);
848
+ if (!fs.existsSync(basePath)) continue;
849
+
850
+ // Build path with all segments except the last one
851
+ const parentSegments = segments.slice(0, -1);
852
+ const parentPath = parentSegments.length > 0
853
+ ? path.join(basePath, ...parentSegments)
854
+ : basePath;
855
+
856
+ if (!fs.existsSync(parentPath)) continue;
857
+
858
+ // Check for dynamic route files
859
+ for (const dynPattern of dynamicPatterns) {
860
+ for (const ext of extensions) {
861
+ // App Router: parent/[id]/page.tsx
862
+ if (baseDir.includes("app")) {
863
+ const appRouterPath = path.join(parentPath, dynPattern, `page${ext}`);
864
+ if (fs.existsSync(appRouterPath)) {
865
+ return path.relative(projectRoot, appRouterPath);
866
+ }
867
+ }
868
+
869
+ // Pages Router: parent/[id].tsx
870
+ const pagesRouterFile = path.join(parentPath, `${dynPattern}${ext}`);
871
+ if (fs.existsSync(pagesRouterFile)) {
872
+ return path.relative(projectRoot, pagesRouterFile);
873
+ }
874
+
875
+ // Pages Router: parent/[id]/index.tsx
876
+ const pagesRouterDir = path.join(parentPath, dynPattern, `index${ext}`);
877
+ if (fs.existsSync(pagesRouterDir)) {
878
+ return path.relative(projectRoot, pagesRouterDir);
879
+ }
880
+ }
881
+ }
882
+
883
+ // Also scan directory for any dynamic segment pattern [...]
884
+ try {
885
+ const entries = fs.readdirSync(parentPath, { withFileTypes: true });
886
+ for (const entry of entries) {
887
+ if (entry.name.startsWith("[") && entry.name.includes("]")) {
888
+ for (const ext of extensions) {
889
+ if (entry.isDirectory()) {
890
+ // App Router or Pages Router with folder
891
+ const pagePath = path.join(parentPath, entry.name, `page${ext}`);
892
+ const indexPath = path.join(parentPath, entry.name, `index${ext}`);
893
+ if (fs.existsSync(pagePath)) {
894
+ return path.relative(projectRoot, pagePath);
895
+ }
896
+ if (fs.existsSync(indexPath)) {
897
+ return path.relative(projectRoot, indexPath);
898
+ }
899
+ } else if (entry.isFile() && entry.name.endsWith(ext)) {
900
+ // Pages Router file-based
901
+ return path.relative(projectRoot, path.join(parentPath, entry.name));
902
+ }
903
+ }
904
+ }
905
+ }
906
+ } catch {
907
+ // Skip if directory can't be read
908
+ }
909
+ }
910
+
651
911
  return null;
652
912
  }
653
913
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.25",
3
+ "version": "1.3.27",
4
4
  "description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",