sonance-brand-mcp 1.3.44 → 1.3.46

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
@@ -408,7 +475,7 @@ Return search/replace patches (NOT full files). The system applies your patches
408
475
  - Blaze Blue: #00A3E1
409
476
 
410
477
  **RESPONSE FORMAT:**
411
- Return ONLY valid JSON:
478
+ CRITICAL: Return ONLY the JSON object below. Do NOT include any text, explanation, or thinking before or after the JSON. No preamble. No "Looking at the screenshot..." No markdown code blocks. Just raw JSON:
412
479
  {
413
480
  "reasoning": "What you understood from the request and your plan",
414
481
  "modifications": [
@@ -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
- };
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
+ );
529
598
 
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
- });
540
-
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)`
@@ -810,6 +863,15 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
810
863
  }
811
864
 
812
865
  jsonText = jsonText.trim();
866
+
867
+ // Robust JSON extraction: find the first { and last } to extract JSON object
868
+ // This handles cases where the LLM includes preamble text before the JSON
869
+ const firstBrace = jsonText.indexOf('{');
870
+ const lastBrace = jsonText.lastIndexOf('}');
871
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
872
+ jsonText = jsonText.substring(firstBrace, lastBrace + 1);
873
+ }
874
+
813
875
  aiResponse = JSON.parse(jsonText);
814
876
  } catch {
815
877
  console.error("Failed to parse AI response:", textResponse.text);
@@ -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
@@ -406,7 +473,7 @@ Return search/replace patches (NOT full files). The system applies your patches
406
473
  - Blaze Blue: #00A3E1
407
474
 
408
475
  **RESPONSE FORMAT:**
409
- Return ONLY valid JSON:
476
+ CRITICAL: Return ONLY the JSON object below. Do NOT include any text, explanation, or thinking before or after the JSON. No preamble. No "Looking at the screenshot..." No markdown code blocks. Just raw JSON:
410
477
  {
411
478
  "reasoning": "What you understood from the request and your plan",
412
479
  "modifications": [
@@ -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)`
@@ -825,6 +878,14 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
825
878
  // Clean up any remaining whitespace
826
879
  jsonText = jsonText.trim();
827
880
 
881
+ // Robust JSON extraction: find the first { and last } to extract JSON object
882
+ // This handles cases where the LLM includes preamble text before the JSON
883
+ const firstBrace = jsonText.indexOf('{');
884
+ const lastBrace = jsonText.lastIndexOf('}');
885
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
886
+ jsonText = jsonText.substring(firstBrace, lastBrace + 1);
887
+ }
888
+
828
889
  aiResponse = JSON.parse(jsonText);
829
890
  } catch {
830
891
  console.error("Failed to parse AI response:", textResponse.text);
@@ -2370,7 +2370,7 @@ export function SonanceDevTools() {
2370
2370
  <div
2371
2371
  ref={headerRef}
2372
2372
  className={cn(
2373
- "flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-[#333F48]",
2373
+ "flex items-center justify-between px-3 py-2 border-b border-gray-200 bg-[#333F48]",
2374
2374
  "cursor-move touch-none"
2375
2375
  )}
2376
2376
  onPointerDown={handleDragStart}
@@ -2380,10 +2380,10 @@ export function SonanceDevTools() {
2380
2380
  onDoubleClick={handleResetPosition}
2381
2381
  title="Drag to move • Double-click to reset position"
2382
2382
  >
2383
- <div className="flex items-center gap-2">
2384
- <GripHorizontal className="h-4 w-4 text-white/50" />
2385
- <Palette className="h-5 w-5 text-[#00A3E1]" />
2386
- <span id="span-sonance-devtools" className="text-sm font-semibold text-white">
2383
+ <div className="flex items-center gap-1.5">
2384
+ <GripHorizontal className="h-3.5 w-3.5 text-white/50" />
2385
+ <Palette className="h-4 w-4 text-[#00A3E1]" />
2386
+ <span id="span-sonance-devtools" className="text-xs font-semibold text-white whitespace-nowrap">
2387
2387
  Sonance DevTools
2388
2388
  </span>
2389
2389
  </div>
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.46",
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",