sonance-brand-mcp 1.3.71 → 1.3.72

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.
@@ -34,6 +34,10 @@ interface VisionFocusedElement {
34
34
  textContent?: string;
35
35
  /** The className string of the element for pattern matching */
36
36
  className?: string;
37
+ /** The element's DOM id attribute for precise code targeting */
38
+ elementId?: string;
39
+ /** IDs of child elements for more precise targeting */
40
+ childIds?: string[];
37
41
  }
38
42
 
39
43
  interface VisionFileModification {
@@ -120,6 +124,52 @@ function validateSyntaxWithAST(content: string, filePath: string): { valid: bool
120
124
  }
121
125
  }
122
126
 
127
+ /**
128
+ * Search for element ID in file content and return line number
129
+ * This enables precise targeting of the exact element the user clicked
130
+ */
131
+ function findElementIdInFile(
132
+ fileContent: string,
133
+ elementId: string | undefined,
134
+ childIds: string[] | undefined
135
+ ): { lineNumber: number; matchedId: string; snippet: string } | null {
136
+ if (!fileContent) return null;
137
+
138
+ const lines = fileContent.split('\n');
139
+
140
+ // Try the element's own ID first
141
+ if (elementId) {
142
+ const pattern = new RegExp(`id=["'\`]${elementId}["'\`]`);
143
+ for (let i = 0; i < lines.length; i++) {
144
+ if (pattern.test(lines[i])) {
145
+ return {
146
+ lineNumber: i + 1,
147
+ matchedId: elementId,
148
+ snippet: lines.slice(Math.max(0, i - 2), i + 5).join('\n'),
149
+ };
150
+ }
151
+ }
152
+ }
153
+
154
+ // Try child IDs
155
+ if (childIds && childIds.length > 0) {
156
+ for (const childId of childIds) {
157
+ const pattern = new RegExp(`id=["'\`]${childId}["'\`]`);
158
+ for (let i = 0; i < lines.length; i++) {
159
+ if (pattern.test(lines[i])) {
160
+ return {
161
+ lineNumber: i + 1,
162
+ matchedId: childId,
163
+ snippet: lines.slice(Math.max(0, i - 2), i + 5).join('\n'),
164
+ };
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ return null;
171
+ }
172
+
123
173
  /**
124
174
  * Result of LLM screenshot analysis for smart file discovery
125
175
  */
@@ -269,6 +319,28 @@ function findFilesContainingElement(
269
319
  // not the focused component the user is looking at.
270
320
  // Instead, we rely on Phase 2a matches and textContent matching.
271
321
 
322
+ // HIGHEST VALUE: Element ID match - this is THE file if it contains the exact ID
323
+ // IDs are unique identifiers and provide the most reliable targeting
324
+ if (el.elementId) {
325
+ const idPattern = new RegExp(`id=["'\`]${el.elementId}["'\`]`);
326
+ if (idPattern.test(content)) {
327
+ score += 500; // VERY HIGH - exact ID match is definitive!
328
+ matches.push(`contains id="${el.elementId}"`);
329
+ }
330
+ }
331
+
332
+ // Also check child IDs
333
+ if (el.childIds && el.childIds.length > 0) {
334
+ for (const childId of el.childIds) {
335
+ const idPattern = new RegExp(`id=["'\`]${childId}["'\`]`);
336
+ if (idPattern.test(content)) {
337
+ score += 400; // Very high - child ID match is also definitive
338
+ matches.push(`contains child id="${childId}"`);
339
+ break; // Only count once
340
+ }
341
+ }
342
+ }
343
+
272
344
  // VERY HIGH VALUE: Exact text content match (e.g., button says "REFRESH")
273
345
  // This is THE file if it contains the exact text the user clicked on
274
346
  if (el.textContent && el.textContent.length >= 2) {
@@ -599,17 +671,12 @@ function searchFilesSmart(
599
671
  return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score, filenameMatch: r.filenameMatch }));
600
672
  }
601
673
 
602
- const VISION_SYSTEM_PROMPT = `You edit code. Return ONLY valid JSON - no explanation, no preamble, no markdown.
674
+ const VISION_SYSTEM_PROMPT = `You are an expert frontend developer. Edit the code to fulfill the user's request.
603
675
 
604
- RULES:
605
- 1. Make ONLY the exact change the user requested. Do not modify unrelated properties.
606
- 2. Copy code EXACTLY from the file - character for character
607
- 3. Make the SMALLEST possible change
608
- 4. For color changes, just change the className
609
- 5. Do not restructure or reorganize code
676
+ Output ONLY this JSON (no other text):
677
+ {"modifications":[{"filePath":"path","patches":[{"search":"exact original code","replace":"modified code"}]}]}
610
678
 
611
- RESPOND WITH ONLY THIS JSON FORMAT (nothing else):
612
- {"modifications":[{"filePath":"path","patches":[{"search":"exact code","replace":"changed code"}]}]}`;
679
+ The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
613
680
 
614
681
  export async function POST(request: Request) {
615
682
  // Only allow in development
@@ -864,10 +931,15 @@ export async function POST(request: Request) {
864
931
  // Extract recommended file from component sources (to show first, avoid duplication)
865
932
  let recommendedFileContent: { path: string; content: string } | null = null;
866
933
  if (recommendedFile) {
934
+ // Check componentSources first
867
935
  const idx = pageContext.componentSources.findIndex(c => c.path === recommendedFile.path);
868
936
  if (idx !== -1) {
869
937
  recommendedFileContent = pageContext.componentSources[idx];
870
938
  pageContext.componentSources.splice(idx, 1); // Remove to avoid duplication
939
+ }
940
+ // Fallback: Check if it's the page file itself
941
+ else if (pageContext.pageFile === recommendedFile.path && pageContext.pageContent) {
942
+ recommendedFileContent = { path: pageContext.pageFile, content: pageContext.pageContent };
871
943
  }
872
944
  }
873
945
 
@@ -879,18 +951,50 @@ User Request: "${userPrompt}"
879
951
 
880
952
  `;
881
953
 
882
- if (focusedElements && focusedElements.length > 0) {
883
- textContent += `FOCUSED ELEMENTS (user clicked on these):
884
- ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
885
-
886
- `;
887
- }
888
-
889
954
  // ========== TARGET COMPONENT ONLY (with line numbers) ==========
890
955
  // CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
891
956
  if (recommendedFileContent) {
892
957
  const content = recommendedFileContent.content;
893
958
 
959
+ // Search for element IDs in the file to enable precise targeting
960
+ let idMatch: { lineNumber: number; matchedId: string; snippet: string } | null = null;
961
+ if (focusedElements && focusedElements.length > 0) {
962
+ for (const el of focusedElements) {
963
+ idMatch = findElementIdInFile(content, el.elementId, el.childIds);
964
+ if (idMatch) break;
965
+ }
966
+ }
967
+
968
+ // Build focused elements section with precise targeting info
969
+ if (focusedElements && focusedElements.length > 0) {
970
+ textContent += `FOCUSED ELEMENTS (user clicked on these):\n`;
971
+ for (const el of focusedElements) {
972
+ textContent += `- ${el.name} (${el.type})`;
973
+ if (el.textContent) {
974
+ textContent += ` with text "${el.textContent.substring(0, 30)}"`;
975
+ }
976
+ textContent += `\n`;
977
+ }
978
+
979
+ // Add precise targeting if we found an ID match
980
+ if (idMatch) {
981
+ textContent += `
982
+ PRECISE TARGET (found by element ID):
983
+ → ID: "${idMatch.matchedId}"
984
+ → Line: ${idMatch.lineNumber}
985
+ → Look for this ID in the code and modify the element that contains it.
986
+
987
+ `;
988
+ debugLog("Found element ID in file", {
989
+ matchedId: idMatch.matchedId,
990
+ lineNumber: idMatch.lineNumber,
991
+ file: recommendedFileContent.path,
992
+ });
993
+ } else {
994
+ textContent += `\n`;
995
+ }
996
+ }
997
+
894
998
  // Add line numbers to make it easy for LLM to reference exact code
895
999
  const linesWithNumbers = content.split('\n').map((line, i) =>
896
1000
  `${String(i + 1).padStart(4, ' ')}| ${line}`
@@ -933,8 +1037,33 @@ ${linesWithNumbers}
933
1037
  usedContext += content.length;
934
1038
  }
935
1039
 
936
- // NOTE: We intentionally skip SUPPORTING COMPONENTS to reduce noise
937
- // The LLM only needs the TARGET file to make accurate edits
1040
+ // ========== KEY UI COMPONENTS ==========
1041
+ // Include UI component definitions (Button, Card, etc.) so LLM knows available variants/props
1042
+ const uiComponentPaths = ['components/ui/button', 'components/ui/card', 'components/ui/input'];
1043
+ const includedUIComponents: string[] = [];
1044
+
1045
+ for (const comp of pageContext.componentSources) {
1046
+ // Check if this is a UI component we should include
1047
+ const isUIComponent = uiComponentPaths.some(uiPath => comp.path.includes(uiPath));
1048
+ if (isUIComponent && usedContext + comp.content.length < TOTAL_CONTEXT_BUDGET) {
1049
+ // Only include the variants/props section, not the whole file
1050
+ const variantsMatch = comp.content.match(/variants:\s*\{[\s\S]*?\n\s*\},\n\s*defaultVariants/);
1051
+ if (variantsMatch) {
1052
+ textContent += `
1053
+ --- UI Component: ${comp.path} (variants only) ---
1054
+ ${variantsMatch[0]}
1055
+ ---
1056
+
1057
+ `;
1058
+ includedUIComponents.push(comp.path);
1059
+ usedContext += variantsMatch[0].length + 100;
1060
+ }
1061
+ }
1062
+ }
1063
+
1064
+ if (includedUIComponents.length > 0) {
1065
+ debugLog("Included UI component definitions", { components: includedUIComponents });
1066
+ }
938
1067
 
939
1068
  // ========== THEME DISCOVERY ==========
940
1069
  // Discover theme tokens and contrast analysis to help LLM make informed color decisions
@@ -33,6 +33,10 @@ interface VisionFocusedElement {
33
33
  textContent?: string;
34
34
  /** The className string of the element for pattern matching */
35
35
  className?: string;
36
+ /** The element's DOM id attribute for precise code targeting */
37
+ elementId?: string;
38
+ /** IDs of child elements for more precise targeting */
39
+ childIds?: string[];
36
40
  }
37
41
 
38
42
  interface VisionFileModification {
@@ -116,6 +120,52 @@ function validateSyntaxWithAST(content: string, filePath: string): { valid: bool
116
120
  }
117
121
  }
118
122
 
123
+ /**
124
+ * Search for element ID in file content and return line number
125
+ * This enables precise targeting of the exact element the user clicked
126
+ */
127
+ function findElementIdInFile(
128
+ fileContent: string,
129
+ elementId: string | undefined,
130
+ childIds: string[] | undefined
131
+ ): { lineNumber: number; matchedId: string; snippet: string } | null {
132
+ if (!fileContent) return null;
133
+
134
+ const lines = fileContent.split('\n');
135
+
136
+ // Try the element's own ID first
137
+ if (elementId) {
138
+ const pattern = new RegExp(`id=["'\`]${elementId}["'\`]`);
139
+ for (let i = 0; i < lines.length; i++) {
140
+ if (pattern.test(lines[i])) {
141
+ return {
142
+ lineNumber: i + 1,
143
+ matchedId: elementId,
144
+ snippet: lines.slice(Math.max(0, i - 2), i + 5).join('\n'),
145
+ };
146
+ }
147
+ }
148
+ }
149
+
150
+ // Try child IDs
151
+ if (childIds && childIds.length > 0) {
152
+ for (const childId of childIds) {
153
+ const pattern = new RegExp(`id=["'\`]${childId}["'\`]`);
154
+ for (let i = 0; i < lines.length; i++) {
155
+ if (pattern.test(lines[i])) {
156
+ return {
157
+ lineNumber: i + 1,
158
+ matchedId: childId,
159
+ snippet: lines.slice(Math.max(0, i - 2), i + 5).join('\n'),
160
+ };
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ return null;
167
+ }
168
+
119
169
  /**
120
170
  * Result of LLM screenshot analysis for smart file discovery
121
171
  */
@@ -265,6 +315,28 @@ function findFilesContainingElement(
265
315
  // not the focused component the user is looking at.
266
316
  // Instead, we rely on Phase 2a matches and textContent matching.
267
317
 
318
+ // HIGHEST VALUE: Element ID match - this is THE file if it contains the exact ID
319
+ // IDs are unique identifiers and provide the most reliable targeting
320
+ if (el.elementId) {
321
+ const idPattern = new RegExp(`id=["'\`]${el.elementId}["'\`]`);
322
+ if (idPattern.test(content)) {
323
+ score += 500; // VERY HIGH - exact ID match is definitive!
324
+ matches.push(`contains id="${el.elementId}"`);
325
+ }
326
+ }
327
+
328
+ // Also check child IDs
329
+ if (el.childIds && el.childIds.length > 0) {
330
+ for (const childId of el.childIds) {
331
+ const idPattern = new RegExp(`id=["'\`]${childId}["'\`]`);
332
+ if (idPattern.test(content)) {
333
+ score += 400; // Very high - child ID match is also definitive
334
+ matches.push(`contains child id="${childId}"`);
335
+ break; // Only count once
336
+ }
337
+ }
338
+ }
339
+
268
340
  // VERY HIGH VALUE: Exact text content match (e.g., button says "REFRESH")
269
341
  // This is THE file if it contains the exact text the user clicked on
270
342
  if (el.textContent && el.textContent.length >= 2) {
@@ -595,17 +667,12 @@ function searchFilesSmart(
595
667
  return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score, filenameMatch: r.filenameMatch }));
596
668
  }
597
669
 
598
- const VISION_SYSTEM_PROMPT = `You edit code. Return ONLY valid JSON - no explanation, no preamble, no markdown.
670
+ const VISION_SYSTEM_PROMPT = `You are an expert frontend developer. Edit the code to fulfill the user's request.
599
671
 
600
- RULES:
601
- 1. Make ONLY the exact change the user requested. Do not modify unrelated properties.
602
- 2. Copy code EXACTLY from the file - character for character
603
- 3. Make the SMALLEST possible change
604
- 4. For color changes, just change the className
605
- 5. Do not restructure or reorganize code
672
+ Output ONLY this JSON (no other text):
673
+ {"modifications":[{"filePath":"path","patches":[{"search":"exact original code","replace":"modified code"}]}]}
606
674
 
607
- RESPOND WITH ONLY THIS JSON FORMAT (nothing else):
608
- {"modifications":[{"filePath":"path","patches":[{"search":"exact code","replace":"changed code"}]}]}`;
675
+ The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
609
676
 
610
677
  export async function POST(request: Request) {
611
678
  // Only allow in development
@@ -833,10 +900,15 @@ export async function POST(request: Request) {
833
900
  // Extract recommended file from component sources (to show first, avoid duplication)
834
901
  let recommendedFileContent: { path: string; content: string } | null = null;
835
902
  if (recommendedFile) {
903
+ // Check componentSources first
836
904
  const idx = pageContext.componentSources.findIndex(c => c.path === recommendedFile.path);
837
905
  if (idx !== -1) {
838
906
  recommendedFileContent = pageContext.componentSources[idx];
839
907
  pageContext.componentSources.splice(idx, 1); // Remove to avoid duplication
908
+ }
909
+ // Fallback: Check if it's the page file itself
910
+ else if (pageContext.pageFile === recommendedFile.path && pageContext.pageContent) {
911
+ recommendedFileContent = { path: pageContext.pageFile, content: pageContext.pageContent };
840
912
  }
841
913
  }
842
914
 
@@ -848,18 +920,50 @@ User Request: "${userPrompt}"
848
920
 
849
921
  `;
850
922
 
851
- if (focusedElements && focusedElements.length > 0) {
852
- textContent += `FOCUSED ELEMENTS (user clicked on these):
853
- ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
854
-
855
- `;
856
- }
857
-
858
923
  // ========== TARGET COMPONENT ONLY (with line numbers) ==========
859
924
  // CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
860
925
  if (recommendedFileContent) {
861
926
  const content = recommendedFileContent.content;
862
927
 
928
+ // Search for element IDs in the file to enable precise targeting
929
+ let idMatch: { lineNumber: number; matchedId: string; snippet: string } | null = null;
930
+ if (focusedElements && focusedElements.length > 0) {
931
+ for (const el of focusedElements) {
932
+ idMatch = findElementIdInFile(content, el.elementId, el.childIds);
933
+ if (idMatch) break;
934
+ }
935
+ }
936
+
937
+ // Build focused elements section with precise targeting info
938
+ if (focusedElements && focusedElements.length > 0) {
939
+ textContent += `FOCUSED ELEMENTS (user clicked on these):\n`;
940
+ for (const el of focusedElements) {
941
+ textContent += `- ${el.name} (${el.type})`;
942
+ if (el.textContent) {
943
+ textContent += ` with text "${el.textContent.substring(0, 30)}"`;
944
+ }
945
+ textContent += `\n`;
946
+ }
947
+
948
+ // Add precise targeting if we found an ID match
949
+ if (idMatch) {
950
+ textContent += `
951
+ PRECISE TARGET (found by element ID):
952
+ → ID: "${idMatch.matchedId}"
953
+ → Line: ${idMatch.lineNumber}
954
+ → Look for this ID in the code and modify the element that contains it.
955
+
956
+ `;
957
+ debugLog("Found element ID in file", {
958
+ matchedId: idMatch.matchedId,
959
+ lineNumber: idMatch.lineNumber,
960
+ file: recommendedFileContent.path,
961
+ });
962
+ } else {
963
+ textContent += `\n`;
964
+ }
965
+ }
966
+
863
967
  // Add line numbers to make it easy for LLM to reference exact code
864
968
  const linesWithNumbers = content.split('\n').map((line, i) =>
865
969
  `${String(i + 1).padStart(4, ' ')}| ${line}`
@@ -902,8 +1006,33 @@ ${linesWithNumbers}
902
1006
  usedContext += content.length;
903
1007
  }
904
1008
 
905
- // NOTE: We intentionally skip SUPPORTING COMPONENTS to reduce noise
906
- // The LLM only needs the TARGET file to make accurate edits
1009
+ // ========== KEY UI COMPONENTS ==========
1010
+ // Include UI component definitions (Button, Card, etc.) so LLM knows available variants/props
1011
+ const uiComponentPaths = ['components/ui/button', 'components/ui/card', 'components/ui/input'];
1012
+ const includedUIComponents: string[] = [];
1013
+
1014
+ for (const comp of pageContext.componentSources) {
1015
+ // Check if this is a UI component we should include
1016
+ const isUIComponent = uiComponentPaths.some(uiPath => comp.path.includes(uiPath));
1017
+ if (isUIComponent && usedContext + comp.content.length < TOTAL_CONTEXT_BUDGET) {
1018
+ // Only include the variants/props section, not the whole file
1019
+ const variantsMatch = comp.content.match(/variants:\s*\{[\s\S]*?\n\s*\},\n\s*defaultVariants/);
1020
+ if (variantsMatch) {
1021
+ textContent += `
1022
+ --- UI Component: ${comp.path} (variants only) ---
1023
+ ${variantsMatch[0]}
1024
+ ---
1025
+
1026
+ `;
1027
+ includedUIComponents.push(comp.path);
1028
+ usedContext += variantsMatch[0].length + 100;
1029
+ }
1030
+ }
1031
+ }
1032
+
1033
+ if (includedUIComponents.length > 0) {
1034
+ debugLog("Included UI component definitions", { components: includedUIComponents });
1035
+ }
907
1036
 
908
1037
  // ========== THEME DISCOVERY ==========
909
1038
  // Discover theme tokens and contrast analysis to help LLM make informed color decisions
@@ -791,7 +791,13 @@ export function SonanceDevTools() {
791
791
  const textContent = (el.textContent?.trim() || "").substring(0, 100); // Cap at 100 chars
792
792
  const className = el.className?.toString() || "";
793
793
 
794
- newTagged.push({ name, rect, type: "component", variantId, textContent, className });
794
+ // Capture element ID and child IDs for precise code targeting
795
+ const elementId = el.id || undefined;
796
+ const childIds = Array.from(el.querySelectorAll('[id]'))
797
+ .map(child => child.id)
798
+ .filter(id => id) as string[];
799
+
800
+ newTagged.push({ name, rect, type: "component", variantId, textContent, className, elementId, childIds: childIds.length > 0 ? childIds : undefined });
795
801
  }
796
802
  }
797
803
  });
@@ -834,7 +840,13 @@ export function SonanceDevTools() {
834
840
  const textContent = (el.textContent?.trim() || "").substring(0, 100); // Cap at 100 chars
835
841
  const elClassName = el.className?.toString() || "";
836
842
 
837
- newTagged.push({ name: genericName, rect, type: "component", variantId, textContent, className: elClassName });
843
+ // Capture element ID and child IDs for precise code targeting
844
+ const elementId = el.id || undefined;
845
+ const childIds = Array.from(el.querySelectorAll('[id]'))
846
+ .map(child => child.id)
847
+ .filter(id => id) as string[];
848
+
849
+ newTagged.push({ name: genericName, rect, type: "component", variantId, textContent, className: elClassName, elementId, childIds: childIds.length > 0 ? childIds : undefined });
838
850
  }
839
851
  });
840
852
  });
@@ -1025,9 +1037,12 @@ export function SonanceDevTools() {
1025
1037
  width: element.rect.width,
1026
1038
  height: element.rect.height,
1027
1039
  },
1028
- // NEW: Capture text and className for dynamic file matching
1040
+ // Capture text and className for dynamic file matching
1029
1041
  textContent: element.textContent,
1030
1042
  className: element.className,
1043
+ // Capture element ID and child IDs for precise code targeting
1044
+ elementId: element.elementId,
1045
+ childIds: element.childIds,
1031
1046
  };
1032
1047
 
1033
1048
  setVisionFocusedElements((prev) => {
@@ -28,6 +28,10 @@ export interface DetectedElement {
28
28
  className?: string;
29
29
  /** Component variant ID (hash of styles/classes) to distinguish visual styles */
30
30
  variantId?: string;
31
+ /** The element's DOM id attribute for precise code targeting */
32
+ elementId?: string;
33
+ /** IDs of child elements for more precise targeting */
34
+ childIds?: string[];
31
35
  }
32
36
 
33
37
  // Logo asset from the API
@@ -226,6 +230,10 @@ export interface VisionFocusedElement {
226
230
  textContent?: string;
227
231
  /** The className string of the element for pattern matching */
228
232
  className?: string;
233
+ /** The element's DOM id attribute for precise code targeting */
234
+ elementId?: string;
235
+ /** IDs of child elements for more precise targeting */
236
+ childIds?: string[];
229
237
  }
230
238
 
231
239
  export interface VisionEditRequest {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.71",
3
+ "version": "1.3.72",
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",