sonance-brand-mcp 1.3.30 → 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"] = [];
@@ -746,17 +746,23 @@ function gatherAllImports(
746
746
  maxDepth: number = 4
747
747
  ): { path: string; content: string }[] {
748
748
  // Prevent infinite loops and limit total files
749
- if (visited.has(filePath) || visited.size > 50) return [];
749
+ if (visited.has(filePath) || visited.size > 50) {
750
+ return [];
751
+ }
750
752
  visited.add(filePath);
751
753
 
752
754
  const results: { path: string; content: string }[] = [];
753
755
  const fullPath = path.join(projectRoot, filePath);
754
756
 
755
- if (!fs.existsSync(fullPath)) return results;
757
+ if (!fs.existsSync(fullPath)) {
758
+ debugLog("[apply] gatherAllImports: file not found", { filePath, fullPath });
759
+ return results;
760
+ }
756
761
 
757
762
  try {
758
763
  const content = fs.readFileSync(fullPath, "utf-8");
759
764
  results.push({ path: filePath, content });
765
+ debugLog("[apply] gatherAllImports: added file", { filePath, contentLength: content.length });
760
766
 
761
767
  // Continue recursing if we haven't hit max depth
762
768
  if (maxDepth > 0) {
@@ -769,8 +775,8 @@ function gatherAllImports(
769
775
  }
770
776
  }
771
777
  }
772
- } catch {
773
- // Skip files that can't be read
778
+ } catch (e) {
779
+ debugLog("[apply] gatherAllImports: error reading file", { filePath, error: String(e) });
774
780
  }
775
781
 
776
782
  return results;
@@ -837,11 +843,12 @@ function discoverLayoutFiles(pageFile: string | null, projectRoot: string): stri
837
843
 
838
844
  /**
839
845
  * Gather context about the current page for AI analysis
840
- * Uses recursive import resolution to build complete component graph
846
+ * Uses recursive import resolution AND keyword search to build complete component graph
841
847
  */
842
848
  function gatherPageContext(
843
849
  pageRoute: string,
844
- projectRoot: string
850
+ projectRoot: string,
851
+ userPrompt?: string
845
852
  ): {
846
853
  pageFile: string | null;
847
854
  pageContent: string;
@@ -880,6 +887,24 @@ function gatherPageContext(
880
887
  }
881
888
  }
882
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
+
883
908
  let globalsCSS = "";
884
909
  const globalsCSSPatterns = [
885
910
  "src/app/globals.css",
@@ -1081,40 +1106,266 @@ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | nul
1081
1106
  }
1082
1107
 
