sonance-brand-mcp 1.3.37 → 1.3.39

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}"
113
122
 
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.
123
+ Analyze this UI to help find the correct source file. Return:
124
+
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;
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
+ }
146
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,28 @@ 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 }[] = [];
363
507
  if (screenshot) {
364
- debugLog("Starting Phase 1: Text extraction from screenshot");
365
- const extractedText = await extractTextFromScreenshot(screenshot, userPrompt, apiKey);
508
+ debugLog("Starting Phase 1: LLM screenshot analysis");
509
+ const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
366
510
 
367
- if (extractedText.length > 0) {
368
- // PHASE 2: Search project for files containing the extracted text
369
- textSearchFiles = searchFilesForExactText(extractedText, projectRoot, 5);
511
+ const hasSearchTerms = analysis.visibleText.length > 0 ||
512
+ analysis.componentNames.length > 0 ||
513
+ analysis.codePatterns.length > 0;
514
+
515
+ if (hasSearchTerms) {
516
+ // PHASE 2: Smart search using component names, text, and code patterns
517
+ const searchResults = searchFilesSmart(analysis, projectRoot, 10);
518
+ smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
370
519
  debugLog("Phase 1+2 complete", {
371
- extractedText,
372
- filesFound: textSearchFiles.map(f => f.path)
520
+ analysis: {
521
+ visibleText: analysis.visibleText.slice(0, 5),
522
+ componentNames: analysis.componentNames,
523
+ codePatterns: analysis.codePatterns.slice(0, 5)
524
+ },
525
+ filesFound: smartSearchFiles.map(f => f.path)
373
526
  });
374
527
  }
375
528
  }
@@ -377,17 +530,18 @@ export async function POST(request: Request) {
377
530
  // Gather page context (with keyword search based on user prompt)
378
531
  const pageContext = gatherPageContext(pageRoute || "/", projectRoot, userPrompt);
379
532
 
380
- // Add text-search discovered files to component sources (if not already present)
533
+ // Add smart-search discovered files to component sources (if not already present)
534
+ // These files are prioritized because they were identified by LLM analysis
381
535
  const existingPaths = new Set([
382
536
  pageContext.pageFile,
383
537
  ...pageContext.componentSources.map(c => c.path)
384
538
  ]);
385
539
 
386
- for (const file of textSearchFiles) {
540
+ for (const file of smartSearchFiles) {
387
541
  if (!existingPaths.has(file.path)) {
388
542
  // Insert at the beginning so these get priority
389
543
  pageContext.componentSources.unshift(file);
390
- debugLog("Added file from text search to context", { path: file.path });
544
+ debugLog("Added file from smart search to context", { path: file.path });
391
545
  }
392
546
  }
393
547
 
@@ -1438,8 +1592,11 @@ function getPathAliases(projectRoot: string): Map<string, string> {
1438
1592
  if (fs.existsSync(tsconfigPath)) {
1439
1593
  try {
1440
1594
  const content = fs.readFileSync(tsconfigPath, "utf-8");
1441
- // Remove comments (tsconfig allows them)
1442
- const cleanContent = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
1595
+ // Remove comments (tsconfig allows them) and trailing commas (valid in tsconfig but not JSON)
1596
+ const cleanContent = content
1597
+ .replace(/\/\/.*$/gm, "")
1598
+ .replace(/\/\*[\s\S]*?\*\//g, "")
1599
+ .replace(/,\s*([\]}])/g, "$1");
1443
1600
  const tsconfig = JSON.parse(cleanContent);
1444
1601
 
1445
1602
  const paths = tsconfig.compilerOptions?.paths || {};
@@ -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}"
120
+
121
+ Analyze this UI to help find the correct source file. Return:
111
122
 
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.
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,28 @@ 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 }[] = [];
373
517
  if (screenshot) {
374
- debugLog("Starting Phase 1: Text extraction from screenshot");
375
- const extractedText = await extractTextFromScreenshot(screenshot, userPrompt, apiKey);
518
+ debugLog("Starting Phase 1: LLM screenshot analysis");
519
+ const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
376
520
 
377
- if (extractedText.length > 0) {
378
- // PHASE 2: Search project for files containing the extracted text
379
- textSearchFiles = searchFilesForExactText(extractedText, projectRoot, 5);
521
+ const hasSearchTerms = analysis.visibleText.length > 0 ||
522
+ analysis.componentNames.length > 0 ||
523
+ analysis.codePatterns.length > 0;
524
+
525
+ if (hasSearchTerms) {
526
+ // PHASE 2: Smart search using component names, text, and code patterns
527
+ const searchResults = searchFilesSmart(analysis, projectRoot, 10);
528
+ smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
380
529
  debugLog("Phase 1+2 complete", {
381
- extractedText,
382
- filesFound: textSearchFiles.map(f => f.path)
530
+ analysis: {
531
+ visibleText: analysis.visibleText.slice(0, 5),
532
+ componentNames: analysis.componentNames,
533
+ codePatterns: analysis.codePatterns.slice(0, 5)
534
+ },
535
+ filesFound: smartSearchFiles.map(f => f.path)
383
536
  });
384
537
  }
385
538
  }
@@ -387,17 +540,18 @@ export async function POST(request: Request) {
387
540
  // Gather page context (including focused element files and keyword search)
388
541
  const pageContext = gatherPageContext(pageRoute, projectRoot, focusedElements, userPrompt);
389
542
 
390
- // Add text-search discovered files to component sources (if not already present)
543
+ // Add smart-search discovered files to component sources (if not already present)
544
+ // These files are prioritized because they were identified by LLM analysis
391
545
  const existingPaths = new Set([
392
546
  pageContext.pageFile,
393
547
  ...pageContext.componentSources.map(c => c.path)
394
548
  ]);
395
549
 
396
- for (const file of textSearchFiles) {
550
+ for (const file of smartSearchFiles) {
397
551
  if (!existingPaths.has(file.path)) {
398
552
  // Insert at the beginning so these get priority
399
553
  pageContext.componentSources.unshift(file);
400
- debugLog("Added file from text search to context", { path: file.path });
554
+ debugLog("Added file from smart search to context", { path: file.path });
401
555
  }
402
556
  }
403
557
 
@@ -1348,8 +1502,11 @@ function getPathAliases(projectRoot: string): Map<string, string> {
1348
1502
  if (fs.existsSync(tsconfigPath)) {
1349
1503
  try {
1350
1504
  const content = fs.readFileSync(tsconfigPath, "utf-8");
1351
- // Remove comments (tsconfig allows them)
1352
- const cleanContent = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
1505
+ // Remove comments (tsconfig allows them) and trailing commas (valid in tsconfig but not JSON)
1506
+ const cleanContent = content
1507
+ .replace(/\/\/.*$/gm, "")
1508
+ .replace(/\/\*[\s\S]*?\*\//g, "")
1509
+ .replace(/,\s*([\]}])/g, "$1");
1353
1510
  const tsconfig = JSON.parse(cleanContent);
1354
1511
 
1355
1512
  const paths = tsconfig.compilerOptions?.paths || {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.37",
3
+ "version": "1.3.39",
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",