sonance-brand-mcp 1.3.38 → 1.3.40

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.
@@ -80,14 +80,23 @@ function debugLog(message: string, data?: unknown) {
80
80
  }
81
81
 
82
82
  /**
83
- * Phase 1: Extract visible text from screenshot
84
- * The LLM reads the screenshot and extracts text content related to the user's edit request
83
+ * Result of LLM screenshot analysis for smart file discovery
85
84
  */
86
- async function extractTextFromScreenshot(
85
+ interface ScreenshotAnalysis {
86
+ visibleText: string[];
87
+ componentNames: string[];
88
+ codePatterns: string[];
89
+ }
90
+
91
+ /**
92
+ * Phase 1: Analyze screenshot using LLM vision for smart file discovery
93
+ * The LLM extracts visible text AND deduces likely component names and code patterns
94
+ */
95
+ async function analyzeScreenshotForSearch(
87
96
  screenshot: string,
88
97
  userPrompt: string,
89
98
  apiKey: string
90
- ): Promise<string[]> {
99
+ ): Promise<ScreenshotAnalysis> {
91
100
  const anthropic = new Anthropic({ apiKey });
92
101
 
93
102
  const base64Data = screenshot.split(",")[1] || screenshot;
@@ -109,15 +118,22 @@ async function extractTextFromScreenshot(
109
118
  },
110
119
  {
111
120
  type: "text",
112
- text: `Look at this screenshot. The user wants to: "${userPrompt}"
121
+ text: `Look at this screenshot of a React application. The user wants to: "${userPrompt}"
122
+
123
+ Analyze this UI to help find the correct source file. Return:
113
124
 
114
- Extract the EXACT text content visible in the UI elements that need to be edited.
115
- Focus on button labels, headings, and any text within the area the user wants to change.
125
+ 1. **visibleText**: Extract the EXACT text visible in UI elements (button labels, headings, tab names, etc.)
126
+ 2. **componentNames**: Deduce likely React component names based on what you see (e.g., "ProcessDetailPanel", "UserSettings", "DataTable"). Think about common React naming conventions.
127
+ 3. **codePatterns**: Suggest code identifiers that might exist (e.g., "handleEdit", "isLoading", "activeTab", "onDelete")
116
128
 
117
129
  Return ONLY valid JSON:
118
- { "textStrings": ["View Assets", "View Flowchart", "exact text 1", "exact text 2"] }
130
+ {
131
+ "visibleText": ["Assets", "Flowchart", "Edit", "Delete"],
132
+ "componentNames": ["ProcessDetailPanel", "ProcessActions", "QuickActions"],
133
+ "codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"]
134
+ }
119
135
 
120
- Be precise - return the exact text as it appears, no extra words.`,
136
+ Be smart - use your knowledge of React patterns to make educated guesses about component and variable names.`,
121
137
  },
122
138
  ],
123
139
  },
@@ -126,7 +142,9 @@ Be precise - return the exact text as it appears, no extra words.`,
126
142
 
127
143
  try {
128
144
  const textBlock = response.content.find((block) => block.type === "text");
129
- if (!textBlock || textBlock.type !== "text") return [];
145
+ if (!textBlock || textBlock.type !== "text") {
146
+ return { visibleText: [], componentNames: [], codePatterns: [] };
147
+ }
130
148
 
131
149
  let jsonText = textBlock.text.trim();
132
150
  const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
@@ -136,34 +154,131 @@ Be precise - return the exact text as it appears, no extra words.`,
136
154
  }
137
155
 
138
156
  const parsed = JSON.parse(jsonText);
139
- const strings = parsed.textStrings || [];
157
+ const result: ScreenshotAnalysis = {
158
+ visibleText: parsed.visibleText || parsed.textStrings || [],
159
+ componentNames: parsed.componentNames || [],
160
+ codePatterns: parsed.codePatterns || [],
161
+ };
140
162
 
141
- debugLog("Phase 1: Extracted text from screenshot", { textStrings: strings });
142
- return strings;
163
+ debugLog("Phase 1: Analyzed screenshot for search", result);
164
+ return result;
143
165
  } catch (e) {
144
- debugLog("Phase 1: Failed to parse text extraction response", { error: String(e) });
145
- return [];
166
+ debugLog("Phase 1: Failed to parse screenshot analysis response", { error: String(e) });
167
+ return { visibleText: [], componentNames: [], codePatterns: [] };
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Search for component files by name across the project
173
+ * Returns the file path if found, null otherwise
174
+ */
175
+ function findComponentFileByName(
176
+ componentName: string,
177
+ projectRoot: string
178
+ ): string | null {
179
+ const normalizedName = componentName.toLowerCase();
180
+ const SEARCH_DIRS = ["src/components", "components", "src", "app"];
181
+ const EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
182
+
183
+ function searchRecursive(
184
+ dir: string,
185
+ fileName: string,
186
+ depth = 0
187
+ ): string | null {
188
+ if (depth > 5) return null;
189
+ try {
190
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
191
+ // Check direct matches (case-insensitive)
192
+ for (const ext of EXTENSIONS) {
193
+ const match = entries.find(
194
+ (e) => e.isFile() && e.name.toLowerCase() === `${fileName}${ext}`
195
+ );
196
+ if (match) return path.join(dir, match.name);
197
+ }
198
+ // Also check for partial matches (e.g., "ProcessDetail" matches "ProcessDetailPanel.tsx")
199
+ for (const entry of entries) {
200
+ if (entry.isFile()) {
201
+ const entryNameLower = entry.name.toLowerCase();
202
+ if (entryNameLower.includes(fileName) && EXTENSIONS.some(ext => entryNameLower.endsWith(ext))) {
203
+ return path.join(dir, entry.name);
204
+ }
205
+ }
206
+ }
207
+ // Recurse into subdirs
208
+ for (const entry of entries) {
209
+ if (
210
+ entry.isDirectory() &&
211
+ !entry.name.startsWith(".") &&
212
+ entry.name !== "node_modules"
213
+ ) {
214
+ const result = searchRecursive(
215
+ path.join(dir, entry.name),
216
+ fileName,
217
+ depth + 1
218
+ );
219
+ if (result) return result;
220
+ }
221
+ }
222
+ } catch {
223
+ /* skip unreadable */
224
+ }
225
+ return null;
146
226
  }
227
+
228
+ for (const dir of SEARCH_DIRS) {
229
+ const searchDir = path.join(projectRoot, dir);
230
+ if (fs.existsSync(searchDir)) {
231
+ const result = searchRecursive(searchDir, normalizedName);
232
+ if (result) return path.relative(projectRoot, result);
233
+ }
234
+ }
235
+ return null;
147
236
  }
148
237
 
149
238
  /**
150
- * Search project files for exact text matches
151
- * Returns file paths and content for files containing any of the search strings
239
+ * Smart file search using LLM-derived analysis
240
+ * Uses intelligent scoring: filename matches > unique text matches > total matches
152
241
  */
153
- function searchFilesForExactText(
154
- searchStrings: string[],
242
+ function searchFilesSmart(
243
+ analysis: ScreenshotAnalysis,
155
244
  projectRoot: string,
156
245
  maxFiles: number = 10
157
- ): { path: string; content: string }[] {
158
- if (searchStrings.length === 0) return [];
246
+ ): { path: string; content: string; score: number }[] {
247
+ const { visibleText, componentNames, codePatterns } = analysis;
248
+
249
+ // Combine all search terms
250
+ const allSearchTerms = [...visibleText, ...codePatterns];
251
+
252
+ if (allSearchTerms.length === 0 && componentNames.length === 0) return [];
159
253
 
160
- const results: { path: string; content: string; matchCount: number }[] = [];
161
- const searchDirs = ["components", "src/components", "app", "src/app"];
254
+ const results: Map<string, { path: string; content: string; score: number; filenameMatch: boolean }> = new Map();
255
+ const searchDirs = ["components", "src/components", "app", "src/app", "pages", "src/pages"];
162
256
  const extensions = [".tsx", ".jsx"];
163
257
  const visited = new Set<string>();
164
258
 
259
+ // Phase 2a: Search by component name (highest priority)
260
+ for (const componentName of componentNames) {
261
+ const foundPath = findComponentFileByName(componentName, projectRoot);
262
+ if (foundPath && !results.has(foundPath)) {
263
+ try {
264
+ const fullPath = path.join(projectRoot, foundPath);
265
+ const content = fs.readFileSync(fullPath, "utf-8");
266
+ results.set(foundPath, {
267
+ path: foundPath,
268
+ content,
269
+ score: 50, // High base score for filename match
270
+ filenameMatch: true
271
+ });
272
+ debugLog("Phase 2a: Found file by component name", { componentName, foundPath });
273
+ } catch {
274
+ // Skip files that can't be read
275
+ }
276
+ }
277
+ }
278
+
279
+ // Phase 2b: Search by text content (case-insensitive, unique term scoring)
165
280
  function searchDir(dirPath: string, depth: number = 0) {
166
- if (depth > 5 || results.length >= maxFiles) return;
281
+ if (depth > 5) return;
167
282
 
168
283
  const fullDirPath = path.join(projectRoot, dirPath);
169
284
  if (!fs.existsSync(fullDirPath) || visited.has(fullDirPath)) return;
@@ -173,8 +288,6 @@ function searchFilesForExactText(
173
288
  const entries = fs.readdirSync(fullDirPath, { withFileTypes: true });
174
289
 
175
290
  for (const entry of entries) {
176
- if (results.length >= maxFiles) break;
177
-
178
291
  const entryPath = path.join(dirPath, entry.name);
179
292
  const fullEntryPath = path.join(projectRoot, entryPath);
180
293
 
@@ -185,17 +298,41 @@ function searchFilesForExactText(
185
298
  } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
186
299
  try {
187
300
  const content = fs.readFileSync(fullEntryPath, "utf-8");
301
+ const contentLower = content.toLowerCase();
188
302
 
189
- // Count exact text matches
190
- let matchCount = 0;
191
- for (const searchText of searchStrings) {
192
- if (content.includes(searchText)) {
193
- matchCount++;
303
+ // Count unique matches and total matches
304
+ let uniqueMatches = 0;
305
+ let totalMatches = 0;
306
+
307
+ for (const searchTerm of allSearchTerms) {
308
+ const searchLower = searchTerm.toLowerCase();
309
+ // Check if term exists (case-insensitive)
310
+ if (contentLower.includes(searchLower)) {
311
+ uniqueMatches++;
312
+ // Count occurrences
313
+ const regex = new RegExp(searchLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
314
+ const matches = content.match(regex);
315
+ totalMatches += matches ? matches.length : 0;
194
316
  }
195
317
  }
196
318
 
197
- if (matchCount > 0) {
198
- results.push({ path: entryPath, content, matchCount });
319
+ if (uniqueMatches > 0) {
320
+ // Score: unique matches * 10 + total matches
321
+ const contentScore = (uniqueMatches * 10) + totalMatches;
322
+
323
+ // Check if we already have this file (from component name search)
324
+ const existing = results.get(entryPath);
325
+ if (existing) {
326
+ // Add content score to existing filename match score
327
+ existing.score += contentScore;
328
+ } else {
329
+ results.set(entryPath, {
330
+ path: entryPath,
331
+ content,
332
+ score: contentScore,
333
+ filenameMatch: false
334
+ });
335
+ }
199
336
  }
200
337
  } catch {
201
338
  // Skip files that can't be read
@@ -209,19 +346,26 @@ function searchFilesForExactText(
209
346
 
210
347
  for (const dir of searchDirs) {
211
348
  searchDir(dir);
212
- if (results.length >= maxFiles) break;
213
349
  }
214
350
 
215
- // Sort by match count (most matches first)
216
- results.sort((a, b) => b.matchCount - a.matchCount);
217
-
218
- debugLog("Phase 2: Found files with text matches", {
219
- searchStrings,
220
- filesFound: results.length,
221
- topMatches: results.slice(0, 5).map(r => ({ path: r.path, matchCount: r.matchCount }))
351
+ // Sort by score (highest first)
352
+ const sortedResults = Array.from(results.values())
353
+ .sort((a, b) => b.score - a.score)
354
+ .slice(0, maxFiles);
355
+
356
+ debugLog("Phase 2: Smart search results", {
357
+ componentNames,
358
+ visibleText: visibleText.slice(0, 5),
359
+ codePatterns: codePatterns.slice(0, 5),
360
+ filesFound: sortedResults.length,
361
+ topMatches: sortedResults.slice(0, 5).map(r => ({
362
+ path: r.path,
363
+ score: r.score,
364
+ filenameMatch: r.filenameMatch
365
+ }))
222
366
  });
223
367
 
224
- return results.slice(0, maxFiles).map(r => ({ path: r.path, content: r.content }));
368
+ return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
225
369
  }
226
370
 
227
371
  const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
@@ -357,19 +501,56 @@ export async function POST(request: Request) {
357
501
  // Generate a unique session ID
358
502
  const newSessionId = randomUUID().slice(0, 8);
359
503
 
360
- // PHASE 1: Extract visible text from screenshot using LLM vision
361
- // This helps find the correct component file even if it's not in the import chain
362
- let textSearchFiles: { path: string; content: string }[] = [];
504
+ // PHASE 1+2: LLM-driven smart file discovery
505
+ // The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
506
+ let smartSearchFiles: { path: string; content: string }[] = [];
507
+ let recommendedFile: { path: string; reason: string } | null = null;
508
+
363
509
  if (screenshot) {
364
- debugLog("Starting Phase 1: Text extraction from screenshot");
365
- const extractedText = await extractTextFromScreenshot(screenshot, userPrompt, apiKey);
510
+ debugLog("Starting Phase 1: LLM screenshot analysis");
511
+ const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
366
512
 
367
- if (extractedText.length > 0) {
368
- // PHASE 2: Search project for files containing the extracted text
369
- textSearchFiles = searchFilesForExactText(extractedText, projectRoot, 5);
513
+ const hasSearchTerms = analysis.visibleText.length > 0 ||
514
+ analysis.componentNames.length > 0 ||
515
+ analysis.codePatterns.length > 0;
516
+
517
+ if (hasSearchTerms) {
518
+ // PHASE 2: Smart search using component names, text, and code patterns
519
+ const searchResults = searchFilesSmart(analysis, projectRoot, 10);
520
+ smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
521
+
522
+ // Identify the recommended file (highest scoring file, prefer filename matches)
523
+ if (searchResults.length > 0) {
524
+ // Find the best filename match first
525
+ const filenameMatch = searchResults.find(r =>
526
+ analysis.componentNames.some(name =>
527
+ r.path.toLowerCase().includes(name.toLowerCase())
528
+ )
529
+ );
530
+
531
+ if (filenameMatch) {
532
+ recommendedFile = {
533
+ path: filenameMatch.path,
534
+ reason: `Component name match from screenshot analysis`
535
+ };
536
+ } else {
537
+ // Fall back to highest score
538
+ recommendedFile = {
539
+ path: searchResults[0].path,
540
+ reason: `Highest content match score (${searchResults[0].score} points)`
541
+ };
542
+ }
543
+ debugLog("Recommended file for editing", recommendedFile);
544
+ }
545
+
370
546
  debugLog("Phase 1+2 complete", {
371
- extractedText,
372
- filesFound: textSearchFiles.map(f => f.path)
547
+ analysis: {
548
+ visibleText: analysis.visibleText.slice(0, 5),
549
+ componentNames: analysis.componentNames,
550
+ codePatterns: analysis.codePatterns.slice(0, 5)
551
+ },
552
+ filesFound: smartSearchFiles.map(f => f.path),
553
+ recommendedFile
373
554
  });
374
555
  }
375
556
  }
@@ -377,17 +558,18 @@ export async function POST(request: Request) {
377
558
  // Gather page context (with keyword search based on user prompt)
378
559
  const pageContext = gatherPageContext(pageRoute || "/", projectRoot, userPrompt);
379
560
 
380
- // Add text-search discovered files to component sources (if not already present)
561
+ // Add smart-search discovered files to component sources (if not already present)
562
+ // These files are prioritized because they were identified by LLM analysis
381
563
  const existingPaths = new Set([
382
564
  pageContext.pageFile,
383
565
  ...pageContext.componentSources.map(c => c.path)
384
566
  ]);
385
567
 
386
- for (const file of textSearchFiles) {
568
+ for (const file of smartSearchFiles) {
387
569
  if (!existingPaths.has(file.path)) {
388
570
  // Insert at the beginning so these get priority
389
571
  pageContext.componentSources.unshift(file);
390
- debugLog("Added file from text search to context", { path: file.path });
572
+ debugLog("Added file from smart search to context", { path: file.path });
391
573
  }
392
574
  }
393
575
 
@@ -415,6 +597,22 @@ User Request: "${userPrompt}"
415
597
 
416
598
  `;
417
599
 
600
+ // Add recommendation if smart search identified a best match
601
+ if (recommendedFile) {
602
+ textContent += `═══════════════════════════════════════════════════════════════════════════════
603
+ ⚡ RECOMMENDED FILE TO EDIT
604
+ ═══════════════════════════════════════════════════════════════════════════════
605
+
606
+ Based on the screenshot analysis, the component you should edit is:
607
+ **${recommendedFile.path}**
608
+ Reason: ${recommendedFile.reason}
609
+
610
+ The page file (${pageContext.pageFile}) is just a wrapper - the actual UI elements are in the component above.
611
+ STRONGLY PREFER editing the recommended file unless you have a specific reason not to.
612
+
613
+ `;
614
+ }
615
+
418
616
  if (focusedElements && focusedElements.length > 0) {
419
617
  textContent += `FOCUSED ELEMENTS (user clicked on these):
420
618
  ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
@@ -78,14 +78,23 @@ function debugLog(message: string, data?: unknown) {
78
78
  }
79
79
 
80
80
  /**
81
- * Phase 1: Extract visible text from screenshot
82
- * The LLM reads the screenshot and extracts text content related to the user's edit request
81
+ * Result of LLM screenshot analysis for smart file discovery
83
82
  */
84
- async function extractTextFromScreenshot(
83
+ interface ScreenshotAnalysis {
84
+ visibleText: string[];
85
+ componentNames: string[];
86
+ codePatterns: string[];
87
+ }
88
+
89
+ /**
90
+ * Phase 1: Analyze screenshot using LLM vision for smart file discovery
91
+ * The LLM extracts visible text AND deduces likely component names and code patterns
92
+ */
93
+ async function analyzeScreenshotForSearch(
85
94
  screenshot: string,
86
95
  userPrompt: string,
87
96
  apiKey: string
88
- ): Promise<string[]> {
97
+ ): Promise<ScreenshotAnalysis> {
89
98
  const anthropic = new Anthropic({ apiKey });
90
99
 
91
100
  const base64Data = screenshot.split(",")[1] || screenshot;
@@ -107,15 +116,22 @@ async function extractTextFromScreenshot(
107
116
  },
108
117
  {
109
118
  type: "text",
110
- text: `Look at this screenshot. The user wants to: "${userPrompt}"
119
+ text: `Look at this screenshot of a React application. The user wants to: "${userPrompt}"
111
120
 
112
- Extract the EXACT text content visible in the UI elements that need to be edited.
113
- Focus on button labels, headings, and any text within the area the user wants to change.
121
+ Analyze this UI to help find the correct source file. Return:
122
+
123
+ 1. **visibleText**: Extract the EXACT text visible in UI elements (button labels, headings, tab names, etc.)
124
+ 2. **componentNames**: Deduce likely React component names based on what you see (e.g., "ProcessDetailPanel", "UserSettings", "DataTable"). Think about common React naming conventions.
125
+ 3. **codePatterns**: Suggest code identifiers that might exist (e.g., "handleEdit", "isLoading", "activeTab", "onDelete")
114
126
 
115
127
  Return ONLY valid JSON:
116
- { "textStrings": ["View Assets", "View Flowchart", "exact text 1", "exact text 2"] }
128
+ {
129
+ "visibleText": ["Assets", "Flowchart", "Edit", "Delete"],
130
+ "componentNames": ["ProcessDetailPanel", "ProcessActions", "QuickActions"],
131
+ "codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"]
132
+ }
117
133
 
118
- Be precise - return the exact text as it appears, no extra words.`,
134
+ Be smart - use your knowledge of React patterns to make educated guesses about component and variable names.`,
119
135
  },
120
136
  ],
121
137
  },
@@ -124,7 +140,9 @@ Be precise - return the exact text as it appears, no extra words.`,
124
140
 
125
141
  try {
126
142
  const textBlock = response.content.find((block) => block.type === "text");
127
- if (!textBlock || textBlock.type !== "text") return [];
143
+ if (!textBlock || textBlock.type !== "text") {
144
+ return { visibleText: [], componentNames: [], codePatterns: [] };
145
+ }
128
146
 
129
147
  let jsonText = textBlock.text.trim();
130
148
  const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
@@ -134,34 +152,131 @@ Be precise - return the exact text as it appears, no extra words.`,
134
152
  }
135
153
 
136
154
  const parsed = JSON.parse(jsonText);
137
- const strings = parsed.textStrings || [];
155
+ const result: ScreenshotAnalysis = {
156
+ visibleText: parsed.visibleText || parsed.textStrings || [],
157
+ componentNames: parsed.componentNames || [],
158
+ codePatterns: parsed.codePatterns || [],
159
+ };
138
160
 
139
- debugLog("Phase 1: Extracted text from screenshot", { textStrings: strings });
140
- return strings;
161
+ debugLog("Phase 1: Analyzed screenshot for search", result);
162
+ return result;
141
163
  } catch (e) {
142
- debugLog("Phase 1: Failed to parse text extraction response", { error: String(e) });
143
- return [];
164
+ debugLog("Phase 1: Failed to parse screenshot analysis response", { error: String(e) });
165
+ return { visibleText: [], componentNames: [], codePatterns: [] };
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Search for component files by name across the project (for smart search)
171
+ * Returns the file path if found, null otherwise
172
+ */
173
+ function findComponentFileByNameSmart(
174
+ componentName: string,
175
+ projectRoot: string
176
+ ): string | null {
177
+ const normalizedName = componentName.toLowerCase();
178
+ const SEARCH_DIRS = ["src/components", "components", "src", "app"];
179
+ const EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
180
+
181
+ function searchRecursive(
182
+ dir: string,
183
+ fileName: string,
184
+ depth = 0
185
+ ): string | null {
186
+ if (depth > 5) return null;
187
+ try {
188
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
189
+ // Check direct matches (case-insensitive)
190
+ for (const ext of EXTENSIONS) {
191
+ const match = entries.find(
192
+ (e) => e.isFile() && e.name.toLowerCase() === `${fileName}${ext}`
193
+ );
194
+ if (match) return path.join(dir, match.name);
195
+ }
196
+ // Also check for partial matches (e.g., "ProcessDetail" matches "ProcessDetailPanel.tsx")
197
+ for (const entry of entries) {
198
+ if (entry.isFile()) {
199
+ const entryNameLower = entry.name.toLowerCase();
200
+ if (entryNameLower.includes(fileName) && EXTENSIONS.some(ext => entryNameLower.endsWith(ext))) {
201
+ return path.join(dir, entry.name);
202
+ }
203
+ }
204
+ }
205
+ // Recurse into subdirs
206
+ for (const entry of entries) {
207
+ if (
208
+ entry.isDirectory() &&
209
+ !entry.name.startsWith(".") &&
210
+ entry.name !== "node_modules"
211
+ ) {
212
+ const result = searchRecursive(
213
+ path.join(dir, entry.name),
214
+ fileName,
215
+ depth + 1
216
+ );
217
+ if (result) return result;
218
+ }
219
+ }
220
+ } catch {
221
+ /* skip unreadable */
222
+ }
223
+ return null;
224
+ }
225
+
226
+ for (const dir of SEARCH_DIRS) {
227
+ const searchDir = path.join(projectRoot, dir);
228
+ if (fs.existsSync(searchDir)) {
229
+ const result = searchRecursive(searchDir, normalizedName);
230
+ if (result) return path.relative(projectRoot, result);
231
+ }
144
232
  }
233
+ return null;
145
234
  }
146
235
 
147
236
  /**
148
- * Search project files for exact text matches
149
- * Returns file paths and content for files containing any of the search strings
237
+ * Smart file search using LLM-derived analysis
238
+ * Uses intelligent scoring: filename matches > unique text matches > total matches
150
239
  */
151
- function searchFilesForExactText(
152
- searchStrings: string[],
240
+ function searchFilesSmart(
241
+ analysis: ScreenshotAnalysis,
153
242
  projectRoot: string,
154
243
  maxFiles: number = 10
155
- ): { path: string; content: string }[] {
156
- if (searchStrings.length === 0) return [];
244
+ ): { path: string; content: string; score: number }[] {
245
+ const { visibleText, componentNames, codePatterns } = analysis;
246
+
247
+ // Combine all search terms
248
+ const allSearchTerms = [...visibleText, ...codePatterns];
249
+
250
+ if (allSearchTerms.length === 0 && componentNames.length === 0) return [];
157
251
 
158
- const results: { path: string; content: string; matchCount: number }[] = [];
159
- const searchDirs = ["components", "src/components", "app", "src/app"];
252
+ const results: Map<string, { path: string; content: string; score: number; filenameMatch: boolean }> = new Map();
253
+ const searchDirs = ["components", "src/components", "app", "src/app", "pages", "src/pages"];
160
254
  const extensions = [".tsx", ".jsx"];
161
255
  const visited = new Set<string>();
162
256
 
257
+ // Phase 2a: Search by component name (highest priority)
258
+ for (const componentName of componentNames) {
259
+ const foundPath = findComponentFileByNameSmart(componentName, projectRoot);
260
+ if (foundPath && !results.has(foundPath)) {
261
+ try {
262
+ const fullPath = path.join(projectRoot, foundPath);
263
+ const content = fs.readFileSync(fullPath, "utf-8");
264
+ results.set(foundPath, {
265
+ path: foundPath,
266
+ content,
267
+ score: 50, // High base score for filename match
268
+ filenameMatch: true
269
+ });
270
+ debugLog("Phase 2a: Found file by component name", { componentName, foundPath });
271
+ } catch {
272
+ // Skip files that can't be read
273
+ }
274
+ }
275
+ }
276
+
277
+ // Phase 2b: Search by text content (case-insensitive, unique term scoring)
163
278
  function searchDir(dirPath: string, depth: number = 0) {
164
- if (depth > 5 || results.length >= maxFiles) return;
279
+ if (depth > 5) return;
165
280
 
166
281
  const fullDirPath = path.join(projectRoot, dirPath);
167
282
  if (!fs.existsSync(fullDirPath) || visited.has(fullDirPath)) return;
@@ -171,8 +286,6 @@ function searchFilesForExactText(
171
286
  const entries = fs.readdirSync(fullDirPath, { withFileTypes: true });
172
287
 
173
288
  for (const entry of entries) {
174
- if (results.length >= maxFiles) break;
175
-
176
289
  const entryPath = path.join(dirPath, entry.name);
177
290
  const fullEntryPath = path.join(projectRoot, entryPath);
178
291
 
@@ -183,17 +296,41 @@ function searchFilesForExactText(
183
296
  } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
184
297
  try {
185
298
  const content = fs.readFileSync(fullEntryPath, "utf-8");
299
+ const contentLower = content.toLowerCase();
186
300
 
187
- // Count exact text matches
188
- let matchCount = 0;
189
- for (const searchText of searchStrings) {
190
- if (content.includes(searchText)) {
191
- matchCount++;
301
+ // Count unique matches and total matches
302
+ let uniqueMatches = 0;
303
+ let totalMatches = 0;
304
+
305
+ for (const searchTerm of allSearchTerms) {
306
+ const searchLower = searchTerm.toLowerCase();
307
+ // Check if term exists (case-insensitive)
308
+ if (contentLower.includes(searchLower)) {
309
+ uniqueMatches++;
310
+ // Count occurrences
311
+ const regex = new RegExp(searchLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
312
+ const matches = content.match(regex);
313
+ totalMatches += matches ? matches.length : 0;
192
314
  }
193
315
  }
194
316
 
195
- if (matchCount > 0) {
196
- results.push({ path: entryPath, content, matchCount });
317
+ if (uniqueMatches > 0) {
318
+ // Score: unique matches * 10 + total matches
319
+ const contentScore = (uniqueMatches * 10) + totalMatches;
320
+
321
+ // Check if we already have this file (from component name search)
322
+ const existing = results.get(entryPath);
323
+ if (existing) {
324
+ // Add content score to existing filename match score
325
+ existing.score += contentScore;
326
+ } else {
327
+ results.set(entryPath, {
328
+ path: entryPath,
329
+ content,
330
+ score: contentScore,
331
+ filenameMatch: false
332
+ });
333
+ }
197
334
  }
198
335
  } catch {
199
336
  // Skip files that can't be read
@@ -207,19 +344,26 @@ function searchFilesForExactText(
207
344
 
208
345
  for (const dir of searchDirs) {
209
346
  searchDir(dir);
210
- if (results.length >= maxFiles) break;
211
347
  }
212
348
 
213
- // Sort by match count (most matches first)
214
- results.sort((a, b) => b.matchCount - a.matchCount);
215
-
216
- debugLog("Phase 2: Found files with text matches", {
217
- searchStrings,
218
- filesFound: results.length,
219
- topMatches: results.slice(0, 5).map(r => ({ path: r.path, matchCount: r.matchCount }))
349
+ // Sort by score (highest first)
350
+ const sortedResults = Array.from(results.values())
351
+ .sort((a, b) => b.score - a.score)
352
+ .slice(0, maxFiles);
353
+
354
+ debugLog("Phase 2: Smart search results", {
355
+ componentNames,
356
+ visibleText: visibleText.slice(0, 5),
357
+ codePatterns: codePatterns.slice(0, 5),
358
+ filesFound: sortedResults.length,
359
+ topMatches: sortedResults.slice(0, 5).map(r => ({
360
+ path: r.path,
361
+ score: r.score,
362
+ filenameMatch: r.filenameMatch
363
+ }))
220
364
  });
221
365
 
222
- return results.slice(0, maxFiles).map(r => ({ path: r.path, content: r.content }));
366
+ return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
223
367
  }
224
368
 
225
369
  const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
@@ -367,19 +511,56 @@ export async function POST(request: Request) {
367
511
  );
368
512
  }
369
513
 
370
- // PHASE 1: Extract visible text from screenshot using LLM vision
371
- // This helps find the correct component file even if it's not in the import chain
372
- let textSearchFiles: { path: string; content: string }[] = [];
514
+ // PHASE 1+2: LLM-driven smart file discovery
515
+ // The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
516
+ let smartSearchFiles: { path: string; content: string }[] = [];
517
+ let recommendedFile: { path: string; reason: string } | null = null;
518
+
373
519
  if (screenshot) {
374
- debugLog("Starting Phase 1: Text extraction from screenshot");
375
- const extractedText = await extractTextFromScreenshot(screenshot, userPrompt, apiKey);
520
+ debugLog("Starting Phase 1: LLM screenshot analysis");
521
+ const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
376
522
 
377
- if (extractedText.length > 0) {
378
- // PHASE 2: Search project for files containing the extracted text
379
- textSearchFiles = searchFilesForExactText(extractedText, projectRoot, 5);
523
+ const hasSearchTerms = analysis.visibleText.length > 0 ||
524
+ analysis.componentNames.length > 0 ||
525
+ analysis.codePatterns.length > 0;
526
+
527
+ if (hasSearchTerms) {
528
+ // PHASE 2: Smart search using component names, text, and code patterns
529
+ const searchResults = searchFilesSmart(analysis, projectRoot, 10);
530
+ smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
531
+
532
+ // Identify the recommended file (highest scoring file, prefer filename matches)
533
+ if (searchResults.length > 0) {
534
+ // Find the best filename match first
535
+ const filenameMatch = searchResults.find(r =>
536
+ analysis.componentNames.some(name =>
537
+ r.path.toLowerCase().includes(name.toLowerCase())
538
+ )
539
+ );
540
+
541
+ if (filenameMatch) {
542
+ recommendedFile = {
543
+ path: filenameMatch.path,
544
+ reason: `Component name match from screenshot analysis`
545
+ };
546
+ } else {
547
+ // Fall back to highest score
548
+ recommendedFile = {
549
+ path: searchResults[0].path,
550
+ reason: `Highest content match score (${searchResults[0].score} points)`
551
+ };
552
+ }
553
+ debugLog("Recommended file for editing", recommendedFile);
554
+ }
555
+
380
556
  debugLog("Phase 1+2 complete", {
381
- extractedText,
382
- filesFound: textSearchFiles.map(f => f.path)
557
+ analysis: {
558
+ visibleText: analysis.visibleText.slice(0, 5),
559
+ componentNames: analysis.componentNames,
560
+ codePatterns: analysis.codePatterns.slice(0, 5)
561
+ },
562
+ filesFound: smartSearchFiles.map(f => f.path),
563
+ recommendedFile
383
564
  });
384
565
  }
385
566
  }
@@ -387,17 +568,18 @@ export async function POST(request: Request) {
387
568
  // Gather page context (including focused element files and keyword search)
388
569
  const pageContext = gatherPageContext(pageRoute, projectRoot, focusedElements, userPrompt);
389
570
 
390
- // Add text-search discovered files to component sources (if not already present)
571
+ // Add smart-search discovered files to component sources (if not already present)
572
+ // These files are prioritized because they were identified by LLM analysis
391
573
  const existingPaths = new Set([
392
574
  pageContext.pageFile,
393
575
  ...pageContext.componentSources.map(c => c.path)
394
576
  ]);
395
577
 
396
- for (const file of textSearchFiles) {
578
+ for (const file of smartSearchFiles) {
397
579
  if (!existingPaths.has(file.path)) {
398
580
  // Insert at the beginning so these get priority
399
581
  pageContext.componentSources.unshift(file);
400
- debugLog("Added file from text search to context", { path: file.path });
582
+ debugLog("Added file from smart search to context", { path: file.path });
401
583
  }
402
584
  }
403
585
 
@@ -425,6 +607,22 @@ User Request: "${userPrompt}"
425
607
 
426
608
  `;
427
609
 
610
+ // Add recommendation if smart search identified a best match
611
+ if (recommendedFile) {
612
+ textContent += `═══════════════════════════════════════════════════════════════════════════════
613
+ ⚡ RECOMMENDED FILE TO EDIT
614
+ ═══════════════════════════════════════════════════════════════════════════════
615
+
616
+ Based on the screenshot analysis, the component you should edit is:
617
+ **${recommendedFile.path}**
618
+ Reason: ${recommendedFile.reason}
619
+
620
+ The page file (${pageContext.pageFile}) is just a wrapper - the actual UI elements are in the component above.
621
+ STRONGLY PREFER editing the recommended file unless you have a specific reason not to.
622
+
623
+ `;
624
+ }
625
+
428
626
  if (focusedElements && focusedElements.length > 0) {
429
627
  textContent += `FOCUSED ELEMENTS (user clicked on these):
430
628
  ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.38",
3
+ "version": "1.3.40",
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",