sonance-brand-mcp 1.3.47 → 1.3.49

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.
@@ -468,6 +468,8 @@ Return search/replace patches (NOT full files). The system applies your patches
468
468
  - "replace" contains your modified version
469
469
  - Include 2-4 lines of context in "search" to make it unique
470
470
  - You may ONLY edit files provided in the PAGE CONTEXT section
471
+ - CRITICAL: NEVER invent or guess code. Your "search" string MUST be copied EXACTLY from the provided file content. If you cannot find the exact code to modify, return an empty modifications array.
472
+ - If the file content appears truncated, only modify code that is visible in the provided content.
471
473
 
472
474
  **SONANCE BRAND COLORS:**
473
475
  - Charcoal: #333F48 (primary text)
@@ -543,6 +545,7 @@ export async function POST(request: Request) {
543
545
  return NextResponse.json({
544
546
  success: result.success,
545
547
  message: result.message,
548
+ error: result.success ? undefined : result.message, // Include error for client compatibility
546
549
  filesReverted: result.filesReverted,
547
550
  });
548
551
  }
@@ -658,10 +661,10 @@ export async function POST(request: Request) {
658
661
  }
659
662
 
660
663
  // ========== SMART CONTEXT BUDGETING ==========
661
- // Total budget: 100k chars (~25k tokens, safe for Claude)
662
- // Priority: Recommended file (full) > Page file (limited) > Other components (dynamic)
663
- const TOTAL_CONTEXT_BUDGET = 100000;
664
- const MAX_RECOMMENDED_FILE = 80000; // 80k chars max for recommended file
664
+ // Claude can handle 200k tokens (~800k chars), so we can safely include large files
665
+ // Priority: Recommended file (NEVER truncate) > Page file (limited) > Other components (dynamic)
666
+ const TOTAL_CONTEXT_BUDGET = 500000; // 500k chars total budget
667
+ const MAX_RECOMMENDED_FILE = Infinity; // NEVER truncate the target file - AI needs full context
665
668
  const MAX_PAGE_FILE = 2000; // Page file is just a wrapper
666
669
  const MAX_GLOBALS_CSS = 1500;
667
670
  const MAX_FILES = 25;
@@ -695,9 +698,8 @@ ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}
695
698
 
696
699
  // ========== TARGET COMPONENT (RECOMMENDED FILE) - SHOWN FIRST ==========
