sonance-brand-mcp 1.3.63 → 1.3.65

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.
@@ -4,6 +4,7 @@ import * as path from "path";
4
4
  import Anthropic from "@anthropic-ai/sdk";
5
5
  import { randomUUID } from "crypto";
6
6
  import { discoverTheme } from "./theme-discovery";
7
+ import * as babelParser from "@babel/parser";
7
8
 
8
9
  /**
9
10
  * Sonance DevTools API - Apply-First Vision Mode
@@ -29,6 +30,10 @@ interface VisionFocusedElement {
29
30
  height: number;
30
31
  };
31
32
  description?: string;
33
+ /** The actual text content of the element (e.g., "REFRESH", "ADD PROCESS") */
34
+ textContent?: string;
35
+ /** The className string of the element for pattern matching */
36
+ className?: string;
32
37
  }
33
38
 
34
39
  interface VisionFileModification {
@@ -82,6 +87,39 @@ function debugLog(message: string, data?: unknown) {
82
87
  }
83
88
  }
84
89
 
90
+ /**
91
+ * AST-based syntax validation using Babel parser
92
+ * This catches actual syntax errors, not just tag counting
93
+ */
94
+ function validateSyntaxWithAST(content: string, filePath: string): { valid: boolean; error?: string } {
95
+ // Only validate JSX/TSX files
96
+ if (!filePath.endsWith('.tsx') && !filePath.endsWith('.jsx') && !filePath.endsWith('.ts') && !filePath.endsWith('.js')) {
97
+ return { valid: true };
98
+ }
99
+
100
+ try {
101
+ const isTypeScript = filePath.endsWith('.tsx') || filePath.endsWith('.ts');
102
+ const hasJsx = filePath.endsWith('.tsx') || filePath.endsWith('.jsx');
103
+
104
+ const plugins: babelParser.ParserPlugin[] = [];
105
+ if (isTypeScript) plugins.push('typescript');
106
+ if (hasJsx) plugins.push('jsx');
107
+
108
+ babelParser.parse(content, {
109
+ sourceType: 'module',
110
+ plugins,
111
+ });
112
+ return { valid: true };
113
+ } catch (e: unknown) {
114
+ const error = e as { message?: string; loc?: { line: number; column: number } };
115
+ const location = error.loc ? ` at line ${error.loc.line}, column ${error.loc.column}` : '';
116
+ return {
117
+ valid: false,
118
+ error: `Syntax error in ${filePath}${location}: ${error.message || 'Unknown parse error'}`
119
+ };
120
+ }
121
+ }
122
+
85
123
  /**
86
124
  * Result of LLM screenshot analysis for smart file discovery
87
125
  */
@@ -176,10 +214,14 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
176
214
  * This helps identify which file actually contains the element the user clicked on
177
215
  *
178
216
  * CURSOR-STYLE: Search file CONTENTS for patterns, not just file names
217
+ *
218
+ * @param phase2aMatches - File paths that Phase 2a identified by component name matching visible text
219
+ * These get a MASSIVE bonus because they're contextually relevant
179
220
  */
