sonance-brand-mcp 1.3.44 → 1.3.45

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.
@@ -168,6 +168,73 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
168
168
  }
169
169
  }
170
170
 
171
+ /**
172
+ * Phase 3: Ask LLM to select the best file from actual file list
173
+ * This replaces guessing - the LLM sees the real filenames and picks one
174
+ */
175
+ async function selectBestFileFromList(
176
+ screenshot: string,
177
+ userPrompt: string,
178
+ candidateFiles: string[],
179
+ apiKey: string
180
+ ): Promise<string | null> {
181
+ if (candidateFiles.length === 0) return null;
182
+
183
+ const anthropic = new Anthropic({ apiKey });
184
+ const base64Data = screenshot.split(",")[1] || screenshot;
185
+
186
+ try {
187
+ const response = await anthropic.messages.create({
188
+ model: "claude-sonnet-4-20250514",
189
+ max_tokens: 256,
190
+ messages: [{
191
+ role: "user",
192
+ content: [
193
+ {
194
+ type: "image",
195
+ source: { type: "base64", media_type: "image/png", data: base64Data }
196
+ },
197
+ {
198
+ type: "text",
199
+ text: `The user wants to: "${userPrompt}"
200
+
201
+ Here are the actual component files found in this codebase:
202
+ ${candidateFiles.map((f, i) => `${i + 1}. ${f}`).join('\n')}
203
+
204
+ Looking at the screenshot, which file MOST LIKELY contains the UI elements the user wants to modify?
205
+
206
+ IMPORTANT: Return ONLY the exact file path from the list above (e.g., "components/ProcessCatalogue/ProcessDetailPanel.tsx").
207
+ Do not add any explanation or other text.`
208
+ }
209
+ ]
210
+ }]
211
+ });
212
+
213
+ const textBlock = response.content.find(b => b.type === "text");
214
+ if (!textBlock || textBlock.type !== "text") return null;
215
+
216
+ const selectedPath = textBlock.text.trim().replace(/^["']|["']$/g, ''); // Remove quotes if present
217
+
218
+ // Validate it's in our list (exact match or ends with the path)
219
+ const matchedFile = candidateFiles.find(f =>
220
+ f === selectedPath ||
221
+ f.endsWith(selectedPath) ||
222
+ selectedPath.endsWith(f)
223
+ );
224
+
225
+ debugLog("Phase 3: LLM selected file from list", {
226
+ selectedPath,
227
+ matchedFile,
228
+ candidateCount: candidateFiles.length
229
+ });
230
+
231
+ return matchedFile || null;
232
+ } catch (e) {
233
+ debugLog("Phase 3: Failed to select file from list", { error: String(e) });
234
+ return null;
235
+ }
236
+ }
237
+
171
238
  /**
172
239
  * Search for component files by name across the project
173
240
  * Returns the file path if found, null otherwise
@@ -519,37 +586,23 @@ export async function POST(request: Request) {
519
586
  const searchResults = searchFilesSmart(analysis, projectRoot, 10);
520
587
  smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
521
588
 
522
- // Identify the recommended file (highest scoring file, prefer filename matches)
589
+ // PHASE 3: Ask LLM to pick the best file from actual file list
523
590
  if (searchResults.length > 0) {
524
- // Helper to extract filename without extension
525
- const getFilenameWithoutExt = (filePath: string): string => {
526
- const filename = filePath.split('/').pop() || '';
527
- return filename.replace(/\.(tsx?|jsx?)$/, '').toLowerCase();
528
- };
529
-
530
- // Find file where FILENAME matches a component name (not just path)
531
- const filenameMatch = searchResults.find(r => {
532
- const filename = getFilenameWithoutExt(r.path);
533
- return analysis.componentNames.some(name => {
534
- const nameLower = name.toLowerCase();
535
- // Exact match: ProcessDetailPanel === processdetailpanel
536
- // Or filename contains the component name
537
- return filename === nameLower || filename.includes(nameLower);
538
- });
539
- });
591
+ const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
592
+ const selectedFile = await selectBestFileFromList(
593
+ screenshot,
594
+ userPrompt,
595
+ candidateFiles,
596
+ apiKey
597
+ );
540
598
 
541
- if (filenameMatch) {
542
- const matchedName = analysis.componentNames.find(name => {
543
- const filename = getFilenameWithoutExt(filenameMatch.path);
544
- const nameLower = name.toLowerCase();
545
- return filename === nameLower || filename.includes(nameLower);
546
- });
599
+ if (selectedFile) {
547
600
  recommendedFile = {
548
- path: filenameMatch.path,
549
- reason: `Filename matches component "${matchedName}" from screenshot`
601
+ path: selectedFile,
602
+ reason: `LLM selected from ${candidateFiles.length} candidate files`
550
603
  };
551
604
  } else {
552
- // Fall back to highest score
605
+ // Fallback to highest score if LLM selection fails
553
606
  recommendedFile = {
554
607
  path: searchResults[0].path,
555
608
  reason: `Highest content match score (${searchResults[0].score} points)`
@@ -166,6 +166,73 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
166
166
  }
167
167
  }
168
168
 
169
+ /**
170
+ * Phase 3: Ask LLM to select the best file from actual file list
171
+ * This replaces guessing - the LLM sees the real filenames and picks one
172
+ */
173
+ async function selectBestFileFromList(
174
+ screenshot: string,
175
+ userPrompt: string,
176
+ candidateFiles: string[],
177
+ apiKey: string
178
+ ): Promise<string | null> {
179
+ if (candidateFiles.length === 0) return null;
180
+
181
+ const anthropic = new Anthropic({ apiKey });
182
+ const base64Data = screenshot.split(",")[1] || screenshot;
183
+
184
+ try {
185
+ const response = await anthropic.messages.create({
186
+ model: "claude-sonnet-4-20250514",
187
+ max_tokens: 256,
188
+ messages: [{
189
+ role: "user",
190
+ content: [
191
+ {
192
+ type: "image",
193
+ source: { type: "base64", media_type: "image/png", data: base64Data }
194
+ },
195
+ {
196
+ type: "text",
197
+ text: `The user wants to: "${userPrompt}"
198
+
199
+ Here are the actual component files found in this codebase:
200
+ ${candidateFiles.map((f, i) => `${i + 1}. ${f}`).join('\n')}
201
+
202
+ Looking at the screenshot, which file MOST LIKELY contains the UI elements the user wants to modify?
203
+
204
+ IMPORTANT: Return ONLY the exact file path from the list above (e.g., "components/ProcessCatalogue/ProcessDetailPanel.tsx").
205
+ Do not add any explanation or other text.`
206
+ }
207
+ ]
208
+ }]
209
+ });
210
+
211
+ const textBlock = response.content.find(b => b.type === "text");
212
+ if (!textBlock || textBlock.type !== "text") return null;
213
+
214
+ const selectedPath = textBlock.text.trim().replace(/^["']|["']$/g, ''); // Remove quotes if present
215
+
216
+ // Validate it's in our list (exact match or ends with the path)
217
+ const matchedFile = candidateFiles.find(f =>
218
+ f === selectedPath ||
219
+ f.endsWith(selectedPath) ||
220
+ selectedPath.endsWith(f)
221
+ );
222
+
223
+ debugLog("Phase 3: LLM selected file from list", {
224
+ selectedPath,
225
+ matchedFile,
226
+ candidateCount: candidateFiles.length
227
+ });
228
+
229
+ return matchedFile || null;
230
+ } catch (e) {
231
+ debugLog("Phase 3: Failed to select file from list", { error: String(e) });
232
+ return null;
233
+ }
234
+ }
235
+
169
236
  /**
170
237
  * Search for component files by name across the project (for smart search)
171
238
  * Returns the file path if found, null otherwise
@@ -529,37 +596,23 @@ export async function POST(request: Request) {
529
596
  const searchResults = searchFilesSmart(analysis, projectRoot, 10);
530
597
  smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
531
598
 
532
- // Identify the recommended file (highest scoring file, prefer filename matches)
599
+ // PHASE 3: Ask LLM to pick the best file from actual file list
533
600
  if (searchResults.length > 0) {
534
- // Helper to extract filename without extension
535
- const getFilenameWithoutExt = (filePath: string): string => {
536
- const filename = filePath.split('/').pop() || '';
537
- return filename.replace(/\.(tsx?|jsx?)$/, '').toLowerCase();
538
- };
539
-
540
- // Find file where FILENAME matches a component name (not just path)
541
- const filenameMatch = searchResults.find(r => {
542
- const filename = getFilenameWithoutExt(r.path);
543
- return analysis.componentNames.some(name => {
544
- const nameLower = name.toLowerCase();
545
- // Exact match: ProcessDetailPanel === processdetailpanel
546
- // Or filename contains the component name
547
- return filename === nameLower || filename.includes(nameLower);
548
- });
549
- });
601
+ const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
602
+ const selectedFile = await selectBestFileFromList(
603
+ screenshot,
604
+ userPrompt,
605
+ candidateFiles,
606
+ apiKey
607
+ );
550
608
 
551
- if (filenameMatch) {
552
- const matchedName = analysis.componentNames.find(name => {
553
- const filename = getFilenameWithoutExt(filenameMatch.path);
554
- const nameLower = name.toLowerCase();
555
- return filename === nameLower || filename.includes(nameLower);
556
- });
609
+ if (selectedFile) {
557
610
  recommendedFile = {
558
- path: filenameMatch.path,
559
- reason: `Filename matches component "${matchedName}" from screenshot`
611
+ path: selectedFile,
612
+ reason: `LLM selected from ${candidateFiles.length} candidate files`
560
613
  };
561
614
  } else {
562
- // Fall back to highest score
615
+ // Fallback to highest score if LLM selection fails
563
616
  recommendedFile = {
564
617
  path: searchResults[0].path,
565
618
  reason: `Highest content match score (${searchResults[0].score} points)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.44",
3
+ "version": "1.3.45",
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",