sonance-brand-mcp 1.3.43 → 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)`
@@ -608,7 +661,7 @@ export async function POST(request: Request) {
608
661
  // Total budget: 100k chars (~25k tokens, safe for Claude)
609
662
  // Priority: Recommended file (full) > Page file (limited) > Other components (dynamic)
610
663
  const TOTAL_CONTEXT_BUDGET = 100000;
611
- const MAX_RECOMMENDED_FILE = 50000; // 50k chars max for recommended file
664
+ const MAX_RECOMMENDED_FILE = 80000; // 80k chars max for recommended file
612
665
  const MAX_PAGE_FILE = 2000; // Page file is just a wrapper
613
666
  const MAX_GLOBALS_CSS = 1500;
614
667
  const MAX_FILES = 25;
@@ -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)`
@@ -618,7 +671,7 @@ export async function POST(request: Request) {
618
671
  // Total budget: 100k chars (~25k tokens, safe for Claude)
619
672
  // Priority: Recommended file (full) > Page file (limited) > Other components (dynamic)
620
673
  const TOTAL_CONTEXT_BUDGET = 100000;
621
- const MAX_RECOMMENDED_FILE = 50000; // 50k chars max for recommended file
674
+ const MAX_RECOMMENDED_FILE = 80000; // 80k chars max for recommended file
622
675
  const MAX_PAGE_FILE = 2000; // Page file is just a wrapper
623
676
  const MAX_GLOBALS_CSS = 1500;
624
677
  const MAX_FILES = 25;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.43",
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",