sonance-brand-mcp 1.3.25 → 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.
@@ -230,14 +230,38 @@ ${pageContext.pageContent ? `\`\`\`tsx\n${pageContext.pageContent}\n\`\`\`` : ""
230
230
  `;
231
231
 
232
232
  if (pageContext.componentSources.length > 0) {
233
- textContent += `IMPORTED COMPONENTS:\n`;
234
- 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
+
235
258
  textContent += `
236
- File: ${comp.path}
259
+ File: ${comp.path}${isPriority ? '' : ' (nested)'}
237
260
  \`\`\`tsx
238
- ${comp.content.substring(0, 3000)}${comp.content.length > 3000 ? "\n// ... (truncated)" : ""}
261
+ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
239
262
  \`\`\`
240
263
  `;
264
+ usedContext += truncatedContent.length;
241
265
  }
242
266
  }
243
267
 
@@ -340,11 +364,29 @@ CRITICAL: Your modified file should have approximately the same number of lines
340
364
  });
341
365
  }
342
366
 
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
+
343
376
  // Process modifications - apply patches to get modified content
344
377
  const modifications: VisionFileModification[] = [];
345
378
  const patchErrors: string[] = [];
346
379
 
347
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
+
348
390
  const fullPath = path.join(projectRoot, mod.filePath);
349
391
  let originalContent = "";
350
392
  if (fs.existsSync(fullPath)) {
@@ -603,8 +645,109 @@ async function revertFromBackups(
603
645
  }
604
646
  }
605
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
+
606
748
  /**
607
749
  * Gather context about the current page for AI analysis
750
+ * Uses recursive import resolution to build complete component graph
608
751
  */
609
752
  function gatherPageContext(
610
753
  pageRoute: string,
@@ -618,50 +761,102 @@ function gatherPageContext(
618
761
  const pageFile = discoverPageFile(pageRoute, projectRoot);
619
762
  let pageContent = "";
620
763
  const componentSources: { path: string; content: string }[] = [];
764
+ const visited = new Set<string>();
621
765
 
622
766
  if (pageFile) {
623
767
  const fullPath = path.join(projectRoot, pageFile);
624
768
  if (fs.existsSync(fullPath)) {
625
769
  pageContent = fs.readFileSync(fullPath, "utf-8");
770
+ visited.add(pageFile);
626
771
 
772
+ // Recursively gather all imported components (up to 4 levels deep)
627
773
  const imports = extractImports(pageContent);
628
774
  for (const importPath of imports) {
629
775
  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
- }
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);
637
788
  }
638
789
  }
639
790
  }
640
791
  }
641
792
 
642
793
  let globalsCSS = "";
643
- const globalsPath = path.join(projectRoot, "src/app/globals.css");
644
- if (fs.existsSync(globalsPath)) {
645
- 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
+ }
646
809
  }
647
810
 
648
811
  return { pageFile, pageContent, componentSources, globalsCSS };
649
812
  }
650
813
 
651
814
  function discoverPageFile(route: string, projectRoot: string): string | null {
815
+ // Handle root route
652
816
  if (route === "/" || route === "") {
653
- const rootPage = "src/app/page.tsx";
654
- if (fs.existsSync(path.join(projectRoot, rootPage))) {
655
- 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
+ }
656
834
  }
657
835
  return null;
658
836
  }
659
837
 
660
838
  const cleanRoute = route.replace(/^\//, "");
839
+
840
+ // First, try exact match patterns
661
841
  const patterns = [
842
+ // App Router patterns (with src)
662
843
  `src/app/${cleanRoute}/page.tsx`,
663
844
  `src/app/${cleanRoute}/page.jsx`,
664
- `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`,
665
860
  ];
666
861
 
667
862
  for (const pattern of patterns) {
@@ -670,6 +865,99 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
670
865
  }
671
866
  }
672
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
+
673
961
  return null;
674
962
  }
675
963
 
@@ -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.26",
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",