sonance-brand-mcp 1.3.105 → 1.3.107
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.
|
@@ -624,6 +624,52 @@ function findElementLineInFile(
|
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
626
|
|
|
627
|
+
// ========== FALLBACK MATCHING (for non-Sonance codebases) ==========
|
|
628
|
+
// These are lower confidence but help find elements in ANY codebase
|
|
629
|
+
|
|
630
|
+
// FALLBACK 1: Flexible text search (just find the text anywhere)
|
|
631
|
+
if (focusedElement.textContent && focusedElement.textContent.trim().length > 5) {
|
|
632
|
+
const searchText = focusedElement.textContent.trim().substring(0, 30);
|
|
633
|
+
for (let i = 0; i < lines.length; i++) {
|
|
634
|
+
if (lines[i].includes(searchText)) {
|
|
635
|
+
debugLog("Fallback: Flexible text match found", { searchText, lineNumber: i + 1 });
|
|
636
|
+
return {
|
|
637
|
+
lineNumber: i + 1,
|
|
638
|
+
snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
|
|
639
|
+
confidence: 'low',
|
|
640
|
+
matchedBy: `flexible text match "${searchText}${focusedElement.textContent.length > 30 ? '...' : ''}"`
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// FALLBACK 2: Class fingerprint (use first few classes as unique identifier)
|
|
647
|
+
if (focusedElement.className && focusedElement.className.trim().length > 15) {
|
|
648
|
+
// Take first 2-3 distinctive classes as a fingerprint
|
|
649
|
+
const classes = focusedElement.className.trim().split(/\s+/);
|
|
650
|
+
// Filter out very common single-word utilities
|
|
651
|
+
const distinctiveClasses = classes.filter(c =>
|
|
652
|
+
c.length > 4 && !['flex', 'grid', 'block', 'hidden', 'relative', 'absolute'].includes(c)
|
|
653
|
+
).slice(0, 3);
|
|
654
|
+
|
|
655
|
+
if (distinctiveClasses.length >= 2) {
|
|
656
|
+
const fingerprint = distinctiveClasses.join(' ');
|
|
657
|
+
for (let i = 0; i < lines.length; i++) {
|
|
658
|
+
// Check if line contains ALL fingerprint classes
|
|
659
|
+
const lineHasAll = distinctiveClasses.every(cls => lines[i].includes(cls));
|
|
660
|
+
if (lineHasAll) {
|
|
661
|
+
debugLog("Fallback: Class fingerprint match found", { fingerprint, lineNumber: i + 1 });
|
|
662
|
+
return {
|
|
663
|
+
lineNumber: i + 1,
|
|
664
|
+
snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
|
|
665
|
+
confidence: 'low',
|
|
666
|
+
matchedBy: `class fingerprint "${fingerprint}"`
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
627
673
|
return null;
|
|
628
674
|
}
|
|
629
675
|
|
|
@@ -655,9 +701,14 @@ function scoreFilesForTextContent(
|
|
|
655
701
|
filesCount: importedFiles.length
|
|
656
702
|
});
|
|
657
703
|
|
|
658
|
-
//
|
|
704
|
+
// Search component files and page files (where JSX text lives)
|
|
705
|
+
// Expanded to work with ANY codebase structure
|
|
659
706
|
const componentFiles = importedFiles.filter(f =>
|
|
660
|
-
f.path.includes('components/') ||
|
|
707
|
+
f.path.includes('components/') ||
|
|
708
|
+
f.path.includes('/ui/') ||
|
|
709
|
+
f.path.includes('/_components/') ||
|
|
710
|
+
f.path.endsWith('.tsx') ||
|
|
711
|
+
f.path.endsWith('.jsx')
|
|
661
712
|
);
|
|
662
713
|
|
|
663
714
|
const results: { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number }[] = [];
|
|
@@ -753,19 +804,42 @@ function findElementInImportedFiles(
|
|
|
753
804
|
const routeName = pageDir.split('/').pop() || ''; // e.g., "typography"
|
|
754
805
|
|
|
755
806
|
for (const file of importedFiles) {
|
|
756
|
-
// Focus on component files (where UI elements live)
|
|
807
|
+
// Focus on component/page files (where UI elements live)
|
|
757
808
|
// Skip types, stores, utils, hooks - they don't contain JSX elements
|
|
758
|
-
|
|
809
|
+
// Expanded to work with ANY codebase structure
|
|
810
|
+
const isComponentFile =
|
|
811
|
+
file.path.includes('components/') ||
|
|
812
|
+
file.path.includes('/ui/') ||
|
|
813
|
+
file.path.includes('/_components/') ||
|
|
814
|
+
file.path.endsWith('.tsx') ||
|
|
815
|
+
file.path.endsWith('.jsx');
|
|
816
|
+
|
|
817
|
+
// Skip non-component files but allow page files
|
|
818
|
+
if (!isComponentFile) continue;
|
|
819
|
+
|
|
820
|
+
// Skip known non-UI files
|
|
821
|
+
if (file.path.includes('/types') ||
|
|
822
|
+
file.path.includes('/hooks/') ||
|
|
823
|
+
file.path.includes('/utils/') ||
|
|
824
|
+
file.path.includes('/lib/') ||
|
|
825
|
+
file.path.includes('.d.ts')) continue;
|
|
759
826
|
|
|
760
827
|
const result = findElementLineInFile(file.content, focusedElement);
|
|
761
|
-
|
|
828
|
+
// Accept ALL matches including low confidence - let the scoring decide
|
|
829
|
+
// Low confidence matches from fallback logic (text/class fingerprint) are still useful
|
|
830
|
+
if (result) {
|
|
762
831
|
let score = 0;
|
|
763
832
|
|
|
764
833
|
// Text content match is highest priority (+100)
|
|
765
|
-
if (result.matchedBy.includes('textContent') || result.matchedBy.includes('word match')) {
|
|
834
|
+
if (result.matchedBy.includes('textContent') || result.matchedBy.includes('word match') || result.matchedBy.includes('flexible text')) {
|
|
766
835
|
score += 100;
|
|
767
836
|
}
|
|
768
837
|
|
|
838
|
+
// Class fingerprint match gets medium score (+60)
|
|
839
|
+
if (result.matchedBy.includes('class fingerprint')) {
|
|
840
|
+
score += 60;
|
|
841
|
+
}
|
|
842
|
+
|
|
769
843
|
// Directory proximity - same directory tree as page file (+50)
|
|
770
844
|
// e.g., for page src/app/typography/page.tsx, prefer src/app/typography/_components/
|
|
771
845
|
if (pageDir && file.path.includes(pageDir)) {
|
|
@@ -778,8 +852,8 @@ function findElementInImportedFiles(
|
|
|
778
852
|
score += 30;
|
|
779
853
|
}
|
|
780
854
|
|
|
781
|
-
// Confidence bonus
|
|
782
|
-
score += result.confidence === 'high' ? 20 :
|
|
855
|
+
// Confidence bonus - low confidence still gets some points
|
|
856
|
+
score += result.confidence === 'high' ? 30 : result.confidence === 'medium' ? 20 : 5;
|
|
783
857
|
|
|
784
858
|
matches.push({
|
|
785
859
|
path: file.path,
|
|
@@ -2232,9 +2306,25 @@ export async function POST(request: Request) {
|
|
|
2232
2306
|
|
|
2233
2307
|
let usedContext = 0;
|
|
2234
2308
|
|
|
2309
|
+
// ========== PHASE 0 PRIORITY: If Phase 0 found a file, use it directly ==========
|
|
2310
|
+
// Phase 0 is deterministic (ID match) so it should ALWAYS take priority
|
|
2311
|
+
let phase0FileContent: { path: string; content: string } | null = null;
|
|
2312
|
+
if (deterministicMatch) {
|
|
2313
|
+
const fullPath = path.join(projectRoot, deterministicMatch.path);
|
|
2314
|
+
if (fs.existsSync(fullPath)) {
|
|
2315
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
2316
|
+
phase0FileContent = { path: deterministicMatch.path, content };
|
|
2317
|
+
debugLog("PHASE 0 PRIORITY: Using deterministic match as target file", {
|
|
2318
|
+
path: deterministicMatch.path,
|
|
2319
|
+
lineNumber: deterministicMatch.lineNumber,
|
|
2320
|
+
contentLength: content.length
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2235
2325
|
// Extract recommended file from component sources (to show first, avoid duplication)
|
|
2236
2326
|
let recommendedFileContent: { path: string; content: string } | null = null;
|
|
2237
|
-
if (recommendedFile) {
|
|
2327
|
+
if (recommendedFile && !phase0FileContent) {
|
|
2238
2328
|
// Check componentSources first
|
|
2239
2329
|
const idx = pageContext.componentSources.findIndex(c => c.path === recommendedFile.path);
|
|
2240
2330
|
if (idx !== -1) {
|
|
@@ -2250,10 +2340,28 @@ export async function POST(request: Request) {
|
|
|
2250
2340
|
// ========== FILE REDIRECT LOGIC (runs BEFORE building instructions) ==========
|
|
2251
2341
|
// This determines the ACTUAL target file by checking for text matches in imported components
|
|
2252
2342
|
// Must run first so that all subsequent code uses the correct file path
|
|
2253
|
-
|
|
2343
|
+
// PHASE 0 FILES SKIP REDIRECT - they're already definitively correct
|
|
2344
|
+
let actualTargetFile = phase0FileContent || recommendedFileContent || (pageContext.pageContent ? { path: pageContext.pageFile, content: pageContext.pageContent } : null);
|
|
2254
2345
|
let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
|
|
2255
2346
|
|
|
2256
|
-
|
|
2347
|
+
// If Phase 0 succeeded, we already know the exact line - use it directly
|
|
2348
|
+
if (phase0FileContent && deterministicMatch) {
|
|
2349
|
+
const lines = phase0FileContent.content.split('\n');
|
|
2350
|
+
const lineNum = deterministicMatch.lineNumber;
|
|
2351
|
+
elementLocation = {
|
|
2352
|
+
lineNumber: lineNum,
|
|
2353
|
+
snippet: lines.slice(Math.max(0, lineNum - 4), lineNum + 5).join('\n'),
|
|
2354
|
+
confidence: 'high',
|
|
2355
|
+
matchedBy: 'Phase 0 deterministic ID match'
|
|
2356
|
+
};
|
|
2357
|
+
debugLog("PHASE 0: Set element location from deterministic match", {
|
|
2358
|
+
lineNumber: lineNum,
|
|
2359
|
+
confidence: 'high'
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// SKIP REDIRECT LOGIC if Phase 0 already determined the file (it's already correct)
|
|
2364
|
+
if (!phase0FileContent && actualTargetFile && focusedElements && focusedElements.length > 0) {
|
|
2257
2365
|
const content = actualTargetFile.content;
|
|
2258
2366
|
|
|
2259
2367
|
// Search for focused element in the file using multiple strategies
|
|
@@ -2580,7 +2688,7 @@ ${linesWithNumbers}
|
|
|
2580
2688
|
path: actualTargetFile.path,
|
|
2581
2689
|
lines: targetContent.split('\n').length,
|
|
2582
2690
|
size: targetContent.length,
|
|
2583
|
-
wasRedirected: actualTargetFile.path !== recommendedFileContent
|
|
2691
|
+
wasRedirected: actualTargetFile.path !== recommendedFileContent?.path
|
|
2584
2692
|
});
|
|
2585
2693
|
} else if (pageContext.pageContent) {
|
|
2586
2694
|
// Fallback: use page file if no recommended file
|
|
@@ -620,6 +620,52 @@ function findElementLineInFile(
|
|
|
620
620
|
}
|
|
621
621
|
}
|
|
622
622
|
|
|
623
|
+
// ========== FALLBACK MATCHING (for non-Sonance codebases) ==========
|
|
624
|
+
// These are lower confidence but help find elements in ANY codebase
|
|
625
|
+
|
|
626
|
+
// FALLBACK 1: Flexible text search (just find the text anywhere)
|
|
627
|
+
if (focusedElement.textContent && focusedElement.textContent.trim().length > 5) {
|
|
628
|
+
const searchText = focusedElement.textContent.trim().substring(0, 30);
|
|
629
|
+
for (let i = 0; i < lines.length; i++) {
|
|
630
|
+
if (lines[i].includes(searchText)) {
|
|
631
|
+
debugLog("Fallback: Flexible text match found", { searchText, lineNumber: i + 1 });
|
|
632
|
+
return {
|
|
633
|
+
lineNumber: i + 1,
|
|
634
|
+
snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
|
|
635
|
+
confidence: 'low',
|
|
636
|
+
matchedBy: `flexible text match "${searchText}${focusedElement.textContent.length > 30 ? '...' : ''}"`
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// FALLBACK 2: Class fingerprint (use first few classes as unique identifier)
|
|
643
|
+
if (focusedElement.className && focusedElement.className.trim().length > 15) {
|
|
644
|
+
// Take first 2-3 distinctive classes as a fingerprint
|
|
645
|
+
const classes = focusedElement.className.trim().split(/\s+/);
|
|
646
|
+
// Filter out very common single-word utilities
|
|
647
|
+
const distinctiveClasses = classes.filter(c =>
|
|
648
|
+
c.length > 4 && !['flex', 'grid', 'block', 'hidden', 'relative', 'absolute'].includes(c)
|
|
649
|
+
).slice(0, 3);
|
|
650
|
+
|
|
651
|
+
if (distinctiveClasses.length >= 2) {
|
|
652
|
+
const fingerprint = distinctiveClasses.join(' ');
|
|
653
|
+
for (let i = 0; i < lines.length; i++) {
|
|
654
|
+
// Check if line contains ALL fingerprint classes
|
|
655
|
+
const lineHasAll = distinctiveClasses.every(cls => lines[i].includes(cls));
|
|
656
|
+
if (lineHasAll) {
|
|
657
|
+
debugLog("Fallback: Class fingerprint match found", { fingerprint, lineNumber: i + 1 });
|
|
658
|
+
return {
|
|
659
|
+
lineNumber: i + 1,
|
|
660
|
+
snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
|
|
661
|
+
confidence: 'low',
|
|
662
|
+
matchedBy: `class fingerprint "${fingerprint}"`
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
623
669
|
return null;
|
|
624
670
|
}
|
|
625
671
|
|
|
@@ -651,9 +697,14 @@ function scoreFilesForTextContent(
|
|
|
651
697
|
filesCount: importedFiles.length
|
|
652
698
|
});
|
|
653
699
|
|
|
654
|
-
//
|
|
700
|
+
// Search component files and page files (where JSX text lives)
|
|
701
|
+
// Expanded to work with ANY codebase structure
|
|
655
702
|
const componentFiles = importedFiles.filter(f =>
|
|
656
|
-
f.path.includes('components/') ||
|
|
703
|
+
f.path.includes('components/') ||
|
|
704
|
+
f.path.includes('/ui/') ||
|
|
705
|
+
f.path.includes('/_components/') ||
|
|
706
|
+
f.path.endsWith('.tsx') ||
|
|
707
|
+
f.path.endsWith('.jsx')
|
|
657
708
|
);
|
|
658
709
|
|
|
659
710
|
const results: { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number }[] = [];
|
|
@@ -749,19 +800,42 @@ function findElementInImportedFiles(
|
|
|
749
800
|
const routeName = pageDir.split('/').pop() || ''; // e.g., "typography"
|
|
750
801
|
|
|
751
802
|
for (const file of importedFiles) {
|
|
752
|
-
// Focus on component files (where UI elements live)
|
|
803
|
+
// Focus on component/page files (where UI elements live)
|
|
753
804
|
// Skip types, stores, utils, hooks - they don't contain JSX elements
|
|
754
|
-
|
|
805
|
+
// Expanded to work with ANY codebase structure
|
|
806
|
+
const isComponentFile =
|
|
807
|
+
file.path.includes('components/') ||
|
|
808
|
+
file.path.includes('/ui/') ||
|
|
809
|
+
file.path.includes('/_components/') ||
|
|
810
|
+
file.path.endsWith('.tsx') ||
|
|
811
|
+
file.path.endsWith('.jsx');
|
|
812
|
+
|
|
813
|
+
// Skip non-component files but allow page files
|
|
814
|
+
if (!isComponentFile) continue;
|
|
815
|
+
|
|
816
|
+
// Skip known non-UI files
|
|
817
|
+
if (file.path.includes('/types') ||
|
|
818
|
+
file.path.includes('/hooks/') ||
|
|
819
|
+
file.path.includes('/utils/') ||
|
|
820
|
+
file.path.includes('/lib/') ||
|
|
821
|
+
file.path.includes('.d.ts')) continue;
|
|
755
822
|
|
|
756
823
|
const result = findElementLineInFile(file.content, focusedElement);
|
|
757
|
-
|
|
824
|
+
// Accept ALL matches including low confidence - let the scoring decide
|
|
825
|
+
// Low confidence matches from fallback logic (text/class fingerprint) are still useful
|
|
826
|
+
if (result) {
|
|
758
827
|
let score = 0;
|
|
759
828
|
|
|
760
829
|
// Text content match is highest priority (+100)
|
|
761
|
-
if (result.matchedBy.includes('textContent') || result.matchedBy.includes('word match')) {
|
|
830
|
+
if (result.matchedBy.includes('textContent') || result.matchedBy.includes('word match') || result.matchedBy.includes('flexible text')) {
|
|
762
831
|
score += 100;
|
|
763
832
|
}
|
|
764
833
|
|
|
834
|
+
// Class fingerprint match gets medium score (+60)
|
|
835
|
+
if (result.matchedBy.includes('class fingerprint')) {
|
|
836
|
+
score += 60;
|
|
837
|
+
}
|
|
838
|
+
|
|
765
839
|
// Directory proximity - same directory tree as page file (+50)
|
|
766
840
|
// e.g., for page src/app/typography/page.tsx, prefer src/app/typography/_components/
|
|
767
841
|
if (pageDir && file.path.includes(pageDir)) {
|
|
@@ -774,8 +848,8 @@ function findElementInImportedFiles(
|
|
|
774
848
|
score += 30;
|
|
775
849
|
}
|
|
776
850
|
|
|
777
|
-
// Confidence bonus
|
|
778
|
-
score += result.confidence === 'high' ? 20 :
|
|
851
|
+
// Confidence bonus - low confidence still gets some points
|
|
852
|
+
score += result.confidence === 'high' ? 30 : result.confidence === 'medium' ? 20 : 5;
|
|
779
853
|
|
|
780
854
|
matches.push({
|
|
781
855
|
path: file.path,
|
|
@@ -2201,9 +2275,25 @@ export async function POST(request: Request) {
|
|
|
2201
2275
|
|
|
2202
2276
|
let usedContext = 0;
|
|
2203
2277
|
|
|
2278
|
+
// ========== PHASE 0 PRIORITY: If Phase 0 found a file, use it directly ==========
|
|
2279
|
+
// Phase 0 is deterministic (ID match) so it should ALWAYS take priority
|
|
2280
|
+
let phase0FileContent: { path: string; content: string } | null = null;
|
|
2281
|
+
if (deterministicMatch) {
|
|
2282
|
+
const fullPath = path.join(projectRoot, deterministicMatch.path);
|
|
2283
|
+
if (fs.existsSync(fullPath)) {
|
|
2284
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
2285
|
+
phase0FileContent = { path: deterministicMatch.path, content };
|
|
2286
|
+
debugLog("PHASE 0 PRIORITY: Using deterministic match as target file", {
|
|
2287
|
+
path: deterministicMatch.path,
|
|
2288
|
+
lineNumber: deterministicMatch.lineNumber,
|
|
2289
|
+
contentLength: content.length
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2204
2294
|
// Extract recommended file from component sources (to show first, avoid duplication)
|
|
2205
2295
|
let recommendedFileContent: { path: string; content: string } | null = null;
|
|
2206
|
-
if (recommendedFile) {
|
|
2296
|
+
if (recommendedFile && !phase0FileContent) {
|
|
2207
2297
|
// Check componentSources first
|
|
2208
2298
|
const idx = pageContext.componentSources.findIndex(c => c.path === recommendedFile.path);
|
|
2209
2299
|
if (idx !== -1) {
|
|
@@ -2219,10 +2309,28 @@ export async function POST(request: Request) {
|
|
|
2219
2309
|
// ========== FILE REDIRECT LOGIC (runs BEFORE building instructions) ==========
|
|
2220
2310
|
// This determines the ACTUAL target file by checking for text matches in imported components
|
|
2221
2311
|
// Must run first so that all subsequent code uses the correct file path
|
|
2222
|
-
|
|
2312
|
+
// PHASE 0 FILES SKIP REDIRECT - they're already definitively correct
|
|
2313
|
+
let actualTargetFile = phase0FileContent || recommendedFileContent || (pageContext.pageContent ? { path: pageContext.pageFile, content: pageContext.pageContent } : null);
|
|
2223
2314
|
let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
|
|
2224
2315
|
|
|
2225
|
-
|
|
2316
|
+
// If Phase 0 succeeded, we already know the exact line - use it directly
|
|
2317
|
+
if (phase0FileContent && deterministicMatch) {
|
|
2318
|
+
const lines = phase0FileContent.content.split('\n');
|
|
2319
|
+
const lineNum = deterministicMatch.lineNumber;
|
|
2320
|
+
elementLocation = {
|
|
2321
|
+
lineNumber: lineNum,
|
|
2322
|
+
snippet: lines.slice(Math.max(0, lineNum - 4), lineNum + 5).join('\n'),
|
|
2323
|
+
confidence: 'high',
|
|
2324
|
+
matchedBy: 'Phase 0 deterministic ID match'
|
|
2325
|
+
};
|
|
2326
|
+
debugLog("PHASE 0: Set element location from deterministic match", {
|
|
2327
|
+
lineNumber: lineNum,
|
|
2328
|
+
confidence: 'high'
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// SKIP REDIRECT LOGIC if Phase 0 already determined the file (it's already correct)
|
|
2333
|
+
if (!phase0FileContent && actualTargetFile && focusedElements && focusedElements.length > 0) {
|
|
2226
2334
|
const content = actualTargetFile.content;
|
|
2227
2335
|
|
|
2228
2336
|
// Search for focused element in the file using multiple strategies
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.107",
|
|
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",
|