sonance-brand-mcp 1.3.87 → 1.3.89
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.
|
@@ -229,6 +229,109 @@ function findElementIdInFile(
|
|
|
229
229
|
return null;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/**
|
|
233
|
+
* PHASE 0: Deterministic Element ID Search (Cursor-style)
|
|
234
|
+
* Grep entire codebase for the element ID. If found in multiple files,
|
|
235
|
+
* use the current route to disambiguate (prioritize page file for route).
|
|
236
|
+
*
|
|
237
|
+
* This is the most reliable signal - like Cursor knowing which file you're in.
|
|
238
|
+
* Element IDs should be unique, so if we find one, that's THE file.
|
|
239
|
+
*/
|
|
240
|
+
function findFilesByElementId(
|
|
241
|
+
projectRoot: string,
|
|
242
|
+
elementId: string,
|
|
243
|
+
currentRoute: string,
|
|
244
|
+
discoverPageFileFn: (route: string, projectRoot: string) => string | null
|
|
245
|
+
): { path: string; lineNumber: number; isRouteMatch: boolean }[] {
|
|
246
|
+
const pattern = new RegExp(`id=["'\`]${elementId}["'\`]`);
|
|
247
|
+
const matches: { path: string; lineNumber: number; isRouteMatch: boolean }[] = [];
|
|
248
|
+
|
|
249
|
+
// Determine expected page file from route
|
|
250
|
+
const routePageFile = discoverPageFileFn(currentRoute, projectRoot);
|
|
251
|
+
|
|
252
|
+
// Search all common project directories that exist
|
|
253
|
+
// This supports: src/app, app/, pages/, components/, lib/ structures
|
|
254
|
+
const commonDirs = ['src', 'app', 'pages', 'components', 'lib'];
|
|
255
|
+
const searchDirs = commonDirs
|
|
256
|
+
.map(dir => path.join(projectRoot, dir))
|
|
257
|
+
.filter(dir => {
|
|
258
|
+
try {
|
|
259
|
+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// If no standard dirs found, search project root (excluding node_modules)
|
|
266
|
+
if (searchDirs.length === 0) {
|
|
267
|
+
searchDirs.push(projectRoot);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
debugLog("PHASE 0: Searching for element ID", {
|
|
271
|
+
elementId,
|
|
272
|
+
currentRoute,
|
|
273
|
+
routePageFile,
|
|
274
|
+
searchDirs: searchDirs.map(d => d.replace(projectRoot + '/', ''))
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
function searchDirRecursive(dir: string): void {
|
|
278
|
+
try {
|
|
279
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
280
|
+
for (const entry of entries) {
|
|
281
|
+
const fullPath = path.join(dir, entry.name);
|
|
282
|
+
|
|
283
|
+
// Skip node_modules, hidden directories, and build outputs
|
|
284
|
+
if (entry.isDirectory()) {
|
|
285
|
+
if (
|
|
286
|
+
entry.name.includes('node_modules') ||
|
|
287
|
+
entry.name.startsWith('.') ||
|
|
288
|
+
entry.name === 'dist' ||
|
|
289
|
+
entry.name === 'build' ||
|
|
290
|
+
entry.name === '.next'
|
|
291
|
+
) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
searchDirRecursive(fullPath);
|
|
295
|
+
} else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
296
|
+
try {
|
|
297
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
298
|
+
const lines = content.split('\n');
|
|
299
|
+
for (let i = 0; i < lines.length; i++) {
|
|
300
|
+
if (pattern.test(lines[i])) {
|
|
301
|
+
const relativePath = fullPath.replace(projectRoot + '/', '');
|
|
302
|
+
const isRouteMatch = relativePath === routePageFile;
|
|
303
|
+
matches.push({
|
|
304
|
+
path: relativePath,
|
|
305
|
+
lineNumber: i + 1,
|
|
306
|
+
isRouteMatch
|
|
307
|
+
});
|
|
308
|
+
debugLog("PHASE 0: Found ID match", {
|
|
309
|
+
file: relativePath,
|
|
310
|
+
line: i + 1,
|
|
311
|
+
isRouteMatch
|
|
312
|
+
});
|
|
313
|
+
break; // One match per file is enough
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
// Skip files that can't be read
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
// Skip directories that can't be read
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Search all directories
|
|
327
|
+
for (const searchDir of searchDirs) {
|
|
328
|
+
searchDirRecursive(searchDir);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Sort: route matches first
|
|
332
|
+
return matches.sort((a, b) => (b.isRouteMatch ? 1 : 0) - (a.isRouteMatch ? 1 : 0));
|
|
333
|
+
}
|
|
334
|
+
|
|
232
335
|
/**
|
|
233
336
|
* Result of LLM screenshot analysis for smart file discovery
|
|
234
337
|
*/
|
|
@@ -927,12 +1030,73 @@ export async function POST(request: Request) {
|
|
|
927
1030
|
// Generate a unique session ID
|
|
928
1031
|
const newSessionId = randomUUID().slice(0, 8);
|
|
929
1032
|
|
|
930
|
-
//
|
|
931
|
-
// The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
|
|
1033
|
+
// Initialize file discovery variables
|
|
932
1034
|
let smartSearchFiles: { path: string; content: string }[] = [];
|
|
933
1035
|
let recommendedFile: { path: string; reason: string } | null = null;
|
|
934
|
-
|
|
935
|
-
|
|
1036
|
+
let deterministicMatch: { path: string; lineNumber: number } | null = null;
|
|
1037
|
+
|
|
1038
|
+
// ========================================================================
|
|
1039
|
+
// PHASE 0: Deterministic Element ID Search (Cursor-style explicit selection)
|
|
1040
|
+
// If we have an element ID, find it directly - no scoring, no heuristics
|
|
1041
|
+
// This is the most reliable signal, like Cursor knowing which file you're in
|
|
1042
|
+
// ========================================================================
|
|
1043
|
+
if (focusedElements?.some(el => el.elementId)) {
|
|
1044
|
+
const elementWithId = focusedElements.find(el => el.elementId)!;
|
|
1045
|
+
debugLog("PHASE 0: Element has ID, starting deterministic search", {
|
|
1046
|
+
elementId: elementWithId.elementId,
|
|
1047
|
+
route: pageRoute
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
const matches = findFilesByElementId(
|
|
1051
|
+
projectRoot,
|
|
1052
|
+
elementWithId.elementId!,
|
|
1053
|
+
pageRoute || "/",
|
|
1054
|
+
discoverPageFile
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
if (matches.length === 1) {
|
|
1058
|
+
// Single match - 100% confidence
|
|
1059
|
+
deterministicMatch = matches[0];
|
|
1060
|
+
recommendedFile = {
|
|
1061
|
+
path: matches[0].path,
|
|
1062
|
+
reason: `Deterministic ID match: id="${elementWithId.elementId}" (unique in codebase)`
|
|
1063
|
+
};
|
|
1064
|
+
debugLog("PHASE 0 SUCCESS: Single ID match - skipping smart search", deterministicMatch);
|
|
1065
|
+
} else if (matches.length > 1) {
|
|
1066
|
+
// Multiple matches - use route to disambiguate
|
|
1067
|
+
const routeMatch = matches.find(m => m.isRouteMatch);
|
|
1068
|
+
if (routeMatch) {
|
|
1069
|
+
deterministicMatch = routeMatch;
|
|
1070
|
+
recommendedFile = {
|
|
1071
|
+
path: routeMatch.path,
|
|
1072
|
+
reason: `Deterministic ID match: id="${elementWithId.elementId}" (route "${pageRoute}" disambiguated from ${matches.length} files)`
|
|
1073
|
+
};
|
|
1074
|
+
debugLog("PHASE 0 SUCCESS: ID found in multiple files, using route match", {
|
|
1075
|
+
match: deterministicMatch,
|
|
1076
|
+
otherFiles: matches.filter(m => !m.isRouteMatch).map(m => m.path)
|
|
1077
|
+
});
|
|
1078
|
+
} else {
|
|
1079
|
+
debugLog("PHASE 0 FALLBACK: ID found in multiple files, no route match", {
|
|
1080
|
+
elementId: elementWithId.elementId,
|
|
1081
|
+
matchCount: matches.length,
|
|
1082
|
+
files: matches.map(m => m.path),
|
|
1083
|
+
route: pageRoute
|
|
1084
|
+
});
|
|
1085
|
+
// Fall through to smart search
|
|
1086
|
+
}
|
|
1087
|
+
} else {
|
|
1088
|
+
debugLog("PHASE 0 FALLBACK: Element ID not found in codebase", {
|
|
1089
|
+
elementId: elementWithId.elementId
|
|
1090
|
+
});
|
|
1091
|
+
// Fall through to smart search
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// ========================================================================
|
|
1096
|
+
// PHASE 1+2: LLM-driven smart file discovery (only if Phase 0 didn't match)
|
|
1097
|
+
// The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
|
|
1098
|
+
// ========================================================================
|
|
1099
|
+
if (!deterministicMatch && screenshot) {
|
|
936
1100
|
debugLog("Starting Phase 1: LLM screenshot analysis");
|
|
937
1101
|
const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
|
|
938
1102
|
|
|
@@ -981,8 +1145,55 @@ export async function POST(request: Request) {
|
|
|
981
1145
|
const phase2aConfirmed = phase2aMatches.length > 0 &&
|
|
982
1146
|
focusedElementHints.some(h => phase2aMatches.includes(h.path) && h.score > 0);
|
|
983
1147
|
|
|
1148
|
+
// Helper: Find best Phase 2a match based on user prompt keywords
|
|
1149
|
+
// e.g., "duplicate button" should prefer ProcessRow.tsx over ProcessCatalogueView.tsx
|
|
1150
|
+
const findBestPhase2aMatch = (): string | null => {
|
|
1151
|
+
if (phase2aMatches.length === 0) return null;
|
|
1152
|
+
if (phase2aMatches.length === 1) return phase2aMatches[0];
|
|
1153
|
+
|
|
1154
|
+
// Extract keywords from user prompt
|
|
1155
|
+
const promptLower = userPrompt.toLowerCase();
|
|
1156
|
+
const keywords = ['row', 'cell', 'item', 'button', 'action', 'duplicate', 'edit', 'delete'];
|
|
1157
|
+
|
|
1158
|
+
// Prefer more specific files (Row > View, Cell > Table, etc.)
|
|
1159
|
+
const specificity = ['row', 'cell', 'item', 'card', 'modal', 'panel', 'detail'];
|
|
1160
|
+
|
|
1161
|
+
// Score each Phase 2a match
|
|
1162
|
+
let bestMatch = phase2aMatches[0];
|
|
1163
|
+
let bestScore = 0;
|
|
1164
|
+
|
|
1165
|
+
for (const match of phase2aMatches) {
|
|
1166
|
+
const matchLower = match.toLowerCase();
|
|
1167
|
+
let score = 0;
|
|
1168
|
+
|
|
1169
|
+
// Bonus for specificity (Row/Cell/Item components)
|
|
1170
|
+
for (const spec of specificity) {
|
|
1171
|
+
if (matchLower.includes(spec)) score += 10;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Bonus for keyword match (prompt mentions button → prefer file with button/action)
|
|
1175
|
+
for (const kw of keywords) {
|
|
1176
|
+
if (promptLower.includes(kw) && matchLower.includes(kw)) score += 20;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Penalty for generic view/container files
|
|
1180
|
+
if (matchLower.includes('view.tsx') && !matchLower.includes('detail')) score -= 5;
|
|
1181
|
+
|
|
1182
|
+
if (score > bestScore) {
|
|
1183
|
+
bestScore = score;
|
|
1184
|
+
bestMatch = match;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return bestMatch;
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
// ====================================================================
|
|
1192
|
+
// FALLBACK PRIORITY LOGIC (only reached when Phase 0 didn't find a match)
|
|
1193
|
+
// This handles cases: no element ID, dynamic IDs, or ambiguous ID matches
|
|
1194
|
+
// ====================================================================
|
|
984
1195
|
if (phase2aConfirmed) {
|
|
985
|
-
// PRIORITY 1: Phase 2a match confirmed by element search
|
|
1196
|
+
// FALLBACK PRIORITY 1: Phase 2a match confirmed by element search
|
|
986
1197
|
const confirmedPath = phase2aMatches.find(p =>
|
|
987
1198
|
focusedElementHints.some(h => h.path === p && h.score > 0)
|
|
988
1199
|
);
|
|
@@ -990,21 +1201,47 @@ export async function POST(request: Request) {
|
|
|
990
1201
|
path: confirmedPath!,
|
|
991
1202
|
reason: `Phase 2a component-name match confirmed by element search`
|
|
992
1203
|
};
|
|
993
|
-
debugLog("PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
|
|
1204
|
+
debugLog("FALLBACK PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
|
|
994
1205
|
} else if (focusedTopScore > smartSearchTopScore * 0.5 && focusedTopScore >= 400) {
|
|
995
|
-
// PRIORITY 2: Focused element has strong
|
|
1206
|
+
// FALLBACK PRIORITY 2: Focused element has strong score
|
|
996
1207
|
recommendedFile = {
|
|
997
1208
|
path: focusedTopPath!,
|
|
998
1209
|
reason: `Focused element match (score: ${focusedTopScore}, exceeds threshold)`
|
|
999
1210
|
};
|
|
1000
|
-
debugLog("PRIORITY 2: Strong focused element match", recommendedFile);
|
|
1001
|
-
} else if (
|
|
1002
|
-
// PRIORITY 3:
|
|
1211
|
+
debugLog("FALLBACK PRIORITY 2: Strong focused element match", recommendedFile);
|
|
1212
|
+
} else if (phase2aMatches.length > 0) {
|
|
1213
|
+
// FALLBACK PRIORITY 3: Phase 2a match WITHOUT focused element
|
|
1214
|
+
// Use prompt-aware selection to pick the best match
|
|
1215
|
+
const bestMatch = findBestPhase2aMatch()!;
|
|
1003
1216
|
recommendedFile = {
|
|
1004
|
-
path:
|
|
1005
|
-
reason: `
|
|
1217
|
+
path: bestMatch,
|
|
1218
|
+
reason: `Phase 2a component-name match (prompt-aware selection from ${phase2aMatches.length} candidates)`
|
|
1006
1219
|
};
|
|
1007
|
-
debugLog("PRIORITY 3:
|
|
1220
|
+
debugLog("FALLBACK PRIORITY 3: Phase 2a match (no focused element)", {
|
|
1221
|
+
selectedPath: bestMatch,
|
|
1222
|
+
allCandidates: phase2aMatches
|
|
1223
|
+
});
|
|
1224
|
+
} else {
|
|
1225
|
+
// FALLBACK PRIORITY 4: Use the page file from the current route
|
|
1226
|
+
const routePageFile = discoverPageFile(pageRoute || "/", projectRoot);
|
|
1227
|
+
|
|
1228
|
+
if (routePageFile && fs.existsSync(path.join(projectRoot, routePageFile))) {
|
|
1229
|
+
recommendedFile = {
|
|
1230
|
+
path: routePageFile,
|
|
1231
|
+
reason: `Page file from route "${pageRoute || "/"}"`
|
|
1232
|
+
};
|
|
1233
|
+
debugLog("FALLBACK PRIORITY 4: Using page file from route", {
|
|
1234
|
+
route: pageRoute || "/",
|
|
1235
|
+
pageFile: routePageFile
|
|
1236
|
+
});
|
|
1237
|
+
} else if (smartSearchTopPath) {
|
|
1238
|
+
// FALLBACK PRIORITY 5: Smart search top result - last resort
|
|
1239
|
+
recommendedFile = {
|
|
1240
|
+
path: smartSearchTopPath,
|
|
1241
|
+
reason: `Smart search top result (score: ${smartSearchTopScore})`
|
|
1242
|
+
};
|
|
1243
|
+
debugLog("FALLBACK PRIORITY 5: Using smart search top result", recommendedFile);
|
|
1244
|
+
}
|
|
1008
1245
|
}
|
|
1009
1246
|
|
|
1010
1247
|
debugLog("Phase 1+2 complete", {
|
|
@@ -225,6 +225,109 @@ function findElementIdInFile(
|
|
|
225
225
|
return null;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
/**
|
|
229
|
+
* PHASE 0: Deterministic Element ID Search (Cursor-style)
|
|
230
|
+
* Grep entire codebase for the element ID. If found in multiple files,
|
|
231
|
+
* use the current route to disambiguate (prioritize page file for route).
|
|
232
|
+
*
|
|
233
|
+
* This is the most reliable signal - like Cursor knowing which file you're in.
|
|
234
|
+
* Element IDs should be unique, so if we find one, that's THE file.
|
|
235
|
+
*/
|
|
236
|
+
function findFilesByElementId(
|
|
237
|
+
projectRoot: string,
|
|
238
|
+
elementId: string,
|
|
239
|
+
currentRoute: string,
|
|
240
|
+
discoverPageFileFn: (route: string, projectRoot: string) => string | null
|
|
241
|
+
): { path: string; lineNumber: number; isRouteMatch: boolean }[] {
|
|
242
|
+
const pattern = new RegExp(`id=["'\`]${elementId}["'\`]`);
|
|
243
|
+
const matches: { path: string; lineNumber: number; isRouteMatch: boolean }[] = [];
|
|
244
|
+
|
|
245
|
+
// Determine expected page file from route
|
|
246
|
+
const routePageFile = discoverPageFileFn(currentRoute, projectRoot);
|
|
247
|
+
|
|
248
|
+
// Search all common project directories that exist
|
|
249
|
+
// This supports: src/app, app/, pages/, components/, lib/ structures
|
|
250
|
+
const commonDirs = ['src', 'app', 'pages', 'components', 'lib'];
|
|
251
|
+
const searchDirs = commonDirs
|
|
252
|
+
.map(dir => path.join(projectRoot, dir))
|
|
253
|
+
.filter(dir => {
|
|
254
|
+
try {
|
|
255
|
+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// If no standard dirs found, search project root (excluding node_modules)
|
|
262
|
+
if (searchDirs.length === 0) {
|
|
263
|
+
searchDirs.push(projectRoot);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
debugLog("PHASE 0: Searching for element ID", {
|
|
267
|
+
elementId,
|
|
268
|
+
currentRoute,
|
|
269
|
+
routePageFile,
|
|
270
|
+
searchDirs: searchDirs.map(d => d.replace(projectRoot + '/', ''))
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
function searchDirRecursive(dir: string): void {
|
|
274
|
+
try {
|
|
275
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
276
|
+
for (const entry of entries) {
|
|
277
|
+
const fullPath = path.join(dir, entry.name);
|
|
278
|
+
|
|
279
|
+
// Skip node_modules, hidden directories, and build outputs
|
|
280
|
+
if (entry.isDirectory()) {
|
|
281
|
+
if (
|
|
282
|
+
entry.name.includes('node_modules') ||
|
|
283
|
+
entry.name.startsWith('.') ||
|
|
284
|
+
entry.name === 'dist' ||
|
|
285
|
+
entry.name === 'build' ||
|
|
286
|
+
entry.name === '.next'
|
|
287
|
+
) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
searchDirRecursive(fullPath);
|
|
291
|
+
} else if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
292
|
+
try {
|
|
293
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
294
|
+
const lines = content.split('\n');
|
|
295
|
+
for (let i = 0; i < lines.length; i++) {
|
|
296
|
+
if (pattern.test(lines[i])) {
|
|
297
|
+
const relativePath = fullPath.replace(projectRoot + '/', '');
|
|
298
|
+
const isRouteMatch = relativePath === routePageFile;
|
|
299
|
+
matches.push({
|
|
300
|
+
path: relativePath,
|
|
301
|
+
lineNumber: i + 1,
|
|
302
|
+
isRouteMatch
|
|
303
|
+
});
|
|
304
|
+
debugLog("PHASE 0: Found ID match", {
|
|
305
|
+
file: relativePath,
|
|
306
|
+
line: i + 1,
|
|
307
|
+
isRouteMatch
|
|
308
|
+
});
|
|
309
|
+
break; // One match per file is enough
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
// Skip files that can't be read
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
// Skip directories that can't be read
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Search all directories
|
|
323
|
+
for (const searchDir of searchDirs) {
|
|
324
|
+
searchDirRecursive(searchDir);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Sort: route matches first
|
|
328
|
+
return matches.sort((a, b) => (b.isRouteMatch ? 1 : 0) - (a.isRouteMatch ? 1 : 0));
|
|
329
|
+
}
|
|
330
|
+
|
|
228
331
|
/**
|
|
229
332
|
* Result of LLM screenshot analysis for smart file discovery
|
|
230
333
|
*/
|
|
@@ -896,12 +999,73 @@ export async function POST(request: Request) {
|
|
|
896
999
|
);
|
|
897
1000
|
}
|
|
898
1001
|
|
|
899
|
-
//
|
|
900
|
-
// The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
|
|
1002
|
+
// Initialize file discovery variables
|
|
901
1003
|
let smartSearchFiles: { path: string; content: string }[] = [];
|
|
902
1004
|
let recommendedFile: { path: string; reason: string } | null = null;
|
|
903
|
-
|
|
904
|
-
|
|
1005
|
+
let deterministicMatch: { path: string; lineNumber: number } | null = null;
|
|
1006
|
+
|
|
1007
|
+
// ========================================================================
|
|
1008
|
+
// PHASE 0: Deterministic Element ID Search (Cursor-style explicit selection)
|
|
1009
|
+
// If we have an element ID, find it directly - no scoring, no heuristics
|
|
1010
|
+
// This is the most reliable signal, like Cursor knowing which file you're in
|
|
1011
|
+
// ========================================================================
|
|
1012
|
+
if (focusedElements?.some(el => el.elementId)) {
|
|
1013
|
+
const elementWithId = focusedElements.find(el => el.elementId)!;
|
|
1014
|
+
debugLog("PHASE 0: Element has ID, starting deterministic search", {
|
|
1015
|
+
elementId: elementWithId.elementId,
|
|
1016
|
+
route: pageRoute
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
const matches = findFilesByElementId(
|
|
1020
|
+
projectRoot,
|
|
1021
|
+
elementWithId.elementId!,
|
|
1022
|
+
pageRoute || "/",
|
|
1023
|
+
discoverPageFile
|
|
1024
|
+
);
|
|
1025
|
+
|
|
1026
|
+
if (matches.length === 1) {
|
|
1027
|
+
// Single match - 100% confidence
|
|
1028
|
+
deterministicMatch = matches[0];
|
|
1029
|
+
recommendedFile = {
|
|
1030
|
+
path: matches[0].path,
|
|
1031
|
+
reason: `Deterministic ID match: id="${elementWithId.elementId}" (unique in codebase)`
|
|
1032
|
+
};
|
|
1033
|
+
debugLog("PHASE 0 SUCCESS: Single ID match - skipping smart search", deterministicMatch);
|
|
1034
|
+
} else if (matches.length > 1) {
|
|
1035
|
+
// Multiple matches - use route to disambiguate
|
|
1036
|
+
const routeMatch = matches.find(m => m.isRouteMatch);
|
|
1037
|
+
if (routeMatch) {
|
|
1038
|
+
deterministicMatch = routeMatch;
|
|
1039
|
+
recommendedFile = {
|
|
1040
|
+
path: routeMatch.path,
|
|
1041
|
+
reason: `Deterministic ID match: id="${elementWithId.elementId}" (route "${pageRoute}" disambiguated from ${matches.length} files)`
|
|
1042
|
+
};
|
|
1043
|
+
debugLog("PHASE 0 SUCCESS: ID found in multiple files, using route match", {
|
|
1044
|
+
match: deterministicMatch,
|
|
1045
|
+
otherFiles: matches.filter(m => !m.isRouteMatch).map(m => m.path)
|
|
1046
|
+
});
|
|
1047
|
+
} else {
|
|
1048
|
+
debugLog("PHASE 0 FALLBACK: ID found in multiple files, no route match", {
|
|
1049
|
+
elementId: elementWithId.elementId,
|
|
1050
|
+
matchCount: matches.length,
|
|
1051
|
+
files: matches.map(m => m.path),
|
|
1052
|
+
route: pageRoute
|
|
1053
|
+
});
|
|
1054
|
+
// Fall through to smart search
|
|
1055
|
+
}
|
|
1056
|
+
} else {
|
|
1057
|
+
debugLog("PHASE 0 FALLBACK: Element ID not found in codebase", {
|
|
1058
|
+
elementId: elementWithId.elementId
|
|
1059
|
+
});
|
|
1060
|
+
// Fall through to smart search
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// ========================================================================
|
|
1065
|
+
// PHASE 1+2: LLM-driven smart file discovery (only if Phase 0 didn't match)
|
|
1066
|
+
// The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
|
|
1067
|
+
// ========================================================================
|
|
1068
|
+
if (!deterministicMatch && screenshot) {
|
|
905
1069
|
debugLog("Starting Phase 1: LLM screenshot analysis");
|
|
906
1070
|
const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
|
|
907
1071
|
|
|
@@ -950,8 +1114,55 @@ export async function POST(request: Request) {
|
|
|
950
1114
|
const phase2aConfirmed = phase2aMatches.length > 0 &&
|
|
951
1115
|
focusedElementHints.some(h => phase2aMatches.includes(h.path) && h.score > 0);
|
|
952
1116
|
|
|
1117
|
+
// Helper: Find best Phase 2a match based on user prompt keywords
|
|
1118
|
+
// e.g., "duplicate button" should prefer ProcessRow.tsx over ProcessCatalogueView.tsx
|
|
1119
|
+
const findBestPhase2aMatch = (): string | null => {
|
|
1120
|
+
if (phase2aMatches.length === 0) return null;
|
|
1121
|
+
if (phase2aMatches.length === 1) return phase2aMatches[0];
|
|
1122
|
+
|
|
1123
|
+
// Extract keywords from user prompt
|
|
1124
|
+
const promptLower = userPrompt.toLowerCase();
|
|
1125
|
+
const keywords = ['row', 'cell', 'item', 'button', 'action', 'duplicate', 'edit', 'delete'];
|
|
1126
|
+
|
|
1127
|
+
// Prefer more specific files (Row > View, Cell > Table, etc.)
|
|
1128
|
+
const specificity = ['row', 'cell', 'item', 'card', 'modal', 'panel', 'detail'];
|
|
1129
|
+
|
|
1130
|
+
// Score each Phase 2a match
|
|
1131
|
+
let bestMatch = phase2aMatches[0];
|
|
1132
|
+
let bestScore = 0;
|
|
1133
|
+
|
|
1134
|
+
for (const match of phase2aMatches) {
|
|
1135
|
+
const matchLower = match.toLowerCase();
|
|
1136
|
+
let score = 0;
|
|
1137
|
+
|
|
1138
|
+
// Bonus for specificity (Row/Cell/Item components)
|
|
1139
|
+
for (const spec of specificity) {
|
|
1140
|
+
if (matchLower.includes(spec)) score += 10;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Bonus for keyword match (prompt mentions button → prefer file with button/action)
|
|
1144
|
+
for (const kw of keywords) {
|
|
1145
|
+
if (promptLower.includes(kw) && matchLower.includes(kw)) score += 20;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Penalty for generic view/container files
|
|
1149
|
+
if (matchLower.includes('view.tsx') && !matchLower.includes('detail')) score -= 5;
|
|
1150
|
+
|
|
1151
|
+
if (score > bestScore) {
|
|
1152
|
+
bestScore = score;
|
|
1153
|
+
bestMatch = match;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return bestMatch;
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
// ====================================================================
|
|
1161
|
+
// FALLBACK PRIORITY LOGIC (only reached when Phase 0 didn't find a match)
|
|
1162
|
+
// This handles cases: no element ID, dynamic IDs, or ambiguous ID matches
|
|
1163
|
+
// ====================================================================
|
|
953
1164
|
if (phase2aConfirmed) {
|
|
954
|
-
// PRIORITY 1: Phase 2a match confirmed by element search
|
|
1165
|
+
// FALLBACK PRIORITY 1: Phase 2a match confirmed by element search
|
|
955
1166
|
const confirmedPath = phase2aMatches.find(p =>
|
|
956
1167
|
focusedElementHints.some(h => h.path === p && h.score > 0)
|
|
957
1168
|
);
|
|
@@ -959,21 +1170,47 @@ export async function POST(request: Request) {
|
|
|
959
1170
|
path: confirmedPath!,
|
|
960
1171
|
reason: `Phase 2a component-name match confirmed by element search`
|
|
961
1172
|
};
|
|
962
|
-
debugLog("PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
|
|
1173
|
+
debugLog("FALLBACK PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
|
|
963
1174
|
} else if (focusedTopScore > smartSearchTopScore * 0.5 && focusedTopScore >= 400) {
|
|
964
|
-
// PRIORITY 2: Focused element has strong
|
|
1175
|
+
// FALLBACK PRIORITY 2: Focused element has strong score
|
|
965
1176
|
recommendedFile = {
|
|
966
1177
|
path: focusedTopPath!,
|
|
967
1178
|
reason: `Focused element match (score: ${focusedTopScore}, exceeds threshold)`
|
|
968
1179
|
};
|
|
969
|
-
debugLog("PRIORITY 2: Strong focused element match", recommendedFile);
|
|
970
|
-
} else if (
|
|
971
|
-
// PRIORITY 3:
|
|
1180
|
+
debugLog("FALLBACK PRIORITY 2: Strong focused element match", recommendedFile);
|
|
1181
|
+
} else if (phase2aMatches.length > 0) {
|
|
1182
|
+
// FALLBACK PRIORITY 3: Phase 2a match WITHOUT focused element
|
|
1183
|
+
// Use prompt-aware selection to pick the best match
|
|
1184
|
+
const bestMatch = findBestPhase2aMatch()!;
|
|
972
1185
|
recommendedFile = {
|
|
973
|
-
path:
|
|
974
|
-
reason: `
|
|
1186
|
+
path: bestMatch,
|
|
1187
|
+
reason: `Phase 2a component-name match (prompt-aware selection from ${phase2aMatches.length} candidates)`
|
|
975
1188
|
};
|
|
976
|
-
debugLog("PRIORITY 3:
|
|
1189
|
+
debugLog("FALLBACK PRIORITY 3: Phase 2a match (no focused element)", {
|
|
1190
|
+
selectedPath: bestMatch,
|
|
1191
|
+
allCandidates: phase2aMatches
|
|
1192
|
+
});
|
|
1193
|
+
} else {
|
|
1194
|
+
// FALLBACK PRIORITY 4: Use the page file from the current route
|
|
1195
|
+
const routePageFile = discoverPageFile(pageRoute || "/", projectRoot);
|
|
1196
|
+
|
|
1197
|
+
if (routePageFile && fs.existsSync(path.join(projectRoot, routePageFile))) {
|
|
1198
|
+
recommendedFile = {
|
|
1199
|
+
path: routePageFile,
|
|
1200
|
+
reason: `Page file from route "${pageRoute || "/"}"`
|
|
1201
|
+
};
|
|
1202
|
+
debugLog("FALLBACK PRIORITY 4: Using page file from route", {
|
|
1203
|
+
route: pageRoute || "/",
|
|
1204
|
+
pageFile: routePageFile
|
|
1205
|
+
});
|
|
1206
|
+
} else if (smartSearchTopPath) {
|
|
1207
|
+
// FALLBACK PRIORITY 5: Smart search top result - last resort
|
|
1208
|
+
recommendedFile = {
|
|
1209
|
+
path: smartSearchTopPath,
|
|
1210
|
+
reason: `Smart search top result (score: ${smartSearchTopScore})`
|
|
1211
|
+
};
|
|
1212
|
+
debugLog("FALLBACK PRIORITY 5: Using smart search top result", recommendedFile);
|
|
1213
|
+
}
|
|
977
1214
|
}
|
|
978
1215
|
|
|
979
1216
|
debugLog("Phase 1+2 complete", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.89",
|
|
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",
|