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
|
-
|
|
234
|
-
|
|
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
|
-
${
|
|
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 &&
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
|
644
|
-
|
|
645
|
-
|
|
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
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
${
|
|
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
|
-
//
|
|
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 &&
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
600
|
-
|
|
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
|
|
615
|
-
|
|
616
|
-
|
|
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
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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",
|