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
- // Only search component files (where JSX text lives)
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/') || f.path.includes('/ui/')
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
- if (!file.path.includes('components/') && !file.path.includes('/ui/') && !file.path.includes('/_components/')) continue;
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
- if (result && result.confidence !== 'low') {
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 : 10;
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
- let actualTargetFile = recommendedFileContent || (pageContext.pageContent ? { path: pageContext.pageFile, content: pageContext.pageContent } : null);
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
- if (actualTargetFile && focusedElements && focusedElements.length > 0) {
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.path
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
- // Only search component files (where JSX text lives)
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/') || f.path.includes('/ui/')
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
- if (!file.path.includes('components/') && !file.path.includes('/ui/') && !file.path.includes('/_components/')) continue;
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
- if (result && result.confidence !== 'low') {
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 : 10;
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
- let actualTargetFile = recommendedFileContent || (pageContext.pageContent ? { path: pageContext.pageFile, content: pageContext.pageContent } : null);
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
- if (actualTargetFile && focusedElements && focusedElements.length > 0) {
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.105",
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",