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)
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
): string
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
|
|
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
|
|
1117
|
-
|
|
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
|
|
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)
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
|
1015
|
-
|
|
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.
|
|
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",
|