180
221
  function findFilesContainingElement(
181
222
  focusedElements: VisionFocusedElement[] | undefined,
182
- candidateFiles: { path: string; content: string }[]
223
+ candidateFiles: { path: string; content: string }[],
224
+ phase2aMatches: string[] = [] // Files that Phase 2a identified by component name
183
225
  ): { path: string; score: number; matches: string[] }[] {
184
226
  if (!focusedElements || focusedElements.length === 0) {
185
227
  return [];
@@ -191,7 +233,13 @@ function findFilesContainingElement(
191
233
  let score = 0;
192
234
  const matches: string[] = [];
193
235
  const content = file.content;
194
- const contentLower = content.toLowerCase();
236
+
237
+ // HIGHEST VALUE: Phase 2a identified this file by matching component name to visible text
238
+ // This means the LLM saw "ProcessRow" in the screenshot and this file is ProcessRow.tsx
239
+ if (phase2aMatches.includes(file.path)) {
240
+ score += 200;
241
+ matches.push('Phase2a component match');
242
+ }
195
243
 
196
244
  for (const el of focusedElements) {
197
245
  // Extract element type from the name (e.g., "button #123" -> "button")
@@ -216,28 +264,42 @@ function findFilesContainingElement(
216
264
  matches.push(`defines ${capitalizedType}`);
217
265
  }
218
266
 
219
- // MEDIUM VALUE: JSX usage of this element type (rendered in this file)
220
- const jsxTagPattern = new RegExp(`<${capitalizedType}[\\s/>]`, 'g');
221
- const jsxMatches = content.match(jsxTagPattern);
222
- if (jsxMatches) {
223
- score += 20 * jsxMatches.length;
224
- matches.push(`<${capitalizedType}> x${jsxMatches.length}`);
225
- }
226
-
227
- // MEDIUM VALUE: HTML tag usage
228
- const htmlTagPattern = new RegExp(`<${elementType}[\\s>]`, 'gi');
229
- const htmlMatches = content.match(htmlTagPattern);
230
- if (htmlMatches) {
231
- score += 10 * htmlMatches.length;
232
- matches.push(`<${elementType}> x${htmlMatches.length}`);
267
+ // NOTE: We intentionally DO NOT count buttons, onClick handlers, or JSX tags
268
+ // because files with MORE of these are often "kitchen sink" components,
269
+ // not the focused component the user is looking at.
270
+ // Instead, we rely on Phase 2a matches and textContent matching.
271
+
272
+ // VERY HIGH VALUE: Exact text content match (e.g., button says "REFRESH")
273
+ // This is THE file if it contains the exact text the user clicked on
274
+ if (el.textContent && el.textContent.length >= 2) {
275
+ // Search for the text in JSX patterns like >REFRESH< or {`REFRESH`} or {"REFRESH"}
276
+ const textPatterns = [
277
+ `>${el.textContent}<`, // JSX text content
278
+ `"${el.textContent}"`, // String literal
279
+ `'${el.textContent}'`, // Single-quoted string
280
+ `\`${el.textContent}\``, // Template literal
281
+ ];
282
+
283
+ for (const pattern of textPatterns) {
284
+ if (content.includes(pattern)) {
285
+ score += 150; // Very high - exact match!
286
+ matches.push(`contains "${el.textContent.substring(0, 20)}"`);
287
+ break; // Only add once per element
288
+ }
289
+ }
233
290
  }
234
291
 
235
- // LOW VALUE: Event handlers (onClick, onPress, etc.)
236
- if (elementType === "button" || componentType === "button") {
237
- const clickHandlers = content.match(/onClick\s*=/gi);
238
- if (clickHandlers) {
239
- score += 5 * clickHandlers.length;
240
- matches.push(`onClick x${clickHandlers.length}`);
292
+ // MEDIUM VALUE: className pattern match
293
+ // If the file contains the same className string, it likely defines this element
294
+ if (el.className && el.className.length >= 10) {
295
+ // Extract the first few unique class names (not all to avoid noise)
296
+ const classNames = el.className.split(/\s+/).filter(c => c.length > 3).slice(0, 5);
297
+ for (const cls of classNames) {
298
+ if (content.includes(cls)) {
299
+ score += 25;
300
+ matches.push(`has class "${cls.substring(0, 20)}"`);
301
+ break; // Only add once per element
302
+ }
241
303
  }
242
304
  }
243
305
 
@@ -412,7 +474,7 @@ function searchFilesSmart(
412
474
  analysis: ScreenshotAnalysis,
413
475
  projectRoot: string,
414
476
  maxFiles: number = 10
415
- ): { path: string; content: string; score: number }[] {
477
+ ): { path: string; content: string; score: number; filenameMatch: boolean }[] {
416
478
  const { visibleText, componentNames, codePatterns } = analysis;
417
479
 
418
480
  // Combine all search terms
@@ -534,7 +596,7 @@ function searchFilesSmart(
534
596
  }))
535
597
  });
536
598
 
537
- return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
599
+ return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score, filenameMatch: r.filenameMatch }));
538
600
  }
539
601
 
