sonance-brand-mcp 1.3.31 → 1.3.32

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 ALL @/ imports (not just @/components) and relative imports
1091
- const importRegex = /import\s+.*?\s+from\s+["'](@\/[^"']+|\.\.?\/[^"']+)["']/g;
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 = importRegex.exec(content)) !== null) {
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 via project-wide search)
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 ALL @/ imports (not just @/components) and relative imports
982
- const importRegex = /import\s+.*?\s+from\s+["'](@\/[^"']+|\.\.?\/[^"']+)["']/g;
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 = importRegex.exec(content)) !== null) {
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.31",
3
+ "version": "1.3.32",
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",