1083
1108
  function extractImports(content: string): string[] {
1084
- const importRegex = /import\s+.*?\s+from\s+["'](@\/components\/[^"']+|\.\.?\/[^"']+)["']/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;
1085
1112
  const imports: string[] = [];
1086
1113
  let match;
1087
1114
 
1088
- while ((match = importRegex.exec(content)) !== null) {
1115
+ while ((match = importExportRegex.exec(content)) !== null) {
1089
1116
  imports.push(match[1]);
1090
1117
  }
1091
1118
 
1119
+ debugLog("[apply] Extracted imports/exports", { count: imports.length, imports: imports.slice(0, 10) });
1092
1120
  return imports;
1093
1121
  }
1094
1122
 
1095
- function resolveImportPath(
1096
- importPath: string,
1097
- fromFile: string,
1098
- projectRoot: string
1099
- ): string | null {
1100
- if (importPath.startsWith("@/")) {
1101
- const resolved = importPath.replace("@/", "src/");
1102
- const withExt = resolved.endsWith(".tsx") ? resolved : `${resolved}.tsx`;
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
+
1245
+ // Cache for tsconfig path aliases
1246
+ let cachedPathAliases: Map<string, string> | null = null;
1247
+ let cachedProjectRoot: string | null = null;
1248
+
1249
+ /**
1250
+ * Read and parse tsconfig.json to get path aliases
1251
+ */
1252
+ function getPathAliases(projectRoot: string): Map<string, string> {
1253
+ // Return cached if same project
1254
+ if (cachedPathAliases && cachedProjectRoot === projectRoot) {
1255
+ return cachedPathAliases;
1256
+ }
1257
+
1258
+ const aliases = new Map<string, string>();
1259
+
1260
+ // Try to read tsconfig.json
1261
+ const tsconfigPath = path.join(projectRoot, "tsconfig.json");
1262
+ if (fs.existsSync(tsconfigPath)) {
1263
+ try {
1264
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
1265
+ // Remove comments (tsconfig allows them)
1266
+ const cleanContent = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
1267
+ const tsconfig = JSON.parse(cleanContent);
1268
+
1269
+ const paths = tsconfig.compilerOptions?.paths || {};
1270
+ const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
1271
+
1272
+ // Parse path mappings
1273
+ for (const [alias, targets] of Object.entries(paths)) {
1274
+ if (Array.isArray(targets) && targets.length > 0) {
1275
+ // Convert @/* to @/ and src/* to src/
1276
+ const cleanAlias = alias.replace("/*", "/");
1277
+ const cleanTarget = (targets[0] as string).replace("/*", "/").replace("./", "");
1278
+ aliases.set(cleanAlias, cleanTarget);
1279
+ }
1280
+ }
1281
+
1282
+ debugLog("[apply] Loaded tsconfig path aliases", { aliases: Object.fromEntries(aliases) });
1283
+ } catch (e) {
1284
+ debugLog("[apply] Failed to parse tsconfig.json", { error: String(e) });
1285
+ }
1286
+ }
1287
+
1288
+ // If no @/ alias found, try common defaults
1289
+ if (!aliases.has("@/")) {
1290
+ // Check if src/ exists
1291
+ if (fs.existsSync(path.join(projectRoot, "src"))) {
1292
+ aliases.set("@/", "src/");
1293
+ } else if (fs.existsSync(path.join(projectRoot, "app"))) {
1294
+ // Next.js App Router without src/
1295
+ aliases.set("@/", "");
1296
+ } else {
1297
+ aliases.set("@/", "");
1298
+ }
1299
+ debugLog("[apply] Using default @/ alias", { alias: aliases.get("@/") });
1300
+ }
1301
+
1302
+ cachedPathAliases = aliases;
1303
+ cachedProjectRoot = projectRoot;
1304
+ return aliases;
1305
+ }
1306
+
1307
+ /**
1308
+ * Try multiple file extensions for an import
1309
+ */
1310
+ function tryFileExtensions(basePath: string, projectRoot: string): string | null {
1311
+ const extensions = [".tsx", ".ts", ".jsx", ".js"];
1312
+
1313
+ // First try exact path (if already has extension)
1314
+ if (extensions.some(ext => basePath.endsWith(ext))) {
1315
+ if (fs.existsSync(path.join(projectRoot, basePath))) {
1316
+ return basePath;
1317
+ }
1318
+ }
1319
+
1320
+ // Try each extension
1321
+ for (const ext of extensions) {
1322
+ const withExt = `${basePath}${ext}`;
1103
1323
  if (fs.existsSync(path.join(projectRoot, withExt))) {
1104
1324
  return withExt;
1105
1325
  }
1106
- const indexPath = `${resolved}/index.tsx`;
1326
+ }
1327
+
1328
+ // Try as directory with index file
1329
+ for (const ext of extensions) {
1330
+ const indexPath = `${basePath}/index${ext}`;
1107
1331
  if (fs.existsSync(path.join(projectRoot, indexPath))) {
1108
1332
  return indexPath;
1109
1333
  }
1110
- return withExt;
1111
1334
  }
1335
+
1336
+ return null;
1337
+ }
1112
1338
 
1339
+ function resolveImportPath(
1340
+ importPath: string,
1341
+ fromFile: string,
1342
+ projectRoot: string
1343
+ ): string | null {
1344
+ const aliases = getPathAliases(projectRoot);
1345
+
1346
+ // Handle aliased imports (e.g., @/, ~/, etc.)
1347
+ for (const [alias, target] of aliases) {
1348
+ if (importPath.startsWith(alias)) {
1349
+ const resolved = importPath.replace(alias, target);
1350
+ const result = tryFileExtensions(resolved, projectRoot);
1351
+ if (result) {
1352
+ debugLog("[apply] Resolved alias import", { importPath, resolved: result });
1353
+ return result;
1354
+ }
1355
+ // Return best guess even if file not found
1356
+ return `${resolved}.tsx`;
1357
+ }
1358
+ }
1359
+
1360
+ // Handle relative imports
1113
1361
  if (importPath.startsWith(".")) {
1114
1362
  const dir = path.dirname(fromFile);
1115
1363
  const resolved = path.join(dir, importPath);
1116
- const withExt = resolved.endsWith(".tsx") ? resolved : `${resolved}.tsx`;
1117
- return withExt;
1364
+ const result = tryFileExtensions(resolved, projectRoot);
1365
+ if (result) {
1366
+ return result;
1367
+ }
1368
+ return `${resolved}.tsx`;
1118
1369
  }
1119
1370
 
1120
1371
  return 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"] = [];
@@ -614,17 +614,23 @@ function gatherAllImports(
614
614
  maxDepth: number = 4
615
615
  ): { path: string; content: string }[] {
616
616
  // Prevent infinite loops and limit total files
617
- if (visited.has(filePath) || visited.size > 50) return [];
617
+ if (visited.has(filePath) || visited.size > 50) {
618
+ return [];
619
+ }
618
620
  visited.add(filePath);
619
621
 
620
622
  const results: { path: string; content: string }[] = [];
621
623
  const fullPath = path.join(projectRoot, filePath);
622
624
 
623
- if (!fs.existsSync(fullPath)) return results;
625
+ if (!fs.existsSync(fullPath)) {
626
+ debugLog("[edit] gatherAllImports: file not found", { filePath, fullPath });
627
+ return results;
628
+ }
624
629
 
625
630
  try {
626
631
  const content = fs.readFileSync(fullPath, "utf-8");
627
632
  results.push({ path: filePath, content });
633
+ debugLog("[edit] gatherAllImports: added file", { filePath, contentLength: content.length });
628
634
 
629
635
  // Continue recursing if we haven't hit max depth
630
636
  if (maxDepth > 0) {
@@ -637,8 +643,8 @@ function gatherAllImports(
637
643
  }
638
644
  }
639
645
  }
640
- } catch {
641
- // Skip files that can't be read
646
+ } catch (e) {
647
+ debugLog("[edit] gatherAllImports: error reading file", { filePath, error: String(e) });
642
648
  }
643
649
 
644
650
  return results;
@@ -705,12 +711,13 @@ function discoverLayoutFiles(pageFile: string | null, projectRoot: string): stri
705
711
 
706
712
  /**
707
713
  * Gather context about the current page for AI analysis
708
- * Uses recursive import resolution to build complete component graph
714
+ * Uses recursive import resolution AND keyword search to build complete component graph
709
715
  */
710
716
  function gatherPageContext(
711
717
  pageRoute: string,
712
718
  projectRoot: string,
713
- focusedElements?: VisionFocusedElement[]
719
+ focusedElements?: VisionFocusedElement[],
720
+ userPrompt?: string
714
721
  ): {
715
722
  pageFile: string | null;
716
723
  pageContent: string;
@@ -762,6 +769,24 @@ function gatherPageContext(
762
769
  }
763
770
  }
764
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
+
765
790
  // Read globals.css - check multiple possible locations
766
791
  let globalsCSS = "";
767
792
  const globalsCSSPatterns = [
@@ -972,17 +997,236 @@ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | nul
972
997
  * Extract import paths from file content
973
998
  */
974
999
  function extractImports(content: string): string[] {
975
- const importRegex = /import\s+.*?\s+from\s+["'](@\/components\/[^"']+|\.\.?\/[^"']+)["']/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;
976
1003
  const imports: string[] = [];
977
1004
  let match;
978
1005
 
979
- while ((match = importRegex.exec(content)) !== null) {
1006
+ while ((match = importExportRegex.exec(content)) !== null) {
980
1007
  imports.push(match[1]);
981
1008
  }
982
1009
 
1010
+ debugLog("[edit] Extracted imports/exports", { count: imports.length, imports: imports.slice(0, 10) });
983
1011
  return imports;
984
1012
  }
985
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
+
1136
+ // Cache for tsconfig path aliases
1137
+ let cachedPathAliases: Map<string, string> | null = null;
1138
+ let cachedProjectRoot: string | null = null;
1139
+
1140
+ /**
1141
+ * Read and parse tsconfig.json to get path aliases
1142
+ */
1143
+ function getPathAliases(projectRoot: string): Map<string, string> {
1144
+ // Return cached if same project
1145
+ if (cachedPathAliases && cachedProjectRoot === projectRoot) {
1146
+ return cachedPathAliases;
1147
+ }
1148
+
1149
+ const aliases = new Map<string, string>();
1150
+
1151
+ // Try to read tsconfig.json
1152
+ const tsconfigPath = path.join(projectRoot, "tsconfig.json");
1153
+ if (fs.existsSync(tsconfigPath)) {
1154
+ try {
1155
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
1156
+ // Remove comments (tsconfig allows them)
1157
+ const cleanContent = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
1158
+ const tsconfig = JSON.parse(cleanContent);
1159
+
1160
+ const paths = tsconfig.compilerOptions?.paths || {};
1161
+ const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
1162
+
1163
+ // Parse path mappings
1164
+ for (const [alias, targets] of Object.entries(paths)) {
1165
+ if (Array.isArray(targets) && targets.length > 0) {
1166
+ // Convert @/* to @/ and src/* to src/
1167
+ const cleanAlias = alias.replace("/*", "/");
1168
+ const cleanTarget = (targets[0] as string).replace("/*", "/").replace("./", "");
1169
+ aliases.set(cleanAlias, cleanTarget);
1170
+ }
1171
+ }
1172
+
1173
+ debugLog("[edit] Loaded tsconfig path aliases", { aliases: Object.fromEntries(aliases) });
1174
+ } catch (e) {
1175
+ debugLog("[edit] Failed to parse tsconfig.json", { error: String(e) });
1176
+ }
1177
+ }
1178
+
1179
+ // If no @/ alias found, try common defaults
1180
+ if (!aliases.has("@/")) {
1181
+ // Check if src/ exists
1182
+ if (fs.existsSync(path.join(projectRoot, "src"))) {
1183
+ aliases.set("@/", "src/");
1184
+ } else if (fs.existsSync(path.join(projectRoot, "app"))) {
1185
+ // Next.js App Router without src/
1186
+ aliases.set("@/", "");
1187
+ } else {
1188
+ aliases.set("@/", "");
1189
+ }
1190
+ debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
1191
+ }
1192
+
1193
+ cachedPathAliases = aliases;
1194
+ cachedProjectRoot = projectRoot;
1195
+ return aliases;
1196
+ }
1197
+
1198
+ /**
1199
+ * Try multiple file extensions for an import
1200
+ */
1201
+ function tryFileExtensions(basePath: string, projectRoot: string): string | null {
1202
+ const extensions = [".tsx", ".ts", ".jsx", ".js"];
1203
+
1204
+ // First try exact path (if already has extension)
1205
+ if (extensions.some(ext => basePath.endsWith(ext))) {
1206
+ if (fs.existsSync(path.join(projectRoot, basePath))) {
1207
+ return basePath;
1208
+ }
1209
+ }
1210
+
1211
+ // Try each extension
1212
+ for (const ext of extensions) {
1213
+ const withExt = `${basePath}${ext}`;
1214
+ if (fs.existsSync(path.join(projectRoot, withExt))) {
1215
+ return withExt;
1216
+ }
1217
+ }
1218
+
1219
+ // Try as directory with index file
1220
+ for (const ext of extensions) {
1221
+ const indexPath = `${basePath}/index${ext}`;
1222
+ if (fs.existsSync(path.join(projectRoot, indexPath))) {
1223
+ return indexPath;
1224
+ }
1225
+ }
1226
+
1227
+ return null;
1228
+ }
1229
+
986
1230
  /**
987
1231
  * Resolve an import path to a file system path
988
1232
  */
@@ -991,28 +1235,31 @@ function resolveImportPath(
991
1235
  fromFile: string,
992
1236
  projectRoot: string
993
1237
  ): string | null {
994
- // Handle @ alias (maps to src/)
995
- if (importPath.startsWith("@/")) {
996
- const resolved = importPath.replace("@/", "src/");
997
- // Try with .tsx extension
998
- const withExt = resolved.endsWith(".tsx") ? resolved : `${resolved}.tsx`;
999
- if (fs.existsSync(path.join(projectRoot, withExt))) {
1000
- return withExt;
1001
- }
1002
- // Try as directory with index
1003
- const indexPath = `${resolved}/index.tsx`;
1004
- if (fs.existsSync(path.join(projectRoot, indexPath))) {
1005
- return indexPath;
1238
+ const aliases = getPathAliases(projectRoot);
1239
+
1240
+ // Handle aliased imports (e.g., @/, ~/, etc.)
1241
+ for (const [alias, target] of aliases) {
1242
+ if (importPath.startsWith(alias)) {
1243
+ const resolved = importPath.replace(alias, target);
1244
+ const result = tryFileExtensions(resolved, projectRoot);
1245
+ if (result) {
1246
+ debugLog("[edit] Resolved alias import", { importPath, resolved: result });
1247
+ return result;
1248
+ }
1249
+ // Return best guess even if file not found
1250
+ return `${resolved}.tsx`;
1006
1251
  }
1007
- return withExt; // Return even if not found for context
1008
1252
  }
1009
1253
 
1010
1254
  // Handle relative imports
1011
1255
  if (importPath.startsWith(".")) {
1012
1256
  const dir = path.dirname(fromFile);
1013
1257
  const resolved = path.join(dir, importPath);
1014
- const withExt = resolved.endsWith(".tsx") ? resolved : `${resolved}.tsx`;
1015
- return withExt;
1258
+ const result = tryFileExtensions(resolved, projectRoot);
1259
+ if (result) {
1260
+ return result;
1261
+ }
1262
+ return `${resolved}.tsx`;
1016
1263
  }
1017
1264
 
1018
1265
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.30",
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",