540
602
  const VISION_SYSTEM_PROMPT = `You edit code. Return ONLY valid JSON - no explanation, no preamble, no markdown.
@@ -678,10 +740,19 @@ export async function POST(request: Request) {
678
740
  const searchResults = searchFilesSmart(analysis, projectRoot, 10);
679
741
  smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
680
742
 
743
+ // Extract Phase 2a matches - files identified by component name matching visible text
744
+ // These are contextually relevant (e.g., "ProcessRow" seen in screenshot -> ProcessRow.tsx)
745
+ const phase2aMatches = searchResults
746
+ .filter(r => r.filenameMatch)
747
+ .map(r => r.path);
748
+
749
+ debugLog("Phase 2a component-name matches", { phase2aMatches });
750
+
681
751
  // PHASE 2.5: Find which files contain the focused element
682
752
  const focusedElementHints = findFilesContainingElement(
683
753
  focusedElements,
684
- searchResults.map(r => ({ path: r.path, content: r.content }))
754
+ searchResults.map(r => ({ path: r.path, content: r.content })),
755
+ phase2aMatches // Pass Phase 2a matches for massive bonus
685
756
  );
686
757
 
687
758
  if (focusedElementHints.length > 0) {
@@ -1199,32 +1270,17 @@ This is better than generating patches with made-up code.`,
1199
1270
  console.log(`[Apply-First] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
1200
1271
  }
1201
1272
 
1202
- // SYNTAX VALIDATION: Check for common JSX/HTML tag mismatches
1203
- // This catches cases where the LLM changes opening tags but not closing tags
1204
- if (mod.filePath.endsWith('.tsx') || mod.filePath.endsWith('.jsx')) {
1205
- const openDivs = (modifiedContent.match(/<div[\s>]/g) || []).length;
1206
- const closeDivs = (modifiedContent.match(/<\/div>/g) || []).length;
1207
- const openSpans = (modifiedContent.match(/<span[\s>]/g) || []).length;
1208
- const closeSpans = (modifiedContent.match(/<\/span>/g) || []).length;
1209
-
1210
- if (openDivs !== closeDivs || openSpans !== closeSpans) {
1211
- debugLog("SYNTAX WARNING: Tag mismatch detected", {
1212
- filePath: mod.filePath,
1213
- divs: { open: openDivs, close: closeDivs },
1214
- spans: { open: openSpans, close: closeSpans },
1215
- });
1216
- console.warn(`[Apply-First] ⚠️ SYNTAX WARNING: Tag mismatch in ${mod.filePath}`);
1217
- console.warn(`[Apply-First] divs: ${openDivs} open, ${closeDivs} close`);
1218
- console.warn(`[Apply-First] spans: ${openSpans} open, ${closeSpans} close`);
1219
-
1220
- // If there's a significant mismatch, reject the change
1221
- const divDiff = Math.abs(openDivs - closeDivs);
1222
- const spanDiff = Math.abs(openSpans - closeSpans);
1223
- if (divDiff > 0 || spanDiff > 0) {
1224
- patchErrors.push(`${mod.filePath}: LLM introduced syntax error - tag mismatch detected (${divDiff} div, ${spanDiff} span). Change rejected.`);
1225
- continue;
1226
- }
1227
- }
1273
+ // AST VALIDATION: Use Babel parser to catch actual syntax errors
1274
+ // This is more accurate than tag counting and catches real issues
1275
+ const syntaxValidation = validateSyntaxWithAST(modifiedContent, mod.filePath);
1276
+ if (!syntaxValidation.valid) {
1277
+ debugLog("SYNTAX ERROR: AST validation failed", {
1278
+ filePath: mod.filePath,
1279
+ error: syntaxValidation.error,
1280
+ });
1281
+ console.warn(`[Apply-First] ⚠️ SYNTAX ERROR: ${syntaxValidation.error}`);
1282
+ patchErrors.push(`${mod.filePath}: ${syntaxValidation.error}`);
1283
+ continue; // Skip this file, trigger retry
1228
1284
  }
1229
1285
  } else {
1230
1286
  // No patches - skip
@@ -3,6 +3,7 @@ import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import Anthropic from "@anthropic-ai/sdk";
5
5
  import { discoverTheme } from "../sonance-vision-apply/theme-discovery";
6
+ import * as babelParser from "@babel/parser";
6
7
 
7
8
  /**
8
9
  * Sonance DevTools API - Vision Mode Editor
@@ -28,6 +29,10 @@ interface VisionFocusedElement {
28
29
  height: number;
29
30
  };
30
31
  description?: string;
32
+ /** The actual text content of the element (e.g., "REFRESH", "ADD PROCESS") */
33
+ textContent?: string;
34
+ /** The className string of the element for pattern matching */
35
+ className?: string;
31
36
  }
32
37
 
33
38
  interface VisionFileModification {
@@ -78,6 +83,39 @@ function debugLog(message: string, data?: unknown) {
78
83
  }
79
84
  }
80
85
 
86
+ /**
87
+ * AST-based syntax validation using Babel parser
88
+ * This catches actual syntax errors, not just tag counting
89
+ */
90
+ function validateSyntaxWithAST(content: string, filePath: string): { valid: boolean; error?: string } {
91
+ // Only validate JSX/TSX files
92
+ if (!filePath.endsWith('.tsx') && !filePath.endsWith('.jsx') && !filePath.endsWith('.ts') && !filePath.endsWith('.js')) {
93
+ return { valid: true };
94
+ }
95
+
96
+ try {
97
+ const isTypeScript = filePath.endsWith('.tsx') || filePath.endsWith('.ts');
98
+ const hasJsx = filePath.endsWith('.tsx') || filePath.endsWith('.jsx');
99
+
100
+ const plugins: babelParser.ParserPlugin[] = [];
101
+ if (isTypeScript) plugins.push('typescript');
102
+ if (hasJsx) plugins.push('jsx');
103
+
104
+ babelParser.parse(content, {
105
+ sourceType: 'module',
106
+ plugins,
107
+ });
108
+ return { valid: true };
109
+ } catch (e: unknown) {
110
+ const error = e as { message?: string; loc?: { line: number; column: number } };
111
+ const location = error.loc ? ` at line ${error.loc.line}, column ${error.loc.column}` : '';
112
+ return {
113
+ valid: false,
114
+ error: `Syntax error in ${filePath}${location}: ${error.message || 'Unknown parse error'}`
115
+ };
116
+ }
117
+ }
118
+
81
119
  /**
82
120
  * Result of LLM screenshot analysis for smart file discovery
83
121
  */
@@ -172,10 +210,14 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
172
210
  * This helps identify which file actually contains the element the user clicked on
173
211
  *
174
212
  * CURSOR-STYLE: Search file CONTENTS for patterns, not just file names
213
+ *
214
+ * @param phase2aMatches - File paths that Phase 2a identified by component name matching visible text
215
+ * These get a MASSIVE bonus because they're contextually relevant
175
216
  */
176
217
  function findFilesContainingElement(
177
218
  focusedElements: VisionFocusedElement[] | undefined,
178
- candidateFiles: { path: string; content: string }[]
219
+ candidateFiles: { path: string; content: string }[],
220
+ phase2aMatches: string[] = [] // Files that Phase 2a identified by component name
179
221
  ): { path: string; score: number; matches: string[] }[] {
180
222
  if (!focusedElements || focusedElements.length === 0) {
181
223
  return [];
@@ -187,7 +229,13 @@ function findFilesContainingElement(
187
229
  let score = 0;
188
230
  const matches: string[] = [];
189
231
  const content = file.content;
190
- const contentLower = content.toLowerCase();
232
+
233
+ // HIGHEST VALUE: Phase 2a identified this file by matching component name to visible text
234
+ // This means the LLM saw "ProcessRow" in the screenshot and this file is ProcessRow.tsx
235
+ if (phase2aMatches.includes(file.path)) {
236
+ score += 200;
237
+ matches.push('Phase2a component match');
238
+ }
191
239
 
192
240
  for (const el of focusedElements) {
193
241
  // Extract element type from the name (e.g., "button #123" -> "button")
@@ -212,28 +260,42 @@ function findFilesContainingElement(
212
260
  matches.push(`defines ${capitalizedType}`);
213
261
  }
214
262
 
215
- // MEDIUM VALUE: JSX usage of this element type (rendered in this file)
216
- const jsxTagPattern = new RegExp(`<${capitalizedType}[\\s/>]`, 'g');
217
- const jsxMatches = content.match(jsxTagPattern);
218
- if (jsxMatches) {
219
- score += 20 * jsxMatches.length;
220
- matches.push(`<${capitalizedType}> x${jsxMatches.length}`);
221
- }
222
-
223
- // MEDIUM VALUE: HTML tag usage
224
- const htmlTagPattern = new RegExp(`<${elementType}[\\s>]`, 'gi');
225
- const htmlMatches = content.match(htmlTagPattern);
226
- if (htmlMatches) {
227
- score += 10 * htmlMatches.length;
228
- matches.push(`<${elementType}> x${htmlMatches.length}`);
263
+ // NOTE: We intentionally DO NOT count buttons, onClick handlers, or JSX tags
264
+ // because files with MORE of these are often "kitchen sink" components,
265
+ // not the focused component the user is looking at.
266
+ // Instead, we rely on Phase 2a matches and textContent matching.
267
+
268
+ // VERY HIGH VALUE: Exact text content match (e.g., button says "REFRESH")
269
+ // This is THE file if it contains the exact text the user clicked on
270
+ if (el.textContent && el.textContent.length >= 2) {
271
+ // Search for the text in JSX patterns like >REFRESH< or {`REFRESH`} or {"REFRESH"}
272
+ const textPatterns = [
273
+ `>${el.textContent}<`, // JSX text content
274
+ `"${el.textContent}"`, // String literal
275
+ `'${el.textContent}'`, // Single-quoted string
276
+ `\`${el.textContent}\``, // Template literal
277
+ ];
278
+
279
+ for (const pattern of textPatterns) {
280
+ if (content.includes(pattern)) {
281
+ score += 150; // Very high - exact match!
282
+ matches.push(`contains "${el.textContent.substring(0, 20)}"`);
283
+ break; // Only add once per element
284
+ }
285
+ }
229
286
  }