697
700
  if (recommendedFileContent) {
698
- const content = recommendedFileContent.content.length > MAX_RECOMMENDED_FILE
699
- ? recommendedFileContent.content.substring(0, MAX_RECOMMENDED_FILE) + "\n// ... (file truncated at 50k chars, showing component definition and main logic)"
700
- : recommendedFileContent.content;
701
+ // Never truncate the recommended file - AI needs full context to avoid hallucination
702
+ const content = recommendedFileContent.content;
701
703
 
702
704
  textContent += `═══════════════════════════════════════════════════════════════════════════════
703
705
  ⚡ TARGET COMPONENT - YOU MUST EDIT THIS FILE
@@ -808,112 +810,166 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
808
810
  text: textContent,
809
811
  });
810
812
 
811
- // Call Claude Vision API
813
+ // Call Claude Vision API with retry mechanism
812
814
  const anthropic = new Anthropic({ apiKey });
813
-
814
- const response = await anthropic.messages.create({
815
- model: "claude-sonnet-4-20250514",
816
- max_tokens: 16384,
817
- messages: [
815
+
816
+ // Build set of valid file paths from page context (needed for validation)
817
+ const validFilePaths = new Set<string>();
818
+ if (pageContext.pageFile) {
819
+ validFilePaths.add(pageContext.pageFile);
820
+ }
821
+ for (const comp of pageContext.componentSources) {
822
+ validFilePaths.add(comp.path);
823
+ }
824
+
825
+ // FIX: Add the recommended file to validFilePaths (it was spliced out for display purposes)
826
+ if (recommendedFileContent) {
827
+ validFilePaths.add(recommendedFileContent.path);
828
+ }
829
+
830
+ // Retry loop for handling patch failures
831
+ const MAX_RETRIES = 1;
832
+ let retryCount = 0;
833
+ let lastPatchErrors: string[] = [];
834
+ let modifications: VisionFileModification[] = [];
835
+ let finalExplanation: string | undefined;
836
+ let finalReasoning: string | undefined;
837
+
838
+ while (retryCount <= MAX_RETRIES) {
839
+ // Build messages for this attempt
840
+ const currentMessages: Anthropic.MessageCreateParams["messages"] = [
818
841
  {
819
842
  role: "user",
820
843
  content: messageContent,
821
844
  },
822
- ],
823
- system: VISION_SYSTEM_PROMPT,
824
- });
845
+ ];
846
+
847
+ // If this is a retry, add feedback about what went wrong
848
+ if (retryCount > 0 && lastPatchErrors.length > 0) {
849
+ debugLog("Retry attempt with feedback", { retryCount, errorCount: lastPatchErrors.length });
850
+
851
+ // Get a snippet of the actual file content to help AI find correct code
852
+ let fileSnippet = "";
853
+ if (recommendedFileContent) {
854
+ // Show first 800 chars to give AI context about what code actually exists
855
+ fileSnippet = recommendedFileContent.content.substring(0, 800).replace(/\n/g, "\n");
856
+ }
857
+
858
+ currentMessages.push({
859
+ role: "assistant",
860
+ content: "I'll analyze the screenshot and generate the patches now.",
861
+ });
862
+ currentMessages.push({
863
+ role: "user",
864
+ content: `PATCH APPLICATION FAILED. Your previous patches referenced code that does not exist in the file (hallucination detected).
825
865
 
826
- // Extract text content from response
827
- const textResponse = response.content.find((block) => block.type === "text");
828
- if (!textResponse || textResponse.type !== "text") {
829
- return NextResponse.json(
830
- { error: "No text response from AI" },
831
- { status: 500 }
832
- );
833
- }
866
+ Failed patches:
867
+ ${lastPatchErrors.join("\n\n")}
834
868
 
835
- // Parse AI response - now expecting patches instead of full file content
836
- let aiResponse: {
837
- reasoning?: string;
838
- modifications: Array<{
839
- filePath: string;
840
- patches?: Patch[];
841
- // Legacy support for modifiedContent (will be deprecated)
842
- modifiedContent?: string;
869
+ ${fileSnippet ? `Here is the ACTUAL beginning of the file you're trying to edit:
870
+ \`\`\`tsx
871
+ ${fileSnippet}...
872
+ \`\`\`
873
+ ` : ""}
874
+ CRITICAL INSTRUCTIONS:
875
+ 1. Your "search" string MUST be copied EXACTLY from the file content I provided above
876
+ 2. Do NOT invent, guess, or imagine code that might exist
877
+ 3. Look for the ACTUAL code in the TARGET COMPONENT section and copy it exactly
878
+
879
+ If you still cannot find the exact code to modify after reviewing the file:
880
+ - Return {"modifications": [], "explanation": "Could not locate the exact code. Please specify which element you want to change more precisely."}
881
+
882
+ This is better than generating patches with made-up code.`,
883
+ });
884
+ }
885
+
886
+ const response = await anthropic.messages.create({
887
+ model: "claude-sonnet-4-20250514",
888
+ max_tokens: 16384,
889
+ messages: currentMessages,
890
+ system: VISION_SYSTEM_PROMPT,
891
+ });
892
+
893
+ // Extract text content from response
894
+ const textResponse = response.content.find((block) => block.type === "text");
895
+ if (!textResponse || textResponse.type !== "text") {
896
+ return NextResponse.json(
897
+ { error: "No text response from AI" },
898
+ { status: 500 }
899
+ );
900
+ }
901
+
902
+ // Parse AI response - now expecting patches instead of full file content
903
+ let aiResponse: {
904
+ reasoning?: string;
905
+ modifications: Array<{
906
+ filePath: string;
907
+ patches?: Patch[];
908
+ // Legacy support for modifiedContent (will be deprecated)
909
+ modifiedContent?: string;
910
+ explanation?: string;
911
+ }>;
843
912
  explanation?: string;
844
- }>;
845
- explanation?: string;
846
- };
913
+ };
847
914
 
848
- try {
849
- let jsonText = textResponse.text.trim();
850
-
851
- // Try to extract JSON from markdown code blocks
852
- const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
853
- jsonText.match(/```\n([\s\S]*?)\n```/);
854
-
855
- if (jsonMatch) {
856
- jsonText = jsonMatch[1];
857
- } else if (jsonText.includes("```json")) {
858
- const start = jsonText.indexOf("```json") + 7;
859
- const end = jsonText.lastIndexOf("```");
860
- if (end > start) {
861
- jsonText = jsonText.substring(start, end);
915
+ try {
916
+ let jsonText = textResponse.text.trim();
917
+
918
+ // Try to extract JSON from markdown code blocks
919
+ const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
920
+ jsonText.match(/```\n([\s\S]*?)\n```/);
921
+
922
+ if (jsonMatch) {
923
+ jsonText = jsonMatch[1];
924
+ } else if (jsonText.includes("```json")) {
925
+ const start = jsonText.indexOf("```json") + 7;
926
+ const end = jsonText.lastIndexOf("```");
927
+ if (end > start) {
928
+ jsonText = jsonText.substring(start, end);
929
+ }
862
930
  }
863
- }
864
931
 
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);
932
+ jsonText = jsonText.trim();
933
+
934
+ // Robust JSON extraction: find the first { and last } to extract JSON object
935
+ // This handles cases where the LLM includes preamble text before the JSON
936
+ const firstBrace = jsonText.indexOf('{');
937
+ const lastBrace = jsonText.lastIndexOf('}');
938
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
939
+ jsonText = jsonText.substring(firstBrace, lastBrace + 1);
940
+ }
941
+
942
+ aiResponse = JSON.parse(jsonText);
943
+ } catch {
944
+ console.error("Failed to parse AI response:", textResponse.text);
945
+ return NextResponse.json(
946
+ { error: "Failed to parse AI response. Please try again." },
947
+ { status: 500 }
948
+ );
873
949
  }
874
-
875
- aiResponse = JSON.parse(jsonText);
876
- } catch {
877
- console.error("Failed to parse AI response:", textResponse.text);
878
- return NextResponse.json(
879
- { error: "Failed to parse AI response. Please try again." },
880
- { status: 500 }
881
- );
882
- }
883
950
 
884
- if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
885
- return NextResponse.json({
886
- success: true,
887
- sessionId: newSessionId,
888
- modifications: [],
889
- explanation: aiResponse.explanation || "No changes needed.",
890
- reasoning: aiResponse.reasoning,
891
- });
892
- }
951
+ finalExplanation = aiResponse.explanation;
952
+ finalReasoning = aiResponse.reasoning;
893
953
 
894
- // Build set of valid file paths from page context
895
- const validFilePaths = new Set<string>();
896
- if (pageContext.pageFile) {
897
- validFilePaths.add(pageContext.pageFile);
898
- }
899
- for (const comp of pageContext.componentSources) {
900
- validFilePaths.add(comp.path);
901
- }
902
-
903
- // FIX: Add the recommended file to validFilePaths (it was spliced out for display purposes)
904
- if (recommendedFileContent) {
905
- validFilePaths.add(recommendedFileContent.path);
906
- }
954
+ if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
955
+ return NextResponse.json({
956
+ success: true,
957
+ sessionId: newSessionId,
958
+ modifications: [],
959
+ explanation: aiResponse.explanation || "No changes needed.",
960
+ reasoning: aiResponse.reasoning,
961
+ });
962
+ }
907
963
 
908
- debugLog("VALIDATION: Valid file paths from page context", {
909
- pageFile: pageContext.pageFile,
910
- validFilePaths: Array.from(validFilePaths),
911
- aiRequestedFiles: aiResponse.modifications.map(m => m.filePath)
912
- });
964
+ debugLog("VALIDATION: Valid file paths from page context", {
965
+ pageFile: pageContext.pageFile,
966
+ validFilePaths: Array.from(validFilePaths),
967
+ aiRequestedFiles: aiResponse.modifications.map(m => m.filePath)
968
+ });
913
969
 
914
- // Process modifications - apply patches to get modified content
915
- const modifications: VisionFileModification[] = [];
916
- const patchErrors: string[] = [];
970
+ // Process modifications - apply patches to get modified content
971
+ modifications = [];
972
+ const patchErrors: string[] = [];
917
973
 
918
974
  for (const mod of aiResponse.modifications) {
919
975
  // Validate that the file path is in the page context
@@ -947,6 +1003,47 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
947
1003
  // New patch-based approach
948
1004
  console.log(`[Apply-First] Applying ${mod.patches.length} patches to ${mod.filePath}`);
949
1005
 
1006
+ // PRE-VALIDATION: Check if all search strings exist in the file BEFORE applying
1007
+ const preValidationErrors: string[] = [];
1008
+ for (const patch of mod.patches) {
1009
+ const normalizedSearch = patch.search.replace(/\\n/g, "\n");
1010
+ if (!originalContent.includes(normalizedSearch)) {
1011
+ // Try fuzzy match as fallback
1012
+ const fuzzyMatch = findFuzzyMatch(normalizedSearch, originalContent);
1013
+ if (!fuzzyMatch) {
1014
+ // Find the closest matching snippet to help with debugging
1015
+ const searchPreview = normalizedSearch.substring(0, 80).replace(/\n/g, "\\n");
1016
+
1017
+ // Look for partial matches to give helpful feedback
1018
+ const searchLines = normalizedSearch.split("\n").filter(l => l.trim().length > 10);
1019
+ const partialMatches: string[] = [];
1020
+ for (const line of searchLines.slice(0, 3)) {
1021
+ const trimmedLine = line.trim();
1022
+ if (trimmedLine.length > 10 && originalContent.includes(trimmedLine)) {
1023
+ partialMatches.push(trimmedLine.substring(0, 50));
1024
+ }
1025
+ }
1026
+
1027
+ let errorMsg = `Patch search string not found: "${searchPreview}..."`;
1028
+ if (partialMatches.length > 0) {
1029
+ errorMsg += ` (partial matches found: ${partialMatches.join(", ")})`;
1030
+ }
1031
+ preValidationErrors.push(errorMsg);
1032
+ debugLog("Pre-validation failed: search string not found", {
1033
+ filePath: mod.filePath,
1034
+ searchPreview,
1035
+ partialMatches
1036
+ });
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ // If pre-validation failed, add to errors and skip this file
1042
+ if (preValidationErrors.length > 0) {
1043
+ patchErrors.push(`${mod.filePath}: AI generated patches with non-existent code (hallucination detected):\n${preValidationErrors.join("\n")}`);
1044
+ continue;
1045
+ }
1046
+
950
1047
  const patchResult = applyPatches(originalContent, mod.patches);
951
1048
 
952
1049
  if (!patchResult.success) {
@@ -993,22 +1090,39 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
993
1090
  });
994
1091
  }
995
1092
 
996
- // If all modifications failed, return error
997
- if (patchErrors.length > 0 && modifications.length === 0) {
998
- console.error("All AI patches failed:", patchErrors);
999
- return NextResponse.json(
1000
- {
1001
- success: false,
1002
- error: `Patch application failed:\n\n${patchErrors.join("\n\n")}`,
1003
- },
1004
- { status: 400 }
1005
- );
1006
- }
1093
+ // If all modifications failed, check if we should retry
1094
+ if (patchErrors.length > 0 && modifications.length === 0) {
1095
+ if (retryCount < MAX_RETRIES) {
1096
+ console.warn(`[Apply-First] All patches failed, retrying (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
1097
+ debugLog("Retry triggered due to patch failures", {
1098
+ retryCount,
1099
+ errorCount: patchErrors.length,
1100
+ errors: patchErrors.slice(0, 3) // Log first 3 errors
1101
+ });
1102
+ lastPatchErrors = patchErrors;
1103
+ retryCount++;
1104
+ continue; // Retry the LLM call
1105
+ }
1106
+
1107
+ // Exhausted retries, return error
1108
+ console.error("All AI patches failed after retries:", patchErrors);
1109
+ return NextResponse.json(
1110
+ {
1111
+ success: false,
1112
+ error: `Patch application failed (after ${retryCount} retry attempts):\n\n${patchErrors.join("\n\n")}`,
1113
+ },
1114
+ { status: 400 }
1115
+ );
1116
+ }
1007
1117
 
1008
- // Log patch errors as warnings if some modifications succeeded
1009
- if (patchErrors.length > 0) {
1010
- console.warn("Some patches failed:", patchErrors);
1011
- }
1118
+ // Log patch errors as warnings if some modifications succeeded
1119
+ if (patchErrors.length > 0) {
1120
+ console.warn("Some patches failed:", patchErrors);
1121
+ }
1122
+
1123
+ // Successfully processed at least some modifications - break out of retry loop
1124
+ break;
1125
+ } // End of retry loop
1012
1126
 
1013
1127
  // Create backups and apply changes atomically
1014
1128
  const applyResult = await applyChangesWithBackup(
@@ -1029,8 +1143,8 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
1029
1143
  sessionId: newSessionId,
1030
1144
  modifications,
1031
1145
  backupPaths: applyResult.backupPaths,
1032
- explanation: aiResponse.explanation,
1033
- reasoning: aiResponse.reasoning,
1146
+ explanation: finalExplanation,
1147
+ reasoning: finalReasoning,
1034
1148
  });
1035
1149
  }
1036
1150
 
@@ -1754,6 +1868,7 @@ function searchFilesForKeywords(
1754
1868
  let cachedPathAliases: Map<string, string> | null = null;
1755
1869
  let cachedProjectRoot: string | null = null;
1756
1870
  let cachedTsconfigMtime: number | null = null;
1871
+ let cachedParseError: string | null = null; // Cache parse errors to avoid log spam
1757
1872
 
1758
1873
  /**
1759
1874
  * Clean tsconfig.json content to make it valid JSON
@@ -1826,21 +1941,23 @@ function getPathAliases(projectRoot: string): Map<string, string> {
1826
1941
  cachedTsconfigMtime = null;
1827
1942
  }
1828
1943
  } catch (e) {
1829
- // Log the error with more context for debugging
1830
1944
  const errorStr = String(e);
1831
- const posMatch = errorStr.match(/position (\d+)/);
1832
- let context = "";
1833
- if (posMatch) {
1834
- const pos = parseInt(posMatch[1], 10);
1835
- const content = fs.readFileSync(tsconfigPath, "utf-8");
1836
- context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
1945
+
1946
+ // Only log parse error once to avoid log spam
1947
+ if (!cachedParseError) {
1948
+ const posMatch = errorStr.match(/position (\d+)/);
1949
+ let context = "";
1950
+ if (posMatch) {
1951
+ const pos = parseInt(posMatch[1], 10);
1952
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
1953
+ context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
1954
+ }
1955
+ debugLog("[apply] Failed to parse tsconfig.json (will use defaults, logging once)", { error: errorStr, context });
1956
+ cachedParseError = errorStr;
1837
1957
  }
1838
- debugLog("[apply] Failed to parse tsconfig.json", { error: errorStr, context });
1839
1958
 
1840
- // Clear cache on error so we retry next time
1841
- cachedPathAliases = null;
1842
- cachedProjectRoot = null;
1843
- cachedTsconfigMtime = null;
1959
+ // Don't clear cache - we'll use the fallback aliases
1960
+ // cachedPathAliases will remain null, triggering fallback below
1844
1961
  }
1845
1962
  }
1846
1963
 
@@ -1855,13 +1972,16 @@ function getPathAliases(projectRoot: string): Map<string, string> {
1855
1972
  } else {
1856
1973
  aliases.set("@/", "");
1857
1974
  }
1858
- debugLog("[apply] Using default @/ alias", { alias: aliases.get("@/") });
1975
+ // Only log default alias once (not when using cached error fallback)
1976
+ if (!cachedParseError) {
1977
+ debugLog("[apply] Using default @/ alias", { alias: aliases.get("@/") });
1978
+ }
1859
1979
  }
1860
1980
 
1861
- // Only cache if we parsed successfully or there's no tsconfig
1862
- if (parsedSuccessfully || !fs.existsSync(tsconfigPath)) {
1863
- cachedPathAliases = aliases;
1864
- cachedProjectRoot = projectRoot;
1981
+ // Cache aliases if parsed successfully, no tsconfig exists, OR we have a parse error (cache fallback)
1982
+ if (parsedSuccessfully || !fs.existsSync(tsconfigPath) || cachedParseError) {
1983
+ cachedPathAliases = aliases;
1984
+ cachedProjectRoot = projectRoot;
1865
1985
  }
1866
1986
 
1867
1987
  return aliases;
@@ -466,6 +466,8 @@ Return search/replace patches (NOT full files). The system applies your patches
466
466
  - "replace" contains your modified version
467
467
  - Include 2-4 lines of context in "search" to make it unique
468
468
  - You may ONLY edit files provided in the PAGE CONTEXT section
469
+ - CRITICAL: NEVER invent or guess code. Your "search" string MUST be copied EXACTLY from the provided file content. If you cannot find the exact code to modify, return an empty modifications array.
470
+ - If the file content appears truncated, only modify code that is visible in the provided content.
469
471
 
470
472
  **SONANCE BRAND COLORS:**
471
473
  - Charcoal: #333F48 (primary text)
@@ -668,10 +670,10 @@ export async function POST(request: Request) {
668
670
  }
669
671
 
670
672
  // ========== SMART CONTEXT BUDGETING ==========
671
- // Total budget: 100k chars (~25k tokens, safe for Claude)
672
- // Priority: Recommended file (full) > Page file (limited) > Other components (dynamic)
673
- const TOTAL_CONTEXT_BUDGET = 100000;
674
- const MAX_RECOMMENDED_FILE = 80000; // 80k chars max for recommended file
673
+ // Claude can handle 200k tokens (~800k chars), so we can safely include large files
674
+ // Priority: Recommended file (NEVER truncate) > Page file (limited) > Other components (dynamic)
675
+ const TOTAL_CONTEXT_BUDGET = 500000; // 500k chars total budget
676
+ const MAX_RECOMMENDED_FILE = Infinity; // NEVER truncate the target file - AI needs full context
675
677
  const MAX_PAGE_FILE = 2000; // Page file is just a wrapper
676
678
  const MAX_GLOBALS_CSS = 1500;
677
679
  const MAX_FILES = 25;
@@ -705,9 +707,8 @@ ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}
705
707
 
706
708
  // ========== TARGET COMPONENT (RECOMMENDED FILE) - SHOWN FIRST ==========
707
709
  if (recommendedFileContent) {
708
- const content = recommendedFileContent.content.length > MAX_RECOMMENDED_FILE
709
- ? recommendedFileContent.content.substring(0, MAX_RECOMMENDED_FILE) + "\n// ... (file truncated at 50k chars, showing component definition and main logic)"
710
- : recommendedFileContent.content;
710
+ // Never truncate the recommended file - AI needs full context to avoid hallucination
711
+ const content = recommendedFileContent.content;
711
712
 
712
713
  textContent += `═══════════════════════════════════════════════════════════════════════════════
713
714
  ⚡ TARGET COMPONENT - YOU MUST EDIT THIS FILE
@@ -818,97 +819,153 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
818
819
  text: textContent,
819
820
  });
820
821
 
821
- // Call Claude Vision API
822
+ // Call Claude Vision API with retry mechanism
822
823
  const anthropic = new Anthropic({ apiKey });
823
-
824
- const response = await anthropic.messages.create({
825
- model: "claude-sonnet-4-20250514",
826
- max_tokens: 16384,
827
- messages: [
824
+
825
+ // Build list of known file paths (for logging)
826
+ const knownPaths = new Set<string>();
827
+ if (pageContext.pageFile) {
828
+ knownPaths.add(pageContext.pageFile);
829
+ }
830
+ for (const comp of pageContext.componentSources) {
831
+ knownPaths.add(comp.path);
832
+ }
833
+
834
+ // Retry loop for handling patch failures
835
+ const MAX_RETRIES = 1;
836
+ let retryCount = 0;
837
+ let lastPatchErrors: string[] = [];
838
+ let modificationsWithOriginals: VisionFileModification[] = [];
839
+ let finalExplanation: string | undefined;
840
+ let finalReasoning: string | undefined;
841
+ let finalAggregatedCSS: string | undefined;
842
+
843
+ while (retryCount <= MAX_RETRIES) {
844
+ // Build messages for this attempt
845
+ const currentMessages: Anthropic.MessageCreateParams["messages"] = [
828
846
  {
829
847
  role: "user",
830
848
  content: messageContent,
831
849
  },
832
- ],
833
- system: VISION_SYSTEM_PROMPT,
834
- });
850
+ ];
851
+
852
+ // If this is a retry, add feedback about what went wrong
853
+ if (retryCount > 0 && lastPatchErrors.length > 0) {
854
+ debugLog("Retry attempt with feedback", { retryCount, errorCount: lastPatchErrors.length });
855
+
856
+ // Get a snippet of the actual file content to help AI find correct code
857
+ let fileSnippet = "";
858
+ if (recommendedFileContent) {
859
+ // Show first 800 chars to give AI context about what code actually exists
860
+ fileSnippet = recommendedFileContent.content.substring(0, 800).replace(/\n/g, "\n");
861
+ }
862
+
863
+ currentMessages.push({
864
+ role: "assistant",
865
+ content: "I'll analyze the screenshot and generate the patches now.",
866
+ });
867
+ currentMessages.push({
868
+ role: "user",
869
+ content: `PATCH APPLICATION FAILED. Your previous patches referenced code that does not exist in the file (hallucination detected).
835
870
 
836
- // Extract text content from response
837
- const textResponse = response.content.find((block) => block.type === "text");
838
- if (!textResponse || textResponse.type !== "text") {
839
- return NextResponse.json(
840
- { error: "No text response from AI" },
841
- { status: 500 }
842
- );
843
- }
871
+ Failed patches:
872
+ ${lastPatchErrors.join("\n\n")}
844
873
 
845
- // Parse AI response - now expecting patches instead of full file content
846
- let aiResponse: {
847
- reasoning?: string;
848
- modifications: Array<{
849
- filePath: string;
850
- patches?: Patch[];
851
- // Legacy support for modifiedContent (will be deprecated)
852
- modifiedContent?: string;
853
- explanation?: string;
854
- previewCSS?: string;
855
- }>;
856
- aggregatedPreviewCSS?: string;
857
- explanation?: string;
858
- };
874
+ ${fileSnippet ? `Here is the ACTUAL beginning of the file you're trying to edit:
875
+ \`\`\`tsx
876
+ ${fileSnippet}...
877
+ \`\`\`
878
+ ` : ""}
879
+ CRITICAL INSTRUCTIONS:
880
+ 1. Your "search" string MUST be copied EXACTLY from the file content I provided above
881
+ 2. Do NOT invent, guess, or imagine code that might exist
882
+ 3. Look for the ACTUAL code in the TARGET COMPONENT section and copy it exactly
859
883
 
860
- try {
861
- let jsonText = textResponse.text.trim();
862
-
863
- // Try to extract JSON from markdown code blocks
864
- const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
865
- jsonText.match(/```\n([\s\S]*?)\n```/);
866
-
867
- if (jsonMatch) {
868
- jsonText = jsonMatch[1];
869
- } else if (jsonText.includes("```json")) {
870
- // Fallback for cases where regex might miss due to newlines
871
- const start = jsonText.indexOf("```json") + 7;
872
- const end = jsonText.lastIndexOf("```");
873
- if (end > start) {
874
- jsonText = jsonText.substring(start, end);
875
- }
884
+ If you still cannot find the exact code to modify after reviewing the file:
885
+ - Return {"modifications": [], "explanation": "Could not locate the exact code. Please specify which element you want to change more precisely."}
886
+
887
+ This is better than generating patches with made-up code.`,
888
+ });
876
889
  }
877
890
 
878
- // Clean up any remaining whitespace
879
- jsonText = jsonText.trim();
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);
891
+ const response = await anthropic.messages.create({
892
+ model: "claude-sonnet-4-20250514",
893
+ max_tokens: 16384,
894
+ messages: currentMessages,
895
+ system: VISION_SYSTEM_PROMPT,
896
+ });
897
+
898
+ // Extract text content from response
899
+ const textResponse = response.content.find((block) => block.type === "text");
900
+ if (!textResponse || textResponse.type !== "text") {
901
+ return NextResponse.json(
902
+ { error: "No text response from AI" },
903
+ { status: 500 }
904
+ );
887
905
  }
888
-
889
- aiResponse = JSON.parse(jsonText);
890
- } catch {
891
- console.error("Failed to parse AI response:", textResponse.text);
892
- return NextResponse.json(
893
- { error: "Failed to parse AI response. Please try again." },
894
- { status: 500 }
895
- );
896
- }
897
906
 
898
- // Build list of known file paths (for logging)
899
- const knownPaths = new Set<string>();
900
- if (pageContext.pageFile) {
901
- knownPaths.add(pageContext.pageFile);
902
- }
903
- for (const comp of pageContext.componentSources) {
904
- knownPaths.add(comp.path);
905
- }
907
+ // Parse AI response - now expecting patches instead of full file content
908
+ let aiResponse: {
909
+ reasoning?: string;
910
+ modifications: Array<{
911
+ filePath: string;
912
+ patches?: Patch[];
913
+ // Legacy support for modifiedContent (will be deprecated)
914
+ modifiedContent?: string;
915
+ explanation?: string;
916
+ previewCSS?: string;
917
+ }>;
918
+ aggregatedPreviewCSS?: string;
919
+ explanation?: string;
920
+ };
906
921
 
907
- debugLog("VALIDATION: Known file paths from page context", {
908
- pageFile: pageContext.pageFile,
909
- knownPaths: Array.from(knownPaths),
910
- aiRequestedFiles: (aiResponse.modifications || []).map(m => m.filePath)
911
- });
922
+ try {
923
+ let jsonText = textResponse.text.trim();
924
+
925
+ // Try to extract JSON from markdown code blocks
926
+ const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
927
+ jsonText.match(/```\n([\s\S]*?)\n```/);
928
+
929
+ if (jsonMatch) {
930
+ jsonText = jsonMatch[1];
931
+ } else if (jsonText.includes("```json")) {
932
+ // Fallback for cases where regex might miss due to newlines
933
+ const start = jsonText.indexOf("```json") + 7;
934
+ const end = jsonText.lastIndexOf("```");
935
+ if (end > start) {
936
+ jsonText = jsonText.substring(start, end);
937
+ }
938
+ }
939
+
940
+ // Clean up any remaining whitespace
941
+ jsonText = jsonText.trim();
942
+
943
+ // Robust JSON extraction: find the first { and last } to extract JSON object
944
+ // This handles cases where the LLM includes preamble text before the JSON
945
+ const firstBrace = jsonText.indexOf('{');
946
+ const lastBrace = jsonText.lastIndexOf('}');
947
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
948
+ jsonText = jsonText.substring(firstBrace, lastBrace + 1);
949
+ }
950
+
951
+ aiResponse = JSON.parse(jsonText);
952
+ } catch {
953
+ console.error("Failed to parse AI response:", textResponse.text);
954
+ return NextResponse.json(
955
+ { error: "Failed to parse AI response. Please try again." },
956
+ { status: 500 }
957
+ );
958
+ }
959
+
960
+ finalExplanation = aiResponse.explanation;
961
+ finalReasoning = aiResponse.reasoning;
962
+ finalAggregatedCSS = aiResponse.aggregatedPreviewCSS;
963
+
964
+ debugLog("VALIDATION: Known file paths from page context", {
965
+ pageFile: pageContext.pageFile,
966
+ knownPaths: Array.from(knownPaths),
967
+ aiRequestedFiles: (aiResponse.modifications || []).map(m => m.filePath)
968
+ });
912
969
 
913
970
  // Validate AI response - trust the LLM to identify the correct file
914
971
  // Only reject paths that are outside the project or don't exist
@@ -949,9 +1006,9 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
949
1006
  }
950
1007
  }
951
1008
 
952
- // Process modifications - apply patches to get modified content
953
- const modificationsWithOriginals: VisionFileModification[] = [];
954
- const patchErrors: string[] = [];
1009
+ // Process modifications - apply patches to get modified content
1010
+ modificationsWithOriginals = [];
1011
+ const patchErrors: string[] = [];
955
1012
 
956
1013
  for (const mod of aiResponse.modifications || []) {
957
1014
  const fullPath = path.join(projectRoot, mod.filePath);
@@ -968,6 +1025,47 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
968
1025
  // New patch-based approach
969
1026
  console.log(`[Vision Mode] Applying ${mod.patches.length} patches to ${mod.filePath}`);
970
1027
 
1028
+ // PRE-VALIDATION: Check if all search strings exist in the file BEFORE applying
1029
+ const preValidationErrors: string[] = [];
1030
+ for (const patch of mod.patches) {
1031
+ const normalizedSearch = patch.search.replace(/\\n/g, "\n");
1032
+ if (!originalContent.includes(normalizedSearch)) {
1033
+ // Try fuzzy match as fallback
1034
+ const fuzzyMatch = findFuzzyMatch(normalizedSearch, originalContent);
1035
+ if (!fuzzyMatch) {
1036
+ // Find the closest matching snippet to help with debugging
1037
+ const searchPreview = normalizedSearch.substring(0, 80).replace(/\n/g, "\\n");
1038
+
1039
+ // Look for partial matches to give helpful feedback
1040
+ const searchLines = normalizedSearch.split("\n").filter(l => l.trim().length > 10);
1041
+ const partialMatches: string[] = [];
1042
+ for (const line of searchLines.slice(0, 3)) {
1043
+ const trimmedLine = line.trim();
1044
+ if (trimmedLine.length > 10 && originalContent.includes(trimmedLine)) {
1045
+ partialMatches.push(trimmedLine.substring(0, 50));
1046
+ }
1047
+ }
1048
+
1049
+ let errorMsg = `Patch search string not found: "${searchPreview}..."`;
1050
+ if (partialMatches.length > 0) {
1051
+ errorMsg += ` (partial matches found: ${partialMatches.join(", ")})`;
1052
+ }
1053
+ preValidationErrors.push(errorMsg);
1054
+ debugLog("Pre-validation failed: search string not found", {
1055
+ filePath: mod.filePath,
1056
+ searchPreview,
1057
+ partialMatches
1058
+ });
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ // If pre-validation failed, add to errors and skip this file
1064
+ if (preValidationErrors.length > 0) {
1065
+ patchErrors.push(`${mod.filePath}: AI generated patches with non-existent code (hallucination detected):\n${preValidationErrors.join("\n")}`);
1066
+ continue;
1067
+ }
1068
+
971
1069
  const patchResult = applyPatches(originalContent, mod.patches);
972
1070
 
973
1071
  if (!patchResult.success) {
@@ -1015,22 +1113,39 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
1015
1113
  });
1016
1114
  }
1017
1115
 
1018
- // If all modifications failed, return error
1019
- if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
1020
- console.error("All AI patches failed:", patchErrors);
1021
- return NextResponse.json(
1022
- {
1023
- success: false,
1024
- error: `Patch application failed:\n\n${patchErrors.join("\n\n")}`,
1025
- } as VisionEditResponse,
1026
- { status: 400 }
1027
- );
1028
- }
1116
+ // If all modifications failed, check if we should retry
1117
+ if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
1118
+ if (retryCount < MAX_RETRIES) {
1119
+ console.warn(`[Vision Mode] All patches failed, retrying (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
1120
+ debugLog("Retry triggered due to patch failures", {
1121
+ retryCount,
1122
+ errorCount: patchErrors.length,
1123
+ errors: patchErrors.slice(0, 3) // Log first 3 errors
1124
+ });
1125
+ lastPatchErrors = patchErrors;
1126
+ retryCount++;
1127
+ continue; // Retry the LLM call
1128
+ }
1129
+
1130
+ // Exhausted retries, return error
1131
+ console.error("All AI patches failed after retries:", patchErrors);
1132
+ return NextResponse.json(
1133
+ {
1134
+ success: false,
1135
+ error: `Patch application failed (after ${retryCount} retry attempts):\n\n${patchErrors.join("\n\n")}`,
1136
+ } as VisionEditResponse,
1137
+ { status: 400 }
1138
+ );
1139
+ }
1029
1140
 
1030
- // Log patch errors as warnings if some modifications succeeded
1031
- if (patchErrors.length > 0) {
1032
- console.warn("Some patches failed:", patchErrors);
1033
- }
1141
+ // Log patch errors as warnings if some modifications succeeded
1142
+ if (patchErrors.length > 0) {
1143
+ console.warn("Some patches failed:", patchErrors);
1144
+ }
1145
+
1146
+ // Successfully processed at least some modifications - break out of retry loop
1147
+ break;
1148
+ } // End of retry loop
1034
1149
 
1035
1150
  // Aggregate preview CSS
1036
1151
  const aggregatedCSS = modificationsWithOriginals
@@ -1041,9 +1156,9 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
1041
1156
  return NextResponse.json({
1042
1157
  success: true,
1043
1158
  modifications: modificationsWithOriginals,
1044
- aggregatedPreviewCSS: aiResponse.aggregatedPreviewCSS || aggregatedCSS,
1045
- explanation: aiResponse.explanation,
1046
- reasoning: aiResponse.reasoning,
1159
+ aggregatedPreviewCSS: finalAggregatedCSS || aggregatedCSS,
1160
+ explanation: finalExplanation,
1161
+ reasoning: finalReasoning,
1047
1162
  } as VisionEditResponse);
1048
1163
  }
1049
1164
 
@@ -1658,6 +1773,7 @@ function searchFilesForKeywords(
1658
1773
  let cachedPathAliases: Map<string, string> | null = null;
1659
1774
  let cachedProjectRoot: string | null = null;
1660
1775
  let cachedTsconfigMtime: number | null = null;
1776
+ let cachedParseError: string | null = null; // Cache parse errors to avoid log spam
1661
1777
 
1662
1778
  /**
1663
1779
  * Clean tsconfig.json content to make it valid JSON
@@ -1730,21 +1846,23 @@ function getPathAliases(projectRoot: string): Map<string, string> {
1730
1846
  cachedTsconfigMtime = null;
1731
1847
  }
1732
1848
  } catch (e) {
1733
- // Log the error with more context for debugging
1734
1849
  const errorStr = String(e);
1735
- const posMatch = errorStr.match(/position (\d+)/);
1736
- let context = "";
1737
- if (posMatch) {
1738
- const pos = parseInt(posMatch[1], 10);
1739
- const content = fs.readFileSync(tsconfigPath, "utf-8");
1740
- context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
1850
+
1851
+ // Only log parse error once to avoid log spam
1852
+ if (!cachedParseError) {
1853
+ const posMatch = errorStr.match(/position (\d+)/);
1854
+ let context = "";
1855
+ if (posMatch) {
1856
+ const pos = parseInt(posMatch[1], 10);
1857
+ const content = fs.readFileSync(tsconfigPath, "utf-8");
1858
+ context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
1859
+ }
1860
+ debugLog("[edit] Failed to parse tsconfig.json (will use defaults, logging once)", { error: errorStr, context });
1861
+ cachedParseError = errorStr;
1741
1862
  }
1742
- debugLog("[edit] Failed to parse tsconfig.json", { error: errorStr, context });
1743
1863
 
1744
- // Clear cache on error so we retry next time
1745
- cachedPathAliases = null;
1746
- cachedProjectRoot = null;
1747
- cachedTsconfigMtime = null;
1864
+ // Don't clear cache - we'll use the fallback aliases
1865
+ // cachedPathAliases will remain null, triggering fallback below
1748
1866
  }
1749
1867
  }
1750
1868
 
@@ -1759,13 +1877,16 @@ function getPathAliases(projectRoot: string): Map<string, string> {
1759
1877
  } else {
1760
1878
  aliases.set("@/", "");
1761
1879
  }
1762
- debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
1880
+ // Only log default alias once (not when using cached error fallback)
1881
+ if (!cachedParseError) {
1882
+ debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
1883
+ }
1763
1884
  }
1764
1885
 
1765
- // Only cache if we parsed successfully or there's no tsconfig
1766
- if (parsedSuccessfully || !fs.existsSync(tsconfigPath)) {
1767
- cachedPathAliases = aliases;
1768
- cachedProjectRoot = projectRoot;
1886
+ // Cache aliases if parsed successfully, no tsconfig exists, OR we have a parse error (cache fallback)
1887
+ if (parsedSuccessfully || !fs.existsSync(tsconfigPath) || cachedParseError) {
1888
+ cachedPathAliases = aliases;
1889
+ cachedProjectRoot = projectRoot;
1769
1890
  }
1770
1891
 
1771
1892
  return aliases;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.47",
3
+ "version": "1.3.49",
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",