sonance-brand-mcp 1.3.74 → 1.3.76

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.
@@ -91,6 +91,39 @@ function debugLog(message: string, data?: unknown) {
91
91
  }
92
92
  }
93
93
 
94
+ /**
95
+ * Extract JSON from LLM response that may contain preamble text
96
+ * Handles: pure JSON, markdown code fences, and text with embedded JSON
97
+ */
98
+ function extractJsonFromResponse(text: string): string {
99
+ // Try direct parse first - if it starts with {, it's likely pure JSON
100
+ const trimmed = text.trim();
101
+ if (trimmed.startsWith('{')) return trimmed;
102
+
103
+ // Extract from markdown code fence (```json ... ``` or ``` ... ```)
104
+ const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
105
+ if (fenceMatch) {
106
+ const extracted = fenceMatch[1].trim();
107
+ debugLog("Extracted JSON from markdown fence", { previewLength: extracted.length });
108
+ return extracted;
109
+ }
110
+
111
+ // Find the JSON object in the text (for responses with preamble)
112
+ const jsonStart = text.indexOf('{');
113
+ const jsonEnd = text.lastIndexOf('}');
114
+ if (jsonStart !== -1 && jsonEnd > jsonStart) {
115
+ const extracted = text.slice(jsonStart, jsonEnd + 1);
116
+ debugLog("Extracted JSON from preamble text", {
117
+ preambleLength: jsonStart,
118
+ jsonLength: extracted.length
119
+ });
120
+ return extracted;
121
+ }
122
+
123
+ // Return as-is if no JSON structure found
124
+ return text;
125
+ }
126
+
94
127
  /**
95
128
  * AST-based syntax validation using Babel parser
96
129
  * This catches actual syntax errors, not just tag counting
@@ -177,6 +210,48 @@ interface ScreenshotAnalysis {
177
210
  visibleText: string[];
178
211
  componentNames: string[];
179
212
  codePatterns: string[];
213
+ elementTypes: string[]; // Detected UI element types: table, button, form, card, list, modal, dropdown, input
214
+ }
215
+
216
+ /**
217
+ * Map element types to JSX/HTML patterns to search for in files
218
+ * This is the key to Cursor-style file discovery - search for WHAT we see, not guessed names
219
+ */
220
+ const ELEMENT_PATTERNS: Record<string, string[]> = {
221
+ table: ['<Table', 'TableRow', 'TableCell', 'TableHeader', 'TableBody', '<table', '<tr', '<td', '<th'],
222
+ button: ['<Button', '<button', 'onClick={', 'handleClick'],
223
+ form: ['<Form', '<form', 'onSubmit', '<Input', '<input', 'handleSubmit'],
224
+ card: ['<Card', 'CardContent', 'CardHeader', 'CardTitle', 'CardDescription'],
225
+ list: ['<List', '<ul', '<li', 'ListItem', '.map(('],
226
+ modal: ['<Modal', '<Dialog', 'DialogContent', 'isOpen', 'onClose', 'onOpenChange'],
227
+ dropdown: ['<Dropdown', '<Select', 'DropdownMenu', 'SelectContent', 'SelectTrigger'],
228
+ input: ['<Input', '<input', '<Textarea', '<textarea', 'onChange={'],
229
+ header: ['<Header', '<header', '<Navbar', '<Nav', 'navigation'],
230
+ sidebar: ['<Sidebar', '<aside', 'SidebarContent', 'NavigationMenu'],
231
+ };
232
+
233
+ /**
234
+ * Score a file based on whether it contains patterns for a specific element type
235
+ * Higher scores indicate the file is more likely to contain the target element
236
+ */
237
+ function scoreFileByElementType(content: string, elementType: string): number {
238
+ const patterns = ELEMENT_PATTERNS[elementType.toLowerCase()] || [];
239
+ let score = 0;
240
+ let matchCount = 0;
241
+
242
+ for (const pattern of patterns) {
243
+ if (content.includes(pattern)) {
244
+ score += 300; // High score per pattern match
245
+ matchCount++;
246
+ }
247
+ }
248
+
249
+ // Bonus for multiple pattern matches (indicates primary component for this element type)
250
+ if (matchCount >= 3) {
251
+ score += 500;
252
+ }
253
+
254
+ return score;
180
255
  }
181
256
 
182
257
  /**
@@ -216,15 +291,17 @@ Analyze this UI to help find the correct source file. Return:
216
291
  1. **visibleText**: Extract the EXACT text visible in UI elements (button labels, headings, tab names, etc.)
217
292
  2. **componentNames**: Deduce likely React component names based on what you see (e.g., "ProcessDetailPanel", "UserSettings", "DataTable"). Think about common React naming conventions.
218
293
  3. **codePatterns**: Suggest code identifiers that might exist (e.g., "handleEdit", "isLoading", "activeTab", "onDelete")
294
+ 4. **elementTypes**: What TYPE of UI element is the user's request about? Pick from: table, button, form, card, list, modal, dropdown, input, header, sidebar. List the most relevant first.
219
295
 
220
296
  Return ONLY valid JSON:
221
297
  {
222
298
  "visibleText": ["Assets", "Flowchart", "Edit", "Delete"],
223
299
  "componentNames": ["ProcessDetailPanel", "ProcessActions", "QuickActions"],
224
- "codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"]
300
+ "codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"],
301
+ "elementTypes": ["table", "button"]
225
302
  }
226
303
 
227
- Be smart - use your knowledge of React patterns to make educated guesses about component and variable names.`,
304
+ Be smart - use your knowledge of React patterns to identify what type of element the user wants to modify.`,
228
305
  },
229
306
  ],
230
307
  },
@@ -234,7 +311,7 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
234
311
  try {
235
312
  const textBlock = response.content.find((block) => block.type === "text");
236
313
  if (!textBlock || textBlock.type !== "text") {
237
- return { visibleText: [], componentNames: [], codePatterns: [] };
314
+ return { visibleText: [], componentNames: [], codePatterns: [], elementTypes: [] };
238
315
  }
239
316
 
240
317
  let jsonText = textBlock.text.trim();
@@ -249,13 +326,14 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
249
326
  visibleText: parsed.visibleText || parsed.textStrings || [],
250
327
  componentNames: parsed.componentNames || [],
251
328
  codePatterns: parsed.codePatterns || [],
329
+ elementTypes: parsed.elementTypes || [],
252
330
  };
253
331
 
254
332
  debugLog("Phase 1: Analyzed screenshot for search", result);
255
333
  return result;
256
334
  } catch (e) {
257
335
  debugLog("Phase 1: Failed to parse screenshot analysis response", { error: String(e) });
258
- return { visibleText: [], componentNames: [], codePatterns: [] };
336
+ return { visibleText: [], componentNames: [], codePatterns: [], elementTypes: [] };
259
337
  }
260
338
  }
261
339
 
@@ -547,12 +625,18 @@ function searchFilesSmart(
547
625
  projectRoot: string,
548
626
  maxFiles: number = 10
549
627
  ): { path: string; content: string; score: number; filenameMatch: boolean }[] {
550
- const { visibleText, componentNames, codePatterns } = analysis;
628
+ const { visibleText, componentNames, codePatterns, elementTypes } = analysis;
551
629
 
552
630
  // Combine all search terms
553
631
  const allSearchTerms = [...visibleText, ...codePatterns];
554
632
 
555
- if (allSearchTerms.length === 0 && componentNames.length === 0) return [];
633
+ // Get the primary element type for pattern-based scoring (Cursor-style)
634
+ const primaryElementType = elementTypes?.[0] || null;
635
+ if (primaryElementType) {
636
+ debugLog("Phase 2: Element type detected for pattern matching", { primaryElementType });
637
+ }
638
+
639
+ if (allSearchTerms.length === 0 && componentNames.length === 0 && !primaryElementType) return [];
556
640
 
557
641
  const results: Map<string, { path: string; content: string; score: number; filenameMatch: boolean }> = new Map();
558
642
  const searchDirs = ["components", "src/components", "app", "src/app", "pages", "src/pages"];
@@ -619,23 +703,47 @@ function searchFilesSmart(
619
703
  }
620
704
  }
621
705
 
706
+ // Calculate content score based on text matches
707
+ let contentScore = 0;
622
708
  if (uniqueMatches > 0) {
623
709
  // Score: unique matches * 10 + total matches
624
- const contentScore = (uniqueMatches * 10) + totalMatches;
625
-
710
+ contentScore = (uniqueMatches * 10) + totalMatches;
711
+ }
712
+
713
+ // CURSOR-STYLE: Add element type pattern scoring
714
+ // Files containing patterns for the detected element type get a big boost
715
+ let elementTypeScore = 0;
716
+ if (primaryElementType) {
717
+ elementTypeScore = scoreFileByElementType(content, primaryElementType);
718
+ }
719
+
720
+ const totalScore = contentScore + elementTypeScore;
721
+
722
+ if (totalScore > 0) {
626
723
  // Check if we already have this file (from component name search)
627
724
  const existing = results.get(entryPath);
628
725
  if (existing) {
629
- // Add content score to existing filename match score
630
- existing.score += contentScore;
726
+ // Add scores to existing entry
727
+ existing.score += totalScore;
631
728
  } else {
632
729
  results.set(entryPath, {
633
730
  path: entryPath,
634
731
  content,
635
- score: contentScore,
732
+ score: totalScore,
636
733
  filenameMatch: false
637
734
  });
638
735
  }
736
+
737
+ // Log high-confidence element type matches
738
+ if (elementTypeScore >= 800) {
739
+ debugLog("Phase 2: High-confidence element type match", {
740
+ file: entryPath,
741
+ elementType: primaryElementType,
742
+ elementScore: elementTypeScore,
743
+ contentScore,
744
+ totalScore
745
+ });
746
+ }
639
747
  }
640
748
  } catch {
641
749
  // Skip files that can't be read
@@ -673,7 +781,10 @@ function searchFilesSmart(
673
781
 
674
782
  const VISION_SYSTEM_PROMPT = `You are an expert frontend developer. Edit the code to fulfill the user's request.
675
783
 
676
- Output ONLY this JSON (no other text):
784
+ CRITICAL: Return ONLY the JSON object. No explanations, no preamble, no markdown code fences.
785
+ Start your response with { and end with }
786
+
787
+ Output format:
677
788
  {"modifications":[{"filePath":"path","patches":[{"search":"exact original code","replace":"modified code"}]}]}
678
789
 
679
790
  The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
@@ -958,7 +1069,7 @@ User Request: "${userPrompt}"
958
1069
 
959
1070
  // Search for element IDs in the file to enable precise targeting
960
1071
  let idMatch: { lineNumber: number; matchedId: string; snippet: string } | null = null;
961
- if (focusedElements && focusedElements.length > 0) {
1072
+ if (focusedElements && focusedElements.length > 0) {
962
1073
  for (const el of focusedElements) {
963
1074
  idMatch = findElementIdInFile(content, el.elementId, el.childIds);
964
1075
  if (idMatch) break;
@@ -1049,7 +1160,7 @@ ${linesWithNumbers}
1049
1160
  // Only include the variants/props section, not the whole file
1050
1161
  const variantsMatch = comp.content.match(/variants:\s*\{[\s\S]*?\n\s*\},\n\s*defaultVariants/);
1051
1162
  if (variantsMatch) {
1052
- textContent += `
1163
+ textContent += `
1053
1164
  --- UI Component: ${comp.path} (variants only) ---
1054
1165
  ${variantsMatch[0]}
1055
1166
  ---
@@ -1123,7 +1234,7 @@ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exa
1123
1234
 
1124
1235
  // Call Claude Vision API with retry mechanism
1125
1236
  const anthropic = new Anthropic({ apiKey });
1126
-
1237
+
1127
1238
  // Build set of valid file paths from page context (needed for validation)
1128
1239
  const validFilePaths = new Set<string>();
1129
1240
  if (pageContext.pageFile) {
@@ -1220,62 +1331,8 @@ This is better than generating patches with made-up code.`,
1220
1331
  };
1221
1332
 
1222
1333
  try {
1223
- let jsonText = textResponse.text.trim();
1224
-
1225
- // Try to extract JSON from markdown code blocks
1226
- const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
1227
- jsonText.match(/```\n([\s\S]*?)\n```/);
1228
-
1229
- if (jsonMatch) {
1230
- jsonText = jsonMatch[1];
1231
- } else if (jsonText.includes("```json")) {
1232
- const start = jsonText.indexOf("```json") + 7;
1233
- const end = jsonText.lastIndexOf("```");
1234
- if (end > start) {
1235
- jsonText = jsonText.substring(start, end);
1236
- }
1237
- }
1238
-
1239
- jsonText = jsonText.trim();
1240
-
1241
- // Robust JSON extraction: look for {"modifications" pattern specifically
1242
- // This handles cases where the LLM includes preamble text with code blocks
1243
- const jsonStartPatterns = ['{"modifications"', '{ "modifications"', '{\n "modifications"'];
1244
- let jsonStart = -1;
1245
-
1246
- for (const pattern of jsonStartPatterns) {
1247
- const idx = jsonText.indexOf(pattern);
1248
- if (idx !== -1 && (jsonStart === -1 || idx < jsonStart)) {
1249
- jsonStart = idx;
1250
- }
1251
- }
1252
-
1253
- if (jsonStart !== -1) {
1254
- // Find the matching closing brace by counting braces
1255
- let braceCount = 0;
1256
- let jsonEnd = -1;
1257
- for (let i = jsonStart; i < jsonText.length; i++) {
1258
- if (jsonText[i] === '{') braceCount++;
1259
- if (jsonText[i] === '}') {
1260
- braceCount--;
1261
- if (braceCount === 0) {
1262
- jsonEnd = i;
1263
- break;
1264
- }
1265
- }
1266
- }
1267
- if (jsonEnd !== -1) {
1268
- jsonText = jsonText.substring(jsonStart, jsonEnd + 1);
1269
- }
1270
- } else {
1271
- // Fallback: try first { to last }
1272
- const firstBrace = jsonText.indexOf('{');
1273
- const lastBrace = jsonText.lastIndexOf('}');
1274
- if (firstBrace !== -1 && lastBrace > firstBrace) {
1275
- jsonText = jsonText.substring(firstBrace, lastBrace + 1);
1276
- }
1277
- }
1278
-
1334
+ // Use robust JSON extraction that handles preamble text, code fences, etc.
1335
+ const jsonText = extractJsonFromResponse(textResponse.text);
1279
1336
  aiResponse = JSON.parse(jsonText);
1280
1337
  } catch {
1281
1338
  console.error("Failed to parse AI response:", textResponse.text);
@@ -87,6 +87,39 @@ function debugLog(message: string, data?: unknown) {
87
87
  }
88
88
  }
89
89
 
90
+ /**
91
+ * Extract JSON from LLM response that may contain preamble text
92
+ * Handles: pure JSON, markdown code fences, and text with embedded JSON
93
+ */
94
+ function extractJsonFromResponse(text: string): string {
95
+ // Try direct parse first - if it starts with {, it's likely pure JSON
96
+ const trimmed = text.trim();
97
+ if (trimmed.startsWith('{')) return trimmed;
98
+
99
+ // Extract from markdown code fence (```json ... ``` or ``` ... ```)
100
+ const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
101
+ if (fenceMatch) {
102
+ const extracted = fenceMatch[1].trim();
103
+ debugLog("Extracted JSON from markdown fence", { previewLength: extracted.length });
104
+ return extracted;
105
+ }
106
+
107
+ // Find the JSON object in the text (for responses with preamble)
108
+ const jsonStart = text.indexOf('{');
109
+ const jsonEnd = text.lastIndexOf('}');
110
+ if (jsonStart !== -1 && jsonEnd > jsonStart) {
111
+ const extracted = text.slice(jsonStart, jsonEnd + 1);
112
+ debugLog("Extracted JSON from preamble text", {
113
+ preambleLength: jsonStart,
114
+ jsonLength: extracted.length
115
+ });
116
+ return extracted;
117
+ }
118
+
119
+ // Return as-is if no JSON structure found
120
+ return text;
121
+ }
122
+
90
123
  /**
91
124
  * AST-based syntax validation using Babel parser
92
125
  * This catches actual syntax errors, not just tag counting
@@ -173,6 +206,48 @@ interface ScreenshotAnalysis {
173
206
  visibleText: string[];
174
207
  componentNames: string[];
175
208
  codePatterns: string[];
209
+ elementTypes: string[]; // Detected UI element types: table, button, form, card, list, modal, dropdown, input
210
+ }
211
+
212
+ /**
213
+ * Map element types to JSX/HTML patterns to search for in files
214
+ * This is the key to Cursor-style file discovery - search for WHAT we see, not guessed names
215
+ */
216
+ const ELEMENT_PATTERNS: Record<string, string[]> = {
217
+ table: ['<Table', 'TableRow', 'TableCell', 'TableHeader', 'TableBody', '<table', '<tr', '<td', '<th'],
218
+ button: ['<Button', '<button', 'onClick={', 'handleClick'],
219
+ form: ['<Form', '<form', 'onSubmit', '<Input', '<input', 'handleSubmit'],
220
+ card: ['<Card', 'CardContent', 'CardHeader', 'CardTitle', 'CardDescription'],
221
+ list: ['<List', '<ul', '<li', 'ListItem', '.map(('],
222
+ modal: ['<Modal', '<Dialog', 'DialogContent', 'isOpen', 'onClose', 'onOpenChange'],
223
+ dropdown: ['<Dropdown', '<Select', 'DropdownMenu', 'SelectContent', 'SelectTrigger'],
224
+ input: ['<Input', '<input', '<Textarea', '<textarea', 'onChange={'],
225
+ header: ['<Header', '<header', '<Navbar', '<Nav', 'navigation'],
226
+ sidebar: ['<Sidebar', '<aside', 'SidebarContent', 'NavigationMenu'],
227
+ };
228
+
229
+ /**
230
+ * Score a file based on whether it contains patterns for a specific element type
231
+ * Higher scores indicate the file is more likely to contain the target element
232
+ */
233
+ function scoreFileByElementType(content: string, elementType: string): number {
234
+ const patterns = ELEMENT_PATTERNS[elementType.toLowerCase()] || [];
235
+ let score = 0;
236
+ let matchCount = 0;
237
+
238
+ for (const pattern of patterns) {
239
+ if (content.includes(pattern)) {
240
+ score += 300; // High score per pattern match
241
+ matchCount++;
242
+ }
243
+ }
244
+
245
+ // Bonus for multiple pattern matches (indicates primary component for this element type)
246
+ if (matchCount >= 3) {
247
+ score += 500;
248
+ }
249
+
250
+ return score;
176
251
  }
177
252
 
178
253
  /**
@@ -212,15 +287,17 @@ Analyze this UI to help find the correct source file. Return:
212
287
  1. **visibleText**: Extract the EXACT text visible in UI elements (button labels, headings, tab names, etc.)
213
288
  2. **componentNames**: Deduce likely React component names based on what you see (e.g., "ProcessDetailPanel", "UserSettings", "DataTable"). Think about common React naming conventions.
214
289
  3. **codePatterns**: Suggest code identifiers that might exist (e.g., "handleEdit", "isLoading", "activeTab", "onDelete")
290
+ 4. **elementTypes**: What TYPE of UI element is the user's request about? Pick from: table, button, form, card, list, modal, dropdown, input, header, sidebar. List the most relevant first.
215
291
 
216
292
  Return ONLY valid JSON:
217
293
  {
218
294
  "visibleText": ["Assets", "Flowchart", "Edit", "Delete"],
219
295
  "componentNames": ["ProcessDetailPanel", "ProcessActions", "QuickActions"],
220
- "codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"]
296
+ "codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"],
297
+ "elementTypes": ["table", "button"]
221
298
  }
222
299
 
223
- Be smart - use your knowledge of React patterns to make educated guesses about component and variable names.`,
300
+ Be smart - use your knowledge of React patterns to identify what type of element the user wants to modify.`,
224
301
  },
225
302
  ],
226
303
  },
@@ -230,7 +307,7 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
230
307
  try {
231
308
  const textBlock = response.content.find((block) => block.type === "text");
232
309
  if (!textBlock || textBlock.type !== "text") {
233
- return { visibleText: [], componentNames: [], codePatterns: [] };
310
+ return { visibleText: [], componentNames: [], codePatterns: [], elementTypes: [] };
234
311
  }
235
312
 
236
313
  let jsonText = textBlock.text.trim();
@@ -245,13 +322,14 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
245
322
  visibleText: parsed.visibleText || parsed.textStrings || [],
246
323
  componentNames: parsed.componentNames || [],
247
324
  codePatterns: parsed.codePatterns || [],
325
+ elementTypes: parsed.elementTypes || [],
248
326
  };
249
327
 
250
328
  debugLog("Phase 1: Analyzed screenshot for search", result);
251
329
  return result;
252
330
  } catch (e) {
253
331
  debugLog("Phase 1: Failed to parse screenshot analysis response", { error: String(e) });
254
- return { visibleText: [], componentNames: [], codePatterns: [] };
332
+ return { visibleText: [], componentNames: [], codePatterns: [], elementTypes: [] };
255
333
  }
256
334
  }
257
335
 
@@ -543,12 +621,18 @@ function searchFilesSmart(
543
621
  projectRoot: string,
544
622
  maxFiles: number = 10
545
623
  ): { path: string; content: string; score: number; filenameMatch: boolean }[] {
546
- const { visibleText, componentNames, codePatterns } = analysis;
624
+ const { visibleText, componentNames, codePatterns, elementTypes } = analysis;
547
625
 
548
626
  // Combine all search terms
549
627
  const allSearchTerms = [...visibleText, ...codePatterns];
550
628
 
551
- if (allSearchTerms.length === 0 && componentNames.length === 0) return [];
629
+ // Get the primary element type for pattern-based scoring (Cursor-style)
630
+ const primaryElementType = elementTypes?.[0] || null;
631
+ if (primaryElementType) {
632
+ debugLog("Phase 2: Element type detected for pattern matching", { primaryElementType });
633
+ }
634
+
635
+ if (allSearchTerms.length === 0 && componentNames.length === 0 && !primaryElementType) return [];
552
636
 
553
637
  const results: Map<string, { path: string; content: string; score: number; filenameMatch: boolean }> = new Map();
554
638
  const searchDirs = ["components", "src/components", "app", "src/app", "pages", "src/pages"];
@@ -615,23 +699,47 @@ function searchFilesSmart(
615
699
  }
616
700
  }
617
701
 
702
+ // Calculate content score based on text matches
703
+ let contentScore = 0;
618
704
  if (uniqueMatches > 0) {
619
705
  // Score: unique matches * 10 + total matches
620
- const contentScore = (uniqueMatches * 10) + totalMatches;
621
-
706
+ contentScore = (uniqueMatches * 10) + totalMatches;
707
+ }
708
+
709
+ // CURSOR-STYLE: Add element type pattern scoring
710
+ // Files containing patterns for the detected element type get a big boost
711
+ let elementTypeScore = 0;
712
+ if (primaryElementType) {
713
+ elementTypeScore = scoreFileByElementType(content, primaryElementType);
714
+ }
715
+
716
+ const totalScore = contentScore + elementTypeScore;
717
+
718
+ if (totalScore > 0) {
622
719
  // Check if we already have this file (from component name search)
623
720
  const existing = results.get(entryPath);
624
721
  if (existing) {
625
- // Add content score to existing filename match score
626
- existing.score += contentScore;
722
+ // Add scores to existing entry
723
+ existing.score += totalScore;
627
724
  } else {
628
725
  results.set(entryPath, {
629
726
  path: entryPath,
630
727
  content,
631
- score: contentScore,
728
+ score: totalScore,
632
729
  filenameMatch: false
633
730
  });
634
731
  }
732
+
733
+ // Log high-confidence element type matches
734
+ if (elementTypeScore >= 800) {
735
+ debugLog("Phase 2: High-confidence element type match", {
736
+ file: entryPath,
737
+ elementType: primaryElementType,
738
+ elementScore: elementTypeScore,
739
+ contentScore,
740
+ totalScore
741
+ });
742
+ }
635
743
  }
636
744
  } catch {
637
745
  // Skip files that can't be read
@@ -669,7 +777,10 @@ function searchFilesSmart(
669
777
 
670
778
  const VISION_SYSTEM_PROMPT = `You are an expert frontend developer. Edit the code to fulfill the user's request.
671
779
 
672
- Output ONLY this JSON (no other text):
780
+ CRITICAL: Return ONLY the JSON object. No explanations, no preamble, no markdown code fences.
781
+ Start your response with { and end with }
782
+
783
+ Output format:
673
784
  {"modifications":[{"filePath":"path","patches":[{"search":"exact original code","replace":"modified code"}]}]}
674
785
 
675
786
  The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
@@ -927,7 +1038,7 @@ User Request: "${userPrompt}"
927
1038
 
928
1039
  // Search for element IDs in the file to enable precise targeting
929
1040
  let idMatch: { lineNumber: number; matchedId: string; snippet: string } | null = null;
930
- if (focusedElements && focusedElements.length > 0) {
1041
+ if (focusedElements && focusedElements.length > 0) {
931
1042
  for (const el of focusedElements) {
932
1043
  idMatch = findElementIdInFile(content, el.elementId, el.childIds);
933
1044
  if (idMatch) break;
@@ -1018,7 +1129,7 @@ ${linesWithNumbers}
1018
1129
  // Only include the variants/props section, not the whole file
1019
1130
  const variantsMatch = comp.content.match(/variants:\s*\{[\s\S]*?\n\s*\},\n\s*defaultVariants/);
1020
1131
  if (variantsMatch) {
1021
- textContent += `
1132
+ textContent += `
1022
1133
  --- UI Component: ${comp.path} (variants only) ---
1023
1134
  ${variantsMatch[0]}
1024
1135
  ---
@@ -1092,7 +1203,7 @@ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exa
1092
1203
 
1093
1204
  // Call Claude Vision API with retry mechanism
1094
1205
  const anthropic = new Anthropic({ apiKey });
1095
-
1206
+
1096
1207
  // Build list of known file paths (for logging)
1097
1208
  const knownPaths = new Set<string>();
1098
1209
  if (pageContext.pageFile) {
@@ -1185,64 +1296,8 @@ This is better than generating patches with made-up code.`,
1185
1296
  };
1186
1297
 
1187
1298
  try {
1188
- let jsonText = textResponse.text.trim();
1189
-
1190
- // Try to extract JSON from markdown code blocks
1191
- const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
1192
- jsonText.match(/```\n([\s\S]*?)\n```/);
1193
-
1194
- if (jsonMatch) {
1195
- jsonText = jsonMatch[1];
1196
- } else if (jsonText.includes("```json")) {
1197
- // Fallback for cases where regex might miss due to newlines
1198
- const start = jsonText.indexOf("```json") + 7;
1199
- const end = jsonText.lastIndexOf("```");
1200
- if (end > start) {
1201
- jsonText = jsonText.substring(start, end);
1202
- }
1203
- }
1204
-
1205
- // Clean up any remaining whitespace
1206
- jsonText = jsonText.trim();
1207
-
1208
- // Robust JSON extraction: look for {"modifications" pattern specifically
1209
- // This handles cases where the LLM includes preamble text with code blocks
1210
- const jsonStartPatterns = ['{"modifications"', '{ "modifications"', '{\n "modifications"'];
1211
- let jsonStart = -1;
1212
-
1213
- for (const pattern of jsonStartPatterns) {
1214
- const idx = jsonText.indexOf(pattern);
1215
- if (idx !== -1 && (jsonStart === -1 || idx < jsonStart)) {
1216
- jsonStart = idx;
1217
- }
1218
- }
1219
-
1220
- if (jsonStart !== -1) {
1221
- // Find the matching closing brace by counting braces
1222
- let braceCount = 0;
1223
- let jsonEnd = -1;
1224
- for (let i = jsonStart; i < jsonText.length; i++) {
1225
- if (jsonText[i] === '{') braceCount++;
1226
- if (jsonText[i] === '}') {
1227
- braceCount--;
1228
- if (braceCount === 0) {
1229
- jsonEnd = i;
1230
- break;
1231
- }
1232
- }
1233
- }
1234
- if (jsonEnd !== -1) {
1235
- jsonText = jsonText.substring(jsonStart, jsonEnd + 1);
1236
- }
1237
- } else {
1238
- // Fallback: try first { to last }
1239
- const firstBrace = jsonText.indexOf('{');
1240
- const lastBrace = jsonText.lastIndexOf('}');
1241
- if (firstBrace !== -1 && lastBrace > firstBrace) {
1242
- jsonText = jsonText.substring(firstBrace, lastBrace + 1);
1243
- }
1244
- }
1245
-
1299
+ // Use robust JSON extraction that handles preamble text, code fences, etc.
1300
+ const jsonText = extractJsonFromResponse(textResponse.text);
1246
1301
  aiResponse = JSON.parse(jsonText);
1247
1302
  } catch {
1248
1303
  console.error("Failed to parse AI response:", textResponse.text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.74",
3
+ "version": "1.3.76",
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",