230
287
 
231
- // LOW VALUE: Event handlers (onClick, onPress, etc.)
232
- if (elementType === "button" || componentType === "button") {
233
- const clickHandlers = content.match(/onClick\s*=/gi);
234
- if (clickHandlers) {
235
- score += 5 * clickHandlers.length;
236
- matches.push(`onClick x${clickHandlers.length}`);
288
+ // MEDIUM VALUE: className pattern match
289
+ // If the file contains the same className string, it likely defines this element
290
+ if (el.className && el.className.length >= 10) {
291
+ // Extract the first few unique class names (not all to avoid noise)
292
+ const classNames = el.className.split(/\s+/).filter(c => c.length > 3).slice(0, 5);
293
+ for (const cls of classNames) {
294
+ if (content.includes(cls)) {
295
+ score += 25;
296
+ matches.push(`has class "${cls.substring(0, 20)}"`);
297
+ break; // Only add once per element
298
+ }
237
299
  }
238
300
  }
239
301
 
@@ -408,7 +470,7 @@ function searchFilesSmart(
408
470
  analysis: ScreenshotAnalysis,
409
471
  projectRoot: string,
410
472
  maxFiles: number = 10
411
- ): { path: string; content: string; score: number }[] {
473
+ ): { path: string; content: string; score: number; filenameMatch: boolean }[] {
412
474
  const { visibleText, componentNames, codePatterns } = analysis;
413
475
 
414
476
  // Combine all search terms
@@ -530,7 +592,7 @@ function searchFilesSmart(
530
592
  }))
531
593
  });
532
594
 
533
- return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
595
+ return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score, filenameMatch: r.filenameMatch }));
534
596
  }
535
597
 
536
598
  const VISION_SYSTEM_PROMPT = `You edit code. Return ONLY valid JSON - no explanation, no preamble, no markdown.
@@ -647,10 +709,19 @@ export async function POST(request: Request) {
647
709
  const searchResults = searchFilesSmart(analysis, projectRoot, 10);
648
710
  smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
649
711
 
712
+ // Extract Phase 2a matches - files identified by component name matching visible text
713
+ // These are contextually relevant (e.g., "ProcessRow" seen in screenshot -> ProcessRow.tsx)
714
+ const phase2aMatches = searchResults
715
+ .filter(r => r.filenameMatch)
716
+ .map(r => r.path);
717
+
718
+ debugLog("Phase 2a component-name matches", { phase2aMatches });
719
+
650
720
  // PHASE 2.5: Find which files contain the focused element
651
721
  const focusedElementHints = findFilesContainingElement(
652
722
  focusedElements,
653
- searchResults.map(r => ({ path: r.path, content: r.content }))
723
+ searchResults.map(r => ({ path: r.path, content: r.content })),
724
+ phase2aMatches // Pass Phase 2a matches for massive bonus
654
725
  );
655
726
 
656
727
  if (focusedElementHints.length > 0) {
@@ -1172,6 +1243,18 @@ This is better than generating patches with made-up code.`,
1172
1243
  modifiedContent = patchResult.modifiedContent;
1173
1244
  console.log(`[Vision Mode] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
1174
1245
  }
1246
+
1247
+ // AST VALIDATION: Use Babel parser to catch actual syntax errors
1248
+ const syntaxValidation = validateSyntaxWithAST(modifiedContent, mod.filePath);
1249
+ if (!syntaxValidation.valid) {
1250
+ debugLog("SYNTAX ERROR: AST validation failed", {
1251
+ filePath: mod.filePath,
1252
+ error: syntaxValidation.error,
1253
+ });
1254
+ console.warn(`[Vision Mode] ⚠️ SYNTAX ERROR: ${syntaxValidation.error}`);
1255
+ patchErrors.push(`${mod.filePath}: ${syntaxValidation.error}`);
1256
+ continue; // Skip this file, trigger retry
1257
+ }
1175
1258
  } else {
1176
1259
  // No patches - skip
1177
1260
  console.warn(`[Vision Mode] No patches for ${mod.filePath}`);
@@ -787,7 +787,11 @@ export function SonanceDevTools() {
787
787
  el.setAttribute("data-sonance-variant", variantId);
788
788
  }
789
789
 
790
- newTagged.push({ name, rect, type: "component", variantId });
790
+ // Capture textContent and className for dynamic element matching
791
+ const textContent = (el.textContent?.trim() || "").substring(0, 100); // Cap at 100 chars
792
+ const className = el.className?.toString() || "";
793
+
794
+ newTagged.push({ name, rect, type: "component", variantId, textContent, className });
791
795
  }
792
796
  }
793
797
  });
@@ -826,7 +830,11 @@ export function SonanceDevTools() {
826
830
  el.setAttribute("data-sonance-name", genericName);
827
831
  }
828
832
 
829
- newTagged.push({ name: genericName, rect, type: "component", variantId });
833
+ // Capture textContent and className for dynamic element matching
834
+ const textContent = (el.textContent?.trim() || "").substring(0, 100); // Cap at 100 chars
835
+ const elClassName = el.className?.toString() || "";
836
+
837
+ newTagged.push({ name: genericName, rect, type: "component", variantId, textContent, className: elClassName });
830
838
  }
831
839
  });
832
840
  });
@@ -1017,6 +1025,9 @@ export function SonanceDevTools() {
1017
1025
  width: element.rect.width,
1018
1026
  height: element.rect.height,
1019
1027
  },
1028
+ // NEW: Capture text and className for dynamic file matching
1029
+ textContent: element.textContent,
1030
+ className: element.className,
1020
1031
  };
1021
1032
 
1022
1033
  setVisionFocusedElements((prev) => {
@@ -22,8 +22,10 @@ export interface DetectedElement {
22
22
  logoId?: string;
23
23
  /** Unique ID for text elements (for selection/editing) */
24
24
  textId?: string;
25
- /** The actual text content (for text elements) */
25
+ /** The actual text content (for all elements - button text, link text, etc.) */
26
26
  textContent?: string;
27
+ /** The className string for pattern matching in code */
28
+ className?: string;
27
29
  /** Component variant ID (hash of styles/classes) to distinguish visual styles */
28
30
  variantId?: string;
29
31
  }
@@ -220,6 +222,10 @@ export interface VisionFocusedElement {
220
222
  height: number;
221
223
  };
222
224
  description?: string;
225
+ /** The actual text content of the element (e.g., "REFRESH", "ADD PROCESS") */
226
+ textContent?: string;
227
+ /** The className string of the element for pattern matching */
228
+ className?: string;
223
229
  }
224
230
 
225
231
  export interface VisionEditRequest {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.63",
3
+ "version": "1.3.65",
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",