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
|
-
*
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
{
|
|
130
|
+
{
|
|
131
|
+
"visibleText": ["Assets", "Flowchart", "Edit", "Delete"],
|
|
132
|
+
"componentNames": ["ProcessDetailPanel", "ProcessActions", "QuickActions"],
|
|
133
|
+
"codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"]
|
|
134
|
+
}
|
|
119
135
|
|
|
120
|
-
Be
|
|
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")
|
|
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
|
|
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:
|
|
142
|
-
return
|
|
163
|
+
debugLog("Phase 1: Analyzed screenshot for search", result);
|
|
164
|
+
return result;
|
|
143
165
|
} catch (e) {
|
|
144
|
-
debugLog("Phase 1: Failed to parse
|
|
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
|
-
*
|
|
151
|
-
*
|
|
239
|
+
* Smart file search using LLM-derived analysis
|
|
240
|
+
* Uses intelligent scoring: filename matches > unique text matches > total matches
|
|
152
241
|
*/
|
|
153
|
-
function
|
|
154
|
-
|
|
242
|
+
function searchFilesSmart(
|
|
243
|
+
analysis: ScreenshotAnalysis,
|
|
155
244
|
projectRoot: string,
|
|
156
245
|
maxFiles: number = 10
|
|
157
|
-
): { path: string; content: string }[] {
|
|
158
|
-
|
|
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;
|
|
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
|
|
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
|
|
190
|
-
let
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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 (
|
|
198
|
-
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
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:
|
|
361
|
-
//
|
|
362
|
-
let
|
|
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:
|
|
365
|
-
const
|
|
508
|
+
debugLog("Starting Phase 1: LLM screenshot analysis");
|
|
509
|
+
const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
|
|
366
510
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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<
|
|
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
|
|
113
|
-
|
|
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
|
-
{
|
|
128
|
+
{
|
|
129
|
+
"visibleText": ["Assets", "Flowchart", "Edit", "Delete"],
|
|
130
|
+
"componentNames": ["ProcessDetailPanel", "ProcessActions", "QuickActions"],
|
|
131
|
+
"codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"]
|
|
132
|
+
}
|
|
117
133
|
|
|
118
|
-
Be
|
|
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")
|
|
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
|
|
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:
|
|
140
|
-
return
|
|
161
|
+
debugLog("Phase 1: Analyzed screenshot for search", result);
|
|
162
|
+
return result;
|
|
141
163
|
} catch (e) {
|
|
142
|
-
debugLog("Phase 1: Failed to parse
|
|
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
|
-
*
|
|
149
|
-
*
|
|
237
|
+
* Smart file search using LLM-derived analysis
|
|
238
|
+
* Uses intelligent scoring: filename matches > unique text matches > total matches
|
|
150
239
|
*/
|
|
151
|
-
function
|
|
152
|
-
|
|
240
|
+
function searchFilesSmart(
|
|
241
|
+
analysis: ScreenshotAnalysis,
|
|
153
242
|
projectRoot: string,
|
|
154
243
|
maxFiles: number = 10
|
|
155
|
-
): { path: string; content: string }[] {
|
|
156
|
-
|
|
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;
|
|
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
|
|
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
|
|
188
|
-
let
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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 (
|
|
196
|
-
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
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:
|
|
371
|
-
//
|
|
372
|
-
let
|
|
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:
|
|
375
|
-
const
|
|
518
|
+
debugLog("Starting Phase 1: LLM screenshot analysis");
|
|
519
|
+
const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
|
|
376
520
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
382
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|