sonance-brand-mcp 1.3.95 → 1.3.97
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.
|
@@ -516,6 +516,98 @@ function findElementLineInFile(
|
|
|
516
516
|
return null;
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Search imported component files for specific TEXT CONTENT
|
|
521
|
+
* This is used to redirect from parent components to child components
|
|
522
|
+
* when the actual visible text lives in an imported file.
|
|
523
|
+
*
|
|
524
|
+
* Example: "What's the essence of this process?" might be in EssenceTab.tsx
|
|
525
|
+
* but the parent QuickAddProcessModal.tsx was selected by Phase 2a.
|
|
526
|
+
*/
|
|
527
|
+
/**
|
|
528
|
+
* Score imported files by how many discovered text strings they contain.
|
|
529
|
+
* This finds the TRUE file by matching ALL visible text, not just one substring.
|
|
530
|
+
*
|
|
531
|
+
* Example: If annotation discovers ["What's the essence?", "Essence Quality", "Show Example"]
|
|
532
|
+
* and EssenceTab.tsx contains 2 of them while QuickAddProcessModal.tsx contains 1,
|
|
533
|
+
* EssenceTab.tsx wins with the higher score.
|
|
534
|
+
*/
|
|
535
|
+
function scoreFilesForTextContent(
|
|
536
|
+
textContents: string[],
|
|
537
|
+
importedFiles: { path: string; content: string }[]
|
|
538
|
+
): { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number } | null {
|
|
539
|
+
if (!textContents || textContents.length === 0) return null;
|
|
540
|
+
|
|
541
|
+
debugLog("Scoring imported files for text content", {
|
|
542
|
+
textCount: textContents.length,
|
|
543
|
+
texts: textContents.slice(0, 5).map(t => t.substring(0, 30)),
|
|
544
|
+
filesCount: importedFiles.length
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Only search component files (where JSX text lives)
|
|
548
|
+
const componentFiles = importedFiles.filter(f =>
|
|
549
|
+
f.path.includes('components/') || f.path.includes('/ui/')
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
const results: { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number }[] = [];
|
|
553
|
+
|
|
554
|
+
for (const file of componentFiles) {
|
|
555
|
+
let score = 0;
|
|
556
|
+
const matchedTexts: string[] = [];
|
|
557
|
+
let firstMatchLine = 0;
|
|
558
|
+
|
|
559
|
+
for (const text of textContents) {
|
|
560
|
+
if (!text || text.length < 5) continue;
|
|
561
|
+
|
|
562
|
+
// Use first 20 chars for matching (handles dynamic suffixes)
|
|
563
|
+
const searchText = text.substring(0, 20);
|
|
564
|
+
|
|
565
|
+
// Check if this text exists in the file
|
|
566
|
+
if (file.content.includes(searchText)) {
|
|
567
|
+
score++;
|
|
568
|
+
matchedTexts.push(text.substring(0, 30) + (text.length > 30 ? '...' : ''));
|
|
569
|
+
|
|
570
|
+
// Find line number for first match (for element location)
|
|
571
|
+
if (firstMatchLine === 0) {
|
|
572
|
+
const lines = file.content.split('\n');
|
|
573
|
+
for (let i = 0; i < lines.length; i++) {
|
|
574
|
+
if (lines[i].includes(searchText)) {
|
|
575
|
+
firstMatchLine = i + 1;
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (score > 0) {
|
|
584
|
+
results.push({
|
|
585
|
+
path: file.path,
|
|
586
|
+
content: file.content,
|
|
587
|
+
score,
|
|
588
|
+
matchedTexts,
|
|
589
|
+
firstMatchLine
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Sort by score descending (highest match count wins)
|
|
595
|
+
results.sort((a, b) => b.score - a.score);
|
|
596
|
+
|
|
597
|
+
if (results.length > 0) {
|
|
598
|
+
debugLog("Text scoring results", {
|
|
599
|
+
topFile: results[0].path,
|
|
600
|
+
topScore: results[0].score,
|
|
601
|
+
topMatches: results[0].matchedTexts,
|
|
602
|
+
allResults: results.slice(0, 3).map(r => ({ path: r.path, score: r.score }))
|
|
603
|
+
});
|
|
604
|
+
} else {
|
|
605
|
+
debugLog("No text matches found in any imported component files");
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return results.length > 0 ? results[0] : null;
|
|
609
|
+
}
|
|
610
|
+
|
|
519
611
|
/**
|
|
520
612
|
* Search imported component files for the focused element
|
|
521
613
|
* Reuses findElementLineInFile() for consistent detection
|
|
@@ -1528,7 +1620,7 @@ export async function POST(request: Request) {
|
|
|
1528
1620
|
const confirmedPath = phase2aMatches.find(p =>
|
|
1529
1621
|
focusedElementHints.some(h => h.path === p && h.score > 0)
|
|
1530
1622
|
);
|
|
1531
|
-
|
|
1623
|
+
recommendedFile = {
|
|
1532
1624
|
path: confirmedPath!,
|
|
1533
1625
|
reason: `Phase 2a component-name match confirmed by element search`
|
|
1534
1626
|
};
|
|
@@ -1552,7 +1644,7 @@ export async function POST(request: Request) {
|
|
|
1552
1644
|
selectedPath: bestMatch,
|
|
1553
1645
|
allCandidates: phase2aMatches
|
|
1554
1646
|
});
|
|
1555
|
-
|
|
1647
|
+
} else {
|
|
1556
1648
|
// FALLBACK PRIORITY 4: Use the page file from the current route
|
|
1557
1649
|
const routePageFile = discoverPageFile(pageRoute || "/", projectRoot);
|
|
1558
1650
|
|
|
@@ -1666,7 +1758,7 @@ User Request: "${userPrompt}"
|
|
|
1666
1758
|
let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
|
|
1667
1759
|
let actualTargetFile = recommendedFileContent; // May change if we redirect
|
|
1668
1760
|
|
|
1669
|
-
|
|
1761
|
+
if (focusedElements && focusedElements.length > 0) {
|
|
1670
1762
|
for (const el of focusedElements) {
|
|
1671
1763
|
elementLocation = findElementLineInFile(content, el);
|
|
1672
1764
|
if (elementLocation) {
|
|
@@ -1680,6 +1772,72 @@ User Request: "${userPrompt}"
|
|
|
1680
1772
|
}
|
|
1681
1773
|
}
|
|
1682
1774
|
|
|
1775
|
+
// TEXT SCORING REDIRECT: Score ALL imported files by how many discovered
|
|
1776
|
+
// text strings they contain. The file with the highest score is the TRUE target.
|
|
1777
|
+
// This handles parent/child component cases accurately.
|
|
1778
|
+
if (elementLocation && elementLocation.confidence !== 'high') {
|
|
1779
|
+
// Collect ALL text content from focused elements
|
|
1780
|
+
const allTextContent = focusedElements
|
|
1781
|
+
.filter(e => e.textContent && e.textContent.length > 5)
|
|
1782
|
+
.map(e => e.textContent!);
|
|
1783
|
+
|
|
1784
|
+
if (allTextContent.length > 0) {
|
|
1785
|
+
debugLog("Medium/low confidence match - scoring imports for all text", {
|
|
1786
|
+
currentFile: recommendedFileContent.path,
|
|
1787
|
+
currentConfidence: elementLocation.confidence,
|
|
1788
|
+
textCount: allTextContent.length,
|
|
1789
|
+
texts: allTextContent.slice(0, 5).map(t => t.substring(0, 25))
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
// Score all imported files
|
|
1793
|
+
const bestMatch = scoreFilesForTextContent(
|
|
1794
|
+
allTextContent,
|
|
1795
|
+
pageContext.componentSources
|
|
1796
|
+
);
|
|
1797
|
+
|
|
1798
|
+
// Also score the current file for comparison
|
|
1799
|
+
const currentFileScore = allTextContent.filter(text =>
|
|
1800
|
+
recommendedFileContent.content.includes(text.substring(0, 20))
|
|
1801
|
+
).length;
|
|
1802
|
+
|
|
1803
|
+
debugLog("Text scoring comparison", {
|
|
1804
|
+
currentFile: recommendedFileContent.path,
|
|
1805
|
+
currentScore: currentFileScore,
|
|
1806
|
+
bestImport: bestMatch?.path || 'none',
|
|
1807
|
+
bestImportScore: bestMatch?.score || 0
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
// Redirect only if imported file has MORE matches than current file
|
|
1811
|
+
if (bestMatch && bestMatch.score > currentFileScore) {
|
|
1812
|
+
debugLog("TEXT REDIRECT: Imported file has more text matches", {
|
|
1813
|
+
originalFile: recommendedFileContent.path,
|
|
1814
|
+
originalScore: currentFileScore,
|
|
1815
|
+
redirectTo: bestMatch.path,
|
|
1816
|
+
redirectScore: bestMatch.score,
|
|
1817
|
+
matchedTexts: bestMatch.matchedTexts
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
// Switch target file to where most text content lives
|
|
1821
|
+
actualTargetFile = {
|
|
1822
|
+
path: bestMatch.path,
|
|
1823
|
+
content: bestMatch.content
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
// Find a line with matched text for element location
|
|
1827
|
+
const lines = bestMatch.content.split('\n');
|
|
1828
|
+
const snippetStart = Math.max(0, bestMatch.firstMatchLine - 4);
|
|
1829
|
+
const snippetEnd = Math.min(lines.length, bestMatch.firstMatchLine + 5);
|
|
1830
|
+
|
|
1831
|
+
elementLocation = {
|
|
1832
|
+
lineNumber: bestMatch.firstMatchLine,
|
|
1833
|
+
snippet: lines.slice(snippetStart, snippetEnd).join('\n'),
|
|
1834
|
+
confidence: 'high',
|
|
1835
|
+
matchedBy: `text scoring: ${bestMatch.score} matches in imported file`
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1683
1841
|
// DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
|
|
1684
1842
|
if (!elementLocation) {
|
|
1685
1843
|
debugLog("Element not in main file, searching imported components...", {
|
|
@@ -512,6 +512,98 @@ function findElementLineInFile(
|
|
|
512
512
|
return null;
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Search imported component files for specific TEXT CONTENT
|
|
517
|
+
* This is used to redirect from parent components to child components
|
|
518
|
+
* when the actual visible text lives in an imported file.
|
|
519
|
+
*
|
|
520
|
+
* Example: "What's the essence of this process?" might be in EssenceTab.tsx
|
|
521
|
+
* but the parent QuickAddProcessModal.tsx was selected by Phase 2a.
|
|
522
|
+
*/
|
|
523
|
+
/**
|
|
524
|
+
* Score imported files by how many discovered text strings they contain.
|
|
525
|
+
* This finds the TRUE file by matching ALL visible text, not just one substring.
|
|
526
|
+
*
|
|
527
|
+
* Example: If annotation discovers ["What's the essence?", "Essence Quality", "Show Example"]
|
|
528
|
+
* and EssenceTab.tsx contains 2 of them while QuickAddProcessModal.tsx contains 1,
|
|
529
|
+
* EssenceTab.tsx wins with the higher score.
|
|
530
|
+
*/
|
|
531
|
+
function scoreFilesForTextContent(
|
|
532
|
+
textContents: string[],
|
|
533
|
+
importedFiles: { path: string; content: string }[]
|
|
534
|
+
): { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number } | null {
|
|
535
|
+
if (!textContents || textContents.length === 0) return null;
|
|
536
|
+
|
|
537
|
+
debugLog("Scoring imported files for text content", {
|
|
538
|
+
textCount: textContents.length,
|
|
539
|
+
texts: textContents.slice(0, 5).map(t => t.substring(0, 30)),
|
|
540
|
+
filesCount: importedFiles.length
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Only search component files (where JSX text lives)
|
|
544
|
+
const componentFiles = importedFiles.filter(f =>
|
|
545
|
+
f.path.includes('components/') || f.path.includes('/ui/')
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
const results: { path: string; content: string; score: number; matchedTexts: string[]; firstMatchLine: number }[] = [];
|
|
549
|
+
|
|
550
|
+
for (const file of componentFiles) {
|
|
551
|
+
let score = 0;
|
|
552
|
+
const matchedTexts: string[] = [];
|
|
553
|
+
let firstMatchLine = 0;
|
|
554
|
+
|
|
555
|
+
for (const text of textContents) {
|
|
556
|
+
if (!text || text.length < 5) continue;
|
|
557
|
+
|
|
558
|
+
// Use first 20 chars for matching (handles dynamic suffixes)
|
|
559
|
+
const searchText = text.substring(0, 20);
|
|
560
|
+
|
|
561
|
+
// Check if this text exists in the file
|
|
562
|
+
if (file.content.includes(searchText)) {
|
|
563
|
+
score++;
|
|
564
|
+
matchedTexts.push(text.substring(0, 30) + (text.length > 30 ? '...' : ''));
|
|
565
|
+
|
|
566
|
+
// Find line number for first match (for element location)
|
|
567
|
+
if (firstMatchLine === 0) {
|
|
568
|
+
const lines = file.content.split('\n');
|
|
569
|
+
for (let i = 0; i < lines.length; i++) {
|
|
570
|
+
if (lines[i].includes(searchText)) {
|
|
571
|
+
firstMatchLine = i + 1;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (score > 0) {
|
|
580
|
+
results.push({
|
|
581
|
+
path: file.path,
|
|
582
|
+
content: file.content,
|
|
583
|
+
score,
|
|
584
|
+
matchedTexts,
|
|
585
|
+
firstMatchLine
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Sort by score descending (highest match count wins)
|
|
591
|
+
results.sort((a, b) => b.score - a.score);
|
|
592
|
+
|
|
593
|
+
if (results.length > 0) {
|
|
594
|
+
debugLog("Text scoring results", {
|
|
595
|
+
topFile: results[0].path,
|
|
596
|
+
topScore: results[0].score,
|
|
597
|
+
topMatches: results[0].matchedTexts,
|
|
598
|
+
allResults: results.slice(0, 3).map(r => ({ path: r.path, score: r.score }))
|
|
599
|
+
});
|
|
600
|
+
} else {
|
|
601
|
+
debugLog("No text matches found in any imported component files");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return results.length > 0 ? results[0] : null;
|
|
605
|
+
}
|
|
606
|
+
|
|
515
607
|
/**
|
|
516
608
|
* Search imported component files for the focused element
|
|
517
609
|
* Reuses findElementLineInFile() for consistent detection
|
|
@@ -1497,7 +1589,7 @@ export async function POST(request: Request) {
|
|
|
1497
1589
|
const confirmedPath = phase2aMatches.find(p =>
|
|
1498
1590
|
focusedElementHints.some(h => h.path === p && h.score > 0)
|
|
1499
1591
|
);
|
|
1500
|
-
|
|
1592
|
+
recommendedFile = {
|
|
1501
1593
|
path: confirmedPath!,
|
|
1502
1594
|
reason: `Phase 2a component-name match confirmed by element search`
|
|
1503
1595
|
};
|
|
@@ -1521,7 +1613,7 @@ export async function POST(request: Request) {
|
|
|
1521
1613
|
selectedPath: bestMatch,
|
|
1522
1614
|
allCandidates: phase2aMatches
|
|
1523
1615
|
});
|
|
1524
|
-
|
|
1616
|
+
} else {
|
|
1525
1617
|
// FALLBACK PRIORITY 4: Use the page file from the current route
|
|
1526
1618
|
const routePageFile = discoverPageFile(pageRoute || "/", projectRoot);
|
|
1527
1619
|
|
|
@@ -1635,7 +1727,7 @@ User Request: "${userPrompt}"
|
|
|
1635
1727
|
let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
|
|
1636
1728
|
let actualTargetFile = recommendedFileContent; // May change if we redirect
|
|
1637
1729
|
|
|
1638
|
-
|
|
1730
|
+
if (focusedElements && focusedElements.length > 0) {
|
|
1639
1731
|
for (const el of focusedElements) {
|
|
1640
1732
|
elementLocation = findElementLineInFile(content, el);
|
|
1641
1733
|
if (elementLocation) {
|
|
@@ -1649,6 +1741,72 @@ User Request: "${userPrompt}"
|
|
|
1649
1741
|
}
|
|
1650
1742
|
}
|
|
1651
1743
|
|
|
1744
|
+
// TEXT SCORING REDIRECT: Score ALL imported files by how many discovered
|
|
1745
|
+
// text strings they contain. The file with the highest score is the TRUE target.
|
|
1746
|
+
// This handles parent/child component cases accurately.
|
|
1747
|
+
if (elementLocation && elementLocation.confidence !== 'high') {
|
|
1748
|
+
// Collect ALL text content from focused elements
|
|
1749
|
+
const allTextContent = focusedElements
|
|
1750
|
+
.filter(e => e.textContent && e.textContent.length > 5)
|
|
1751
|
+
.map(e => e.textContent!);
|
|
1752
|
+
|
|
1753
|
+
if (allTextContent.length > 0) {
|
|
1754
|
+
debugLog("Medium/low confidence match - scoring imports for all text", {
|
|
1755
|
+
currentFile: recommendedFileContent.path,
|
|
1756
|
+
currentConfidence: elementLocation.confidence,
|
|
1757
|
+
textCount: allTextContent.length,
|
|
1758
|
+
texts: allTextContent.slice(0, 5).map(t => t.substring(0, 25))
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
// Score all imported files
|
|
1762
|
+
const bestMatch = scoreFilesForTextContent(
|
|
1763
|
+
allTextContent,
|
|
1764
|
+
pageContext.componentSources
|
|
1765
|
+
);
|
|
1766
|
+
|
|
1767
|
+
// Also score the current file for comparison
|
|
1768
|
+
const currentFileScore = allTextContent.filter(text =>
|
|
1769
|
+
recommendedFileContent.content.includes(text.substring(0, 20))
|
|
1770
|
+
).length;
|
|
1771
|
+
|
|
1772
|
+
debugLog("Text scoring comparison", {
|
|
1773
|
+
currentFile: recommendedFileContent.path,
|
|
1774
|
+
currentScore: currentFileScore,
|
|
1775
|
+
bestImport: bestMatch?.path || 'none',
|
|
1776
|
+
bestImportScore: bestMatch?.score || 0
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
// Redirect only if imported file has MORE matches than current file
|
|
1780
|
+
if (bestMatch && bestMatch.score > currentFileScore) {
|
|
1781
|
+
debugLog("TEXT REDIRECT: Imported file has more text matches", {
|
|
1782
|
+
originalFile: recommendedFileContent.path,
|
|
1783
|
+
originalScore: currentFileScore,
|
|
1784
|
+
redirectTo: bestMatch.path,
|
|
1785
|
+
redirectScore: bestMatch.score,
|
|
1786
|
+
matchedTexts: bestMatch.matchedTexts
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
// Switch target file to where most text content lives
|
|
1790
|
+
actualTargetFile = {
|
|
1791
|
+
path: bestMatch.path,
|
|
1792
|
+
content: bestMatch.content
|
|
1793
|
+
};
|
|
1794
|
+
|
|
1795
|
+
// Find a line with matched text for element location
|
|
1796
|
+
const lines = bestMatch.content.split('\n');
|
|
1797
|
+
const snippetStart = Math.max(0, bestMatch.firstMatchLine - 4);
|
|
1798
|
+
const snippetEnd = Math.min(lines.length, bestMatch.firstMatchLine + 5);
|
|
1799
|
+
|
|
1800
|
+
elementLocation = {
|
|
1801
|
+
lineNumber: bestMatch.firstMatchLine,
|
|
1802
|
+
snippet: lines.slice(snippetStart, snippetEnd).join('\n'),
|
|
1803
|
+
confidence: 'high',
|
|
1804
|
+
matchedBy: `text scoring: ${bestMatch.score} matches in imported file`
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1652
1810
|
// DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
|
|
1653
1811
|
if (!elementLocation) {
|
|
1654
1812
|
debugLog("Element not in main file, searching imported components...", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.97",
|
|
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",
|