sonance-brand-mcp 1.3.31 → 1.3.33
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.
|
@@ -212,8 +212,8 @@ export async function POST(request: Request) {
|
|
|
212
212
|
// Generate a unique session ID
|
|
213
213
|
const newSessionId = randomUUID().slice(0, 8);
|
|
214
214
|
|
|
215
|
-
// Gather page context
|
|
216
|
-
const pageContext = gatherPageContext(pageRoute || "/", projectRoot);
|
|
215
|
+
// Gather page context (with keyword search based on user prompt)
|
|
216
|
+
const pageContext = gatherPageContext(pageRoute || "/", projectRoot, userPrompt);
|
|
217
217
|
|
|
218
218
|
// Build user message with vision
|
|
219
219
|
const messageContent: Anthropic.MessageCreateParams["messages"][0]["content"] = [];
|
|
@@ -843,11 +843,12 @@ function discoverLayoutFiles(pageFile: string | null, projectRoot: string): stri
|
|
|
843
843
|
|
|
844
844
|
/**
|
|
845
845
|
* Gather context about the current page for AI analysis
|
|
846
|
-
* Uses recursive import resolution to build complete component graph
|
|
846
|
+
* Uses recursive import resolution AND keyword search to build complete component graph
|
|
847
847
|
*/
|
|
848
848
|
function gatherPageContext(
|
|
849
849
|
pageRoute: string,
|
|
850
|
-
projectRoot: string
|
|
850
|
+
projectRoot: string,
|
|
851
|
+
userPrompt?: string
|
|
851
852
|
): {
|
|
852
853
|
pageFile: string | null;
|
|
853
854
|
pageContent: string;
|
|
@@ -886,6 +887,24 @@ function gatherPageContext(
|
|
|
886
887
|
}
|
|
887
888
|
}
|
|
888
889
|
|
|
890
|
+
// KEYWORD SEARCH: Find additional files based on user prompt keywords
|
|
891
|
+
// This catches files that aren't in the import chain but contain relevant UI text
|
|
892
|
+
if (userPrompt) {
|
|
893
|
+
const keywords = extractKeywordsFromPrompt(userPrompt);
|
|
894
|
+
if (keywords.length > 0) {
|
|
895
|
+
const searchResults = searchFilesForKeywords(keywords, projectRoot, 15);
|
|
896
|
+
|
|
897
|
+
for (const result of searchResults) {
|
|
898
|
+
// Only add if not already in our list
|
|
899
|
+
if (!visited.has(result.path)) {
|
|
900
|
+
visited.add(result.path);
|
|
901
|
+
componentSources.push({ path: result.path, content: result.content });
|
|
902
|
+
debugLog("[apply] Added file from keyword search", { path: result.path, matchCount: result.matchCount });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
889
908
|
let globalsCSS = "";
|
|
890
909
|
const globalsCSSPatterns = [
|
|
891
910
|
"src/app/globals.css",
|
|
@@ -1087,19 +1106,142 @@ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | nul
|
|
|
1087
1106
|
}
|
|
1088
1107
|
|
|
1089
1108
|
function extractImports(content: string): string[] {
|
|
1090
|
-
// Match
|
|
1091
|
-
|
|
1109
|
+
// Match both import AND export statements for @/ and relative paths
|
|
1110
|
+
// This catches barrel files (index.ts) that use: export { Foo } from './Foo'
|
|
1111
|
+
const importExportRegex = /(?:import|export)\s+.*?\s+from\s+["'](@\/[^"']+|\.\.?\/[^"']+)["']/g;
|
|
1092
1112
|
const imports: string[] = [];
|
|
1093
1113
|
let match;
|
|
1094
1114
|
|
|
1095
|
-
while ((match =
|
|
1115
|
+
while ((match = importExportRegex.exec(content)) !== null) {
|
|
1096
1116
|
imports.push(match[1]);
|
|
1097
1117
|
}
|
|
1098
1118
|
|
|
1099
|
-
debugLog("[apply] Extracted imports", { count: imports.length, imports: imports.slice(0, 10) });
|
|
1119
|
+
debugLog("[apply] Extracted imports/exports", { count: imports.length, imports: imports.slice(0, 10) });
|
|
1100
1120
|
return imports;
|
|
1101
1121
|
}
|
|
1102
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* Extract meaningful keywords from user prompt for file searching
|
|
1125
|
+
* Looks for quoted strings, capitalized phrases, and specific patterns
|
|
1126
|
+
*/
|
|
1127
|
+
function extractKeywordsFromPrompt(prompt: string): string[] {
|
|
1128
|
+
const keywords: string[] = [];
|
|
1129
|
+
|
|
1130
|
+
// 1. Extract quoted strings (e.g., "View Assets", 'Edit Process')
|
|
1131
|
+
const quotedRegex = /["']([^"']+)["']/g;
|
|
1132
|
+
let match;
|
|
1133
|
+
while ((match = quotedRegex.exec(prompt)) !== null) {
|
|
1134
|
+
if (match[1].length > 2 && match[1].length < 50) {
|
|
1135
|
+
keywords.push(match[1]);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// 2. Extract capitalized phrases (e.g., "View Assets", "Quick Actions")
|
|
1140
|
+
// Matches 2+ capitalized words together
|
|
1141
|
+
const capitalizedRegex = /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b/g;
|
|
1142
|
+
while ((match = capitalizedRegex.exec(prompt)) !== null) {
|
|
1143
|
+
if (!keywords.includes(match[1])) {
|
|
1144
|
+
keywords.push(match[1]);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// 3. Extract single capitalized words that might be component names
|
|
1149
|
+
const singleCapRegex = /\b([A-Z][a-z]{3,})\b/g;
|
|
1150
|
+
while ((match = singleCapRegex.exec(prompt)) !== null) {
|
|
1151
|
+
// Skip common words
|
|
1152
|
+
const skipWords = ["The", "This", "That", "Make", "Change", "Edit", "Update", "Add", "Remove", "Delete"];
|
|
1153
|
+
if (!skipWords.includes(match[1]) && !keywords.includes(match[1])) {
|
|
1154
|
+
keywords.push(match[1]);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
debugLog("[apply] Extracted keywords from prompt", { prompt: prompt.substring(0, 100), keywords });
|
|
1159
|
+
return keywords;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Recursively search files in a directory for keyword matches
|
|
1164
|
+
* Returns file paths that contain any of the keywords
|
|
1165
|
+
*/
|
|
1166
|
+
function searchFilesForKeywords(
|
|
1167
|
+
keywords: string[],
|
|
1168
|
+
projectRoot: string,
|
|
1169
|
+
maxFiles: number = 10
|
|
1170
|
+
): { path: string; content: string; matchCount: number }[] {
|
|
1171
|
+
if (keywords.length === 0) return [];
|
|
1172
|
+
|
|
1173
|
+
const results: { path: string; content: string; matchCount: number }[] = [];
|
|
1174
|
+
const searchDirs = ["components", "src/components", "app", "src/app", "pages", "src/pages"];
|
|
1175
|
+
const extensions = [".tsx", ".jsx", ".ts", ".js"];
|
|
1176
|
+
const visited = new Set<string>();
|
|
1177
|
+
|
|
1178
|
+
function searchDir(dirPath: string, depth: number = 0) {
|
|
1179
|
+
if (depth > 5 || results.length >= maxFiles) return;
|
|
1180
|
+
|
|
1181
|
+
const fullDirPath = path.join(projectRoot, dirPath);
|
|
1182
|
+
if (!fs.existsSync(fullDirPath) || visited.has(fullDirPath)) return;
|
|
1183
|
+
visited.add(fullDirPath);
|
|
1184
|
+
|
|
1185
|
+
try {
|
|
1186
|
+
const entries = fs.readdirSync(fullDirPath, { withFileTypes: true });
|
|
1187
|
+
|
|
1188
|
+
for (const entry of entries) {
|
|
1189
|
+
if (results.length >= maxFiles) break;
|
|
1190
|
+
|
|
1191
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
1192
|
+
const fullEntryPath = path.join(projectRoot, entryPath);
|
|
1193
|
+
|
|
1194
|
+
if (entry.isDirectory()) {
|
|
1195
|
+
// Skip node_modules, .git, etc.
|
|
1196
|
+
if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
1197
|
+
searchDir(entryPath, depth + 1);
|
|
1198
|
+
}
|
|
1199
|
+
} else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
1200
|
+
try {
|
|
1201
|
+
const content = fs.readFileSync(fullEntryPath, "utf-8");
|
|
1202
|
+
|
|
1203
|
+
// Count keyword matches
|
|
1204
|
+
let matchCount = 0;
|
|
1205
|
+
for (const keyword of keywords) {
|
|
1206
|
+
// Case-insensitive search
|
|
1207
|
+
const regex = new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
1208
|
+
const matches = content.match(regex);
|
|
1209
|
+
if (matches) {
|
|
1210
|
+
matchCount += matches.length;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (matchCount > 0) {
|
|
1215
|
+
results.push({ path: entryPath, content, matchCount });
|
|
1216
|
+
}
|
|
1217
|
+
} catch {
|
|
1218
|
+
// Skip files that can't be read
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
} catch {
|
|
1223
|
+
// Skip directories that can't be read
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Search each directory
|
|
1228
|
+
for (const dir of searchDirs) {
|
|
1229
|
+
searchDir(dir);
|
|
1230
|
+
if (results.length >= maxFiles) break;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Sort by match count (most matches first)
|
|
1234
|
+
results.sort((a, b) => b.matchCount - a.matchCount);
|
|
1235
|
+
|
|
1236
|
+
debugLog("[apply] Keyword search results", {
|
|
1237
|
+
keywords,
|
|
1238
|
+
filesFound: results.length,
|
|
1239
|
+
topMatches: results.slice(0, 5).map(r => ({ path: r.path, matchCount: r.matchCount }))
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
return results.slice(0, maxFiles);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1103
1245
|
// Cache for tsconfig path aliases
|
|
1104
1246
|
let cachedPathAliases: Map<string, string> | null = null;
|
|
1105
1247
|
let cachedProjectRoot: string | null = null;
|
|
@@ -222,8 +222,8 @@ export async function POST(request: Request) {
|
|
|
222
222
|
);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
// Gather page context (including focused element files
|
|
226
|
-
const pageContext = gatherPageContext(pageRoute, projectRoot, focusedElements);
|
|
225
|
+
// Gather page context (including focused element files and keyword search)
|
|
226
|
+
const pageContext = gatherPageContext(pageRoute, projectRoot, focusedElements, userPrompt);
|
|
227
227
|
|
|
228
228
|
// Build user message with vision
|
|
229
229
|
const messageContent: Anthropic.MessageCreateParams["messages"][0]["content"] = [];
|
|
@@ -711,12 +711,13 @@ function discoverLayoutFiles(pageFile: string | null, projectRoot: string): stri
|
|
|
711
711
|
|
|
712
712
|
/**
|
|
713
713
|
* Gather context about the current page for AI analysis
|
|
714
|
-
* Uses recursive import resolution to build complete component graph
|
|
714
|
+
* Uses recursive import resolution AND keyword search to build complete component graph
|
|
715
715
|
*/
|
|
716
716
|
function gatherPageContext(
|
|
717
717
|
pageRoute: string,
|
|
718
718
|
projectRoot: string,
|
|
719
|
-
focusedElements?: VisionFocusedElement[]
|
|
719
|
+
focusedElements?: VisionFocusedElement[],
|
|
720
|
+
userPrompt?: string
|
|
720
721
|
): {
|
|
721
722
|
pageFile: string | null;
|
|
722
723
|
pageContent: string;
|
|
@@ -768,6 +769,24 @@ function gatherPageContext(
|
|
|
768
769
|
}
|
|
769
770
|
}
|
|
770
771
|
|
|
772
|
+
// KEYWORD SEARCH: Find additional files based on user prompt keywords
|
|
773
|
+
// This catches files that aren't in the import chain but contain relevant UI text
|
|
774
|
+
if (userPrompt) {
|
|
775
|
+
const keywords = extractKeywordsFromPrompt(userPrompt);
|
|
776
|
+
if (keywords.length > 0) {
|
|
777
|
+
const searchResults = searchFilesForKeywords(keywords, projectRoot, 15);
|
|
778
|
+
|
|
779
|
+
for (const result of searchResults) {
|
|
780
|
+
// Only add if not already in our list
|
|
781
|
+
if (!visited.has(result.path)) {
|
|
782
|
+
visited.add(result.path);
|
|
783
|
+
componentSources.push({ path: result.path, content: result.content });
|
|
784
|
+
debugLog("[edit] Added file from keyword search", { path: result.path, matchCount: result.matchCount });
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
771
790
|
// Read globals.css - check multiple possible locations
|
|
772
791
|
let globalsCSS = "";
|
|
773
792
|
const globalsCSSPatterns = [
|
|
@@ -978,19 +997,142 @@ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | nul
|
|
|
978
997
|
* Extract import paths from file content
|
|
979
998
|
*/
|
|
980
999
|
function extractImports(content: string): string[] {
|
|
981
|
-
// Match
|
|
982
|
-
|
|
1000
|
+
// Match both import AND export statements for @/ and relative paths
|
|
1001
|
+
// This catches barrel files (index.ts) that use: export { Foo } from './Foo'
|
|
1002
|
+
const importExportRegex = /(?:import|export)\s+.*?\s+from\s+["'](@\/[^"']+|\.\.?\/[^"']+)["']/g;
|
|
983
1003
|
const imports: string[] = [];
|
|
984
1004
|
let match;
|
|
985
1005
|
|
|
986
|
-
while ((match =
|
|
1006
|
+
while ((match = importExportRegex.exec(content)) !== null) {
|
|
987
1007
|
imports.push(match[1]);
|
|
988
1008
|
}
|
|
989
1009
|
|
|
990
|
-
debugLog("[edit] Extracted imports", { count: imports.length, imports: imports.slice(0, 10) });
|
|
1010
|
+
debugLog("[edit] Extracted imports/exports", { count: imports.length, imports: imports.slice(0, 10) });
|
|
991
1011
|
return imports;
|
|
992
1012
|
}
|
|
993
1013
|
|
|
1014
|
+
/**
|
|
1015
|
+
* Extract meaningful keywords from user prompt for file searching
|
|
1016
|
+
* Looks for quoted strings, capitalized phrases, and specific patterns
|
|
1017
|
+
*/
|
|
1018
|
+
function extractKeywordsFromPrompt(prompt: string): string[] {
|
|
1019
|
+
const keywords: string[] = [];
|
|
1020
|
+
|
|
1021
|
+
// 1. Extract quoted strings (e.g., "View Assets", 'Edit Process')
|
|
1022
|
+
const quotedRegex = /["']([^"']+)["']/g;
|
|
1023
|
+
let match;
|
|
1024
|
+
while ((match = quotedRegex.exec(prompt)) !== null) {
|
|
1025
|
+
if (match[1].length > 2 && match[1].length < 50) {
|
|
1026
|
+
keywords.push(match[1]);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// 2. Extract capitalized phrases (e.g., "View Assets", "Quick Actions")
|
|
1031
|
+
// Matches 2+ capitalized words together
|
|
1032
|
+
const capitalizedRegex = /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b/g;
|
|
1033
|
+
while ((match = capitalizedRegex.exec(prompt)) !== null) {
|
|
1034
|
+
if (!keywords.includes(match[1])) {
|
|
1035
|
+
keywords.push(match[1]);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// 3. Extract single capitalized words that might be component names
|
|
1040
|
+
const singleCapRegex = /\b([A-Z][a-z]{3,})\b/g;
|
|
1041
|
+
while ((match = singleCapRegex.exec(prompt)) !== null) {
|
|
1042
|
+
// Skip common words
|
|
1043
|
+
const skipWords = ["The", "This", "That", "Make", "Change", "Edit", "Update", "Add", "Remove", "Delete"];
|
|
1044
|
+
if (!skipWords.includes(match[1]) && !keywords.includes(match[1])) {
|
|
1045
|
+
keywords.push(match[1]);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
debugLog("[edit] Extracted keywords from prompt", { prompt: prompt.substring(0, 100), keywords });
|
|
1050
|
+
return keywords;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Recursively search files in a directory for keyword matches
|
|
1055
|
+
* Returns file paths that contain any of the keywords
|
|
1056
|
+
*/
|
|
1057
|
+
function searchFilesForKeywords(
|
|
1058
|
+
keywords: string[],
|
|
1059
|
+
projectRoot: string,
|
|
1060
|
+
maxFiles: number = 10
|
|
1061
|
+
): { path: string; content: string; matchCount: number }[] {
|
|
1062
|
+
if (keywords.length === 0) return [];
|
|
1063
|
+
|
|
1064
|
+
const results: { path: string; content: string; matchCount: number }[] = [];
|
|
1065
|
+
const searchDirs = ["components", "src/components", "app", "src/app", "pages", "src/pages"];
|
|
1066
|
+
const extensions = [".tsx", ".jsx", ".ts", ".js"];
|
|
1067
|
+
const visited = new Set<string>();
|
|
1068
|
+
|
|
1069
|
+
function searchDir(dirPath: string, depth: number = 0) {
|
|
1070
|
+
if (depth > 5 || results.length >= maxFiles) return;
|
|
1071
|
+
|
|
1072
|
+
const fullDirPath = path.join(projectRoot, dirPath);
|
|
1073
|
+
if (!fs.existsSync(fullDirPath) || visited.has(fullDirPath)) return;
|
|
1074
|
+
visited.add(fullDirPath);
|
|
1075
|
+
|
|
1076
|
+
try {
|
|
1077
|
+
const entries = fs.readdirSync(fullDirPath, { withFileTypes: true });
|
|
1078
|
+
|
|
1079
|
+
for (const entry of entries) {
|
|
1080
|
+
if (results.length >= maxFiles) break;
|
|
1081
|
+
|
|
1082
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
1083
|
+
const fullEntryPath = path.join(projectRoot, entryPath);
|
|
1084
|
+
|
|
1085
|
+
if (entry.isDirectory()) {
|
|
1086
|
+
// Skip node_modules, .git, etc.
|
|
1087
|
+
if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
1088
|
+
searchDir(entryPath, depth + 1);
|
|
1089
|
+
}
|
|
1090
|
+
} else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
1091
|
+
try {
|
|
1092
|
+
const content = fs.readFileSync(fullEntryPath, "utf-8");
|
|
1093
|
+
|
|
1094
|
+
// Count keyword matches
|
|
1095
|
+
let matchCount = 0;
|
|
1096
|
+
for (const keyword of keywords) {
|
|
1097
|
+
// Case-insensitive search
|
|
1098
|
+
const regex = new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
1099
|
+
const matches = content.match(regex);
|
|
1100
|
+
if (matches) {
|
|
1101
|
+
matchCount += matches.length;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (matchCount > 0) {
|
|
1106
|
+
results.push({ path: entryPath, content, matchCount });
|
|
1107
|
+
}
|
|
1108
|
+
} catch {
|
|
1109
|
+
// Skip files that can't be read
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
} catch {
|
|
1114
|
+
// Skip directories that can't be read
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Search each directory
|
|
1119
|
+
for (const dir of searchDirs) {
|
|
1120
|
+
searchDir(dir);
|
|
1121
|
+
if (results.length >= maxFiles) break;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Sort by match count (most matches first)
|
|
1125
|
+
results.sort((a, b) => b.matchCount - a.matchCount);
|
|
1126
|
+
|
|
1127
|
+
debugLog("[edit] Keyword search results", {
|
|
1128
|
+
keywords,
|
|
1129
|
+
filesFound: results.length,
|
|
1130
|
+
topMatches: results.slice(0, 5).map(r => ({ path: r.path, matchCount: r.matchCount }))
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
return results.slice(0, maxFiles);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
994
1136
|
// Cache for tsconfig path aliases
|
|
995
1137
|
let cachedPathAliases: Map<string, string> | null = null;
|
|
996
1138
|
let cachedProjectRoot: string | null = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.33",
|
|
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",
|