sonance-brand-mcp 1.3.47 → 1.3.48

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)
@@ -658,10 +660,10 @@ export async function POST(request: Request) {
658
660
  }
659
661
 
660
662
  // ========== 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
663
+ // Claude can handle 200k tokens (~800k chars), so we can safely include large files
664
+ // Priority: Recommended file (NEVER truncate) > Page file (limited) > Other components (dynamic)
665
+ const TOTAL_CONTEXT_BUDGET = 500000; // 500k chars total budget
666
+ const MAX_RECOMMENDED_FILE = Infinity; // NEVER truncate the target file - AI needs full context
665
667
  const MAX_PAGE_FILE = 2000; // Page file is just a wrapper
666
668
  const MAX_GLOBALS_CSS = 1500;
667
669
  const MAX_FILES = 25;
@@ -695,9 +697,8 @@ ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}
695
697
 
696
698
  // ========== TARGET COMPONENT (RECOMMENDED FILE) - SHOWN FIRST ==========
697
699
  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;
700
+ // Never truncate the recommended file - AI needs full context to avoid hallucination
701
+ const content = recommendedFileContent.content;
701
702
 
702
703
  textContent += `═══════════════════════════════════════════════════════════════════════════════
703
704
  ⚡ TARGET COMPONENT - YOU MUST EDIT THIS FILE
@@ -808,112 +809,148 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
808
809
  text: textContent,
809
810
  });
810
811
 
811
- // Call Claude Vision API
812
+ // Call Claude Vision API with retry mechanism
812
813
  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: [
814
+
815
+ // Build set of valid file paths from page context (needed for validation)
816
+ const validFilePaths = new Set<string>();
817
+ if (pageContext.pageFile) {
818
+ validFilePaths.add(pageContext.pageFile);
819
+ }
820
+ for (const comp of pageContext.componentSources) {
821
+ validFilePaths.add(comp.path);
822
+ }
823
+
824
+ // FIX: Add the recommended file to validFilePaths (it was spliced out for display purposes)
825
+ if (recommendedFileContent) {
826
+ validFilePaths.add(recommendedFileContent.path);
827
+ }
828
+
829
+ // Retry loop for handling patch failures
830
+ const MAX_RETRIES = 1;
831
+ let retryCount = 0;
832
+ let lastPatchErrors: string[] = [];
833
+ let modifications: VisionFileModification[] = [];
834
+ let finalExplanation: string | undefined;
835
+ let finalReasoning: string | undefined;
836
+
837
+ while (retryCount <= MAX_RETRIES) {
838
+ // Build messages for this attempt
839
+ const currentMessages: Anthropic.MessageCreateParams["messages"] = [
818
840
  {
819
841
  role: "user",
820
842
  content: messageContent,
821
843
  },
822
- ],
823
- system: VISION_SYSTEM_PROMPT,
824
- });
844
+ ];
845
+
846
+ // If this is a retry, add feedback about what went wrong
847
+ if (retryCount > 0 && lastPatchErrors.length > 0) {
848
+ debugLog("Retry attempt with feedback", { retryCount, errorCount: lastPatchErrors.length });
849
+ currentMessages.push({
850
+ role: "assistant",
851
+ content: "I'll analyze the screenshot and generate the patches now.",
852
+ });
853
+ currentMessages.push({
854
+ role: "user",
855
+ content: `PATCH APPLICATION FAILED. Your previous patches referenced code that does not exist in the file (hallucination detected).
825
856
 
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
- }
857
+ Failed patches:
858
+ ${lastPatchErrors.join("\n\n")}
859
+
860
+ IMPORTANT: You must copy the "search" string EXACTLY from the file content I provided. Do NOT invent or guess code.
861
+ Look carefully at the ACTUAL file content in the TARGET COMPONENT section above and try again.
862
+
863
+ If you cannot find the exact code to modify, return an empty modifications array with an explanation.`,
864
+ });
865
+ }
834
866
 
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;
867
+ const response = await anthropic.messages.create({
868
+ model: "claude-sonnet-4-20250514",
869
+ max_tokens: 16384,
870
+ messages: currentMessages,
871
+ system: VISION_SYSTEM_PROMPT,
872
+ });
873
+
874
+ // Extract text content from response
875
+ const textResponse = response.content.find((block) => block.type === "text");
876
+ if (!textResponse || textResponse.type !== "text") {
877
+ return NextResponse.json(
878
+ { error: "No text response from AI" },
879
+ { status: 500 }
880
+ );
881
+ }
882
+
883
+ // Parse AI response - now expecting patches instead of full file content
884
+ let aiResponse: {
885
+ reasoning?: string;
886
+ modifications: Array<{
887
+ filePath: string;
888
+ patches?: Patch[];
889
+ // Legacy support for modifiedContent (will be deprecated)
890
+ modifiedContent?: string;
891
+ explanation?: string;
892
+ }>;
843
893
  explanation?: string;
844
- }>;
845
- explanation?: string;
846
- };
894
+ };
847
895
 
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);
896
+ try {
897
+ let jsonText = textResponse.text.trim();
898
+
899
+ // Try to extract JSON from markdown code blocks
900
+ const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
901
+ jsonText.match(/```\n([\s\S]*?)\n```/);
902
+
903
+ if (jsonMatch) {
904
+ jsonText = jsonMatch[1];
905
+ } else if (jsonText.includes("```json")) {
906
+ const start = jsonText.indexOf("```json") + 7;
907
+ const end = jsonText.lastIndexOf("```");
908
+ if (end > start) {
909
+ jsonText = jsonText.substring(start, end);
910
+ }
862
911
  }
863
- }
864
912
 
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);
913
+ jsonText = jsonText.trim();
914
+
915
+ // Robust JSON extraction: find the first { and last } to extract JSON object
916
+ // This handles cases where the LLM includes preamble text before the JSON
917
+ const firstBrace = jsonText.indexOf('{');
918
+ const lastBrace = jsonText.lastIndexOf('}');
919
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
920
+ jsonText = jsonText.substring(firstBrace, lastBrace + 1);
921
+ }
922
+
923
+ aiResponse = JSON.parse(jsonText);
924
+ } catch {
925
+ console.error("Failed to parse AI response:", textResponse.text);
926
+ return NextResponse.json(
927
+ { error: "Failed to parse AI response. Please try again." },
928
+ { status: 500 }
929
+ );
873
930
  }
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
931
 
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
- }
932
+ finalExplanation = aiResponse.explanation;
933
+ finalReasoning = aiResponse.reasoning;
893
934
 
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
- }
935
+ if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
936
+ return NextResponse.json({
937
+ success: true,
938
+ sessionId: newSessionId,
939
+ modifications: [],
940
+ explanation: aiResponse.explanation || "No changes needed.",
941
+ reasoning: aiResponse.reasoning,
942
+ });
943
+ }
907
944
 
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
- });
945
+ debugLog("VALIDATION: Valid file paths from page context", {
946
+ pageFile: pageContext.pageFile,
947
+ validFilePaths: Array.from(validFilePaths),
948
+ aiRequestedFiles: aiResponse.modifications.map(m => m.filePath)
949
+ });
913
950
 
914
- // Process modifications - apply patches to get modified content
915
- const modifications: VisionFileModification[] = [];
916
- const patchErrors: string[] = [];
951
+ // Process modifications - apply patches to get modified content
952
+ modifications = [];
953
+ const patchErrors: string[] = [];
917
954
 
918
955
  for (const mod of aiResponse.modifications) {
919
956
  // Validate that the file path is in the page context
@@ -947,6 +984,47 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
947
984
  // New patch-based approach
948
985
  console.log(`[Apply-First] Applying ${mod.patches.length} patches to ${mod.filePath}`);
949
986
 
987
+ // PRE-VALIDATION: Check if all search strings exist in the file BEFORE applying
988
+ const preValidationErrors: string[] = [];
989
+ for (const patch of mod.patches) {
990
+ const normalizedSearch = patch.search.replace(/\\n/g, "\n");
991
+ if (!originalContent.includes(normalizedSearch)) {
992
+ // Try fuzzy match as fallback
993
+ const fuzzyMatch = findFuzzyMatch(normalizedSearch, originalContent);
994
+ if (!fuzzyMatch) {
995
+ // Find the closest matching snippet to help with debugging
996
+ const searchPreview = normalizedSearch.substring(0, 80).replace(/\n/g, "\\n");
997
+
998
+ // Look for partial matches to give helpful feedback
999
+ const searchLines = normalizedSearch.split("\n").filter(l => l.trim().length > 10);
1000
+ const partialMatches: string[] = [];
1001
+ for (const line of searchLines.slice(0, 3)) {
1002
+ const trimmedLine = line.trim();
1003
+ if (trimmedLine.length > 10 && originalContent.includes(trimmedLine)) {
1004
+ partialMatches.push(trimmedLine.substring(0, 50));
1005
+ }
1006
+ }
1007
+
1008
+ let errorMsg = `Patch search string not found: "${searchPreview}..."`;
1009
+ if (partialMatches.length > 0) {
1010
+ errorMsg += ` (partial matches found: ${partialMatches.join(", ")})`;
1011
+ }
1012
+ preValidationErrors.push(errorMsg);
1013
+ debugLog("Pre-validation failed: search string not found", {
1014
+ filePath: mod.filePath,
1015
+ searchPreview,
1016
+ partialMatches
1017
+ });
1018
+ }
1019
+ }
1020
+ }
1021
+
1022
+ // If pre-validation failed, add to errors and skip this file
1023
+ if (preValidationErrors.length > 0) {
1024
+ patchErrors.push(`${mod.filePath}: AI generated patches with non-existent code (hallucination detected):\n${preValidationErrors.join("\n")}`);
1025
+ continue;
1026
+ }
1027
+
950
1028
  const patchResult = applyPatches(originalContent, mod.patches);
951
1029
 
952
1030
  if (!patchResult.success) {
@@ -993,22 +1071,39 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
993
1071
  });
994
1072
  }
995
1073
 
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
- }
1074
+ // If all modifications failed, check if we should retry
1075
+ if (patchErrors.length > 0 && modifications.length === 0) {
1076
+ if (retryCount < MAX_RETRIES) {
1077
+ console.warn(`[Apply-First] All patches failed, retrying (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
1078
+ debugLog("Retry triggered due to patch failures", {
1079
+ retryCount,
1080
+ errorCount: patchErrors.length,
1081
+ errors: patchErrors.slice(0, 3) // Log first 3 errors
1082
+ });
1083
+ lastPatchErrors = patchErrors;
1084
+ retryCount++;
1085
+ continue; // Retry the LLM call
1086
+ }
1087
+
1088
+ // Exhausted retries, return error
1089
+ console.error("All AI patches failed after retries:", patchErrors);
1090
+ return NextResponse.json(
1091
+ {
1092
+ success: false,
1093
+ error: `Patch application failed (after ${retryCount} retry attempts):\n\n${patchErrors.join("\n\n")}`,
1094
+ },
1095
+ { status: 400 }
1096
+ );
1097
+ }
1007
1098
 
1008
- // Log patch errors as warnings if some modifications succeeded
1009
- if (patchErrors.length > 0) {
1010
- console.warn("Some patches failed:", patchErrors);
1011
- }
1099
+ // Log patch errors as warnings if some modifications succeeded
1100
+ if (patchErrors.length > 0) {
1101
+ console.warn("Some patches failed:", patchErrors);
1102
+ }
1103
+
1104
+ // Successfully processed at least some modifications - break out of retry loop
1105
+ break;
1106
+ } // End of retry loop
1012
1107
 
1013
1108
  // Create backups and apply changes atomically
1014
1109
  const applyResult = await applyChangesWithBackup(
@@ -1029,8 +1124,8 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
1029
1124
  sessionId: newSessionId,
1030
1125
  modifications,
1031
1126
  backupPaths: applyResult.backupPaths,
1032
- explanation: aiResponse.explanation,
1033
- reasoning: aiResponse.reasoning,
1127
+ explanation: finalExplanation,
1128
+ reasoning: finalReasoning,
1034
1129
  });
1035
1130
  }
1036
1131
 
@@ -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,135 @@ 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
+ currentMessages.push({
856
+ role: "assistant",
857
+ content: "I'll analyze the screenshot and generate the patches now.",
858
+ });
859
+ currentMessages.push({
860
+ role: "user",
861
+ content: `PATCH APPLICATION FAILED. Your previous patches referenced code that does not exist in the file (hallucination detected).
835
862
 
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
- }
863
+ Failed patches:
864
+ ${lastPatchErrors.join("\n\n")}
844
865
 
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
- };
866
+ IMPORTANT: You must copy the "search" string EXACTLY from the file content I provided. Do NOT invent or guess code.
867
+ Look carefully at the ACTUAL file content in the TARGET COMPONENT section above and try again.
859
868
 
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
- }
869
+ If you cannot find the exact code to modify, return an empty modifications array with an explanation.`,
870
+ });
876
871
  }
877
872
 
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);
873
+ const response = await anthropic.messages.create({
874
+ model: "claude-sonnet-4-20250514",
875
+ max_tokens: 16384,
876
+ messages: currentMessages,
877
+ system: VISION_SYSTEM_PROMPT,
878
+ });
879
+
880
+ // Extract text content from response
881
+ const textResponse = response.content.find((block) => block.type === "text");
882
+ if (!textResponse || textResponse.type !== "text") {
883
+ return NextResponse.json(
884
+ { error: "No text response from AI" },
885
+ { status: 500 }
886
+ );
887
887
  }
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
888
 
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
- }
889
+ // Parse AI response - now expecting patches instead of full file content
890
+ let aiResponse: {
891
+ reasoning?: string;
892
+ modifications: Array<{
893
+ filePath: string;
894
+ patches?: Patch[];
895
+ // Legacy support for modifiedContent (will be deprecated)
896
+ modifiedContent?: string;
897
+ explanation?: string;
898
+ previewCSS?: string;
899
+ }>;
900
+ aggregatedPreviewCSS?: string;
901
+ explanation?: string;
902
+ };
906
903
 
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
- });
904
+ try {
905
+ let jsonText = textResponse.text.trim();
906
+
907
+ // Try to extract JSON from markdown code blocks
908
+ const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
909
+ jsonText.match(/```\n([\s\S]*?)\n```/);
910
+
911
+ if (jsonMatch) {
912
+ jsonText = jsonMatch[1];
913
+ } else if (jsonText.includes("```json")) {
914
+ // Fallback for cases where regex might miss due to newlines
915
+ const start = jsonText.indexOf("```json") + 7;
916
+ const end = jsonText.lastIndexOf("```");
917
+ if (end > start) {
918
+ jsonText = jsonText.substring(start, end);
919
+ }
920
+ }
921
+
922
+ // Clean up any remaining whitespace
923
+ jsonText = jsonText.trim();
924
+
925
+ // Robust JSON extraction: find the first { and last } to extract JSON object
926
+ // This handles cases where the LLM includes preamble text before the JSON
927
+ const firstBrace = jsonText.indexOf('{');
928
+ const lastBrace = jsonText.lastIndexOf('}');
929
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
930
+ jsonText = jsonText.substring(firstBrace, lastBrace + 1);
931
+ }
932
+
933
+ aiResponse = JSON.parse(jsonText);
934
+ } catch {
935
+ console.error("Failed to parse AI response:", textResponse.text);
936
+ return NextResponse.json(
937
+ { error: "Failed to parse AI response. Please try again." },
938
+ { status: 500 }
939
+ );
940
+ }
941
+
942
+ finalExplanation = aiResponse.explanation;
943
+ finalReasoning = aiResponse.reasoning;
944
+ finalAggregatedCSS = aiResponse.aggregatedPreviewCSS;
945
+
946
+ debugLog("VALIDATION: Known file paths from page context", {
947
+ pageFile: pageContext.pageFile,
948
+ knownPaths: Array.from(knownPaths),
949
+ aiRequestedFiles: (aiResponse.modifications || []).map(m => m.filePath)
950
+ });
912
951
 
913
952
  // Validate AI response - trust the LLM to identify the correct file
914
953
  // Only reject paths that are outside the project or don't exist
@@ -949,9 +988,9 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
949
988
  }
950
989
  }
951
990
 
952
- // Process modifications - apply patches to get modified content
953
- const modificationsWithOriginals: VisionFileModification[] = [];
954
- const patchErrors: string[] = [];
991
+ // Process modifications - apply patches to get modified content
992
+ modificationsWithOriginals = [];
993
+ const patchErrors: string[] = [];
955
994
 
956
995
  for (const mod of aiResponse.modifications || []) {
957
996
  const fullPath = path.join(projectRoot, mod.filePath);
@@ -968,6 +1007,47 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
968
1007
  // New patch-based approach
969
1008
  console.log(`[Vision Mode] Applying ${mod.patches.length} patches to ${mod.filePath}`);
970
1009
 
1010
+ // PRE-VALIDATION: Check if all search strings exist in the file BEFORE applying
1011
+ const preValidationErrors: string[] = [];
1012
+ for (const patch of mod.patches) {
1013
+ const normalizedSearch = patch.search.replace(/\\n/g, "\n");
1014
+ if (!originalContent.includes(normalizedSearch)) {
1015
+ // Try fuzzy match as fallback
1016
+ const fuzzyMatch = findFuzzyMatch(normalizedSearch, originalContent);
1017
+ if (!fuzzyMatch) {
1018
+ // Find the closest matching snippet to help with debugging
1019
+ const searchPreview = normalizedSearch.substring(0, 80).replace(/\n/g, "\\n");
1020
+
1021
+ // Look for partial matches to give helpful feedback
1022
+ const searchLines = normalizedSearch.split("\n").filter(l => l.trim().length > 10);
1023
+ const partialMatches: string[] = [];
1024
+ for (const line of searchLines.slice(0, 3)) {
1025
+ const trimmedLine = line.trim();
1026
+ if (trimmedLine.length > 10 && originalContent.includes(trimmedLine)) {
1027
+ partialMatches.push(trimmedLine.substring(0, 50));
1028
+ }
1029
+ }
1030
+
1031
+ let errorMsg = `Patch search string not found: "${searchPreview}..."`;
1032
+ if (partialMatches.length > 0) {
1033
+ errorMsg += ` (partial matches found: ${partialMatches.join(", ")})`;
1034
+ }
1035
+ preValidationErrors.push(errorMsg);
1036
+ debugLog("Pre-validation failed: search string not found", {
1037
+ filePath: mod.filePath,
1038
+ searchPreview,
1039
+ partialMatches
1040
+ });
1041
+ }
1042
+ }
1043
+ }
1044
+
1045
+ // If pre-validation failed, add to errors and skip this file
1046
+ if (preValidationErrors.length > 0) {
1047
+ patchErrors.push(`${mod.filePath}: AI generated patches with non-existent code (hallucination detected):\n${preValidationErrors.join("\n")}`);
1048
+ continue;
1049
+ }
1050
+
971
1051
  const patchResult = applyPatches(originalContent, mod.patches);
972
1052
 
973
1053
  if (!patchResult.success) {
@@ -1015,22 +1095,39 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
1015
1095
  });
1016
1096
  }
1017
1097
 
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
- }
1098
+ // If all modifications failed, check if we should retry
1099
+ if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
1100
+ if (retryCount < MAX_RETRIES) {
1101
+ console.warn(`[Vision Mode] All patches failed, retrying (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
1102
+ debugLog("Retry triggered due to patch failures", {
1103
+ retryCount,
1104
+ errorCount: patchErrors.length,
1105
+ errors: patchErrors.slice(0, 3) // Log first 3 errors
1106
+ });
1107
+ lastPatchErrors = patchErrors;
1108
+ retryCount++;
1109
+ continue; // Retry the LLM call
1110
+ }
1111
+
1112
+ // Exhausted retries, return error
1113
+ console.error("All AI patches failed after retries:", patchErrors);
1114
+ return NextResponse.json(
1115
+ {
1116
+ success: false,
1117
+ error: `Patch application failed (after ${retryCount} retry attempts):\n\n${patchErrors.join("\n\n")}`,
1118
+ } as VisionEditResponse,
1119
+ { status: 400 }
1120
+ );
1121
+ }
1029
1122
 
1030
- // Log patch errors as warnings if some modifications succeeded
1031
- if (patchErrors.length > 0) {
1032
- console.warn("Some patches failed:", patchErrors);
1033
- }
1123
+ // Log patch errors as warnings if some modifications succeeded
1124
+ if (patchErrors.length > 0) {
1125
+ console.warn("Some patches failed:", patchErrors);
1126
+ }
1127
+
1128
+ // Successfully processed at least some modifications - break out of retry loop
1129
+ break;
1130
+ } // End of retry loop
1034
1131
 
1035
1132
  // Aggregate preview CSS
1036
1133
  const aggregatedCSS = modificationsWithOriginals
@@ -1041,9 +1138,9 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
1041
1138
  return NextResponse.json({
1042
1139
  success: true,
1043
1140
  modifications: modificationsWithOriginals,
1044
- aggregatedPreviewCSS: aiResponse.aggregatedPreviewCSS || aggregatedCSS,
1045
- explanation: aiResponse.explanation,
1046
- reasoning: aiResponse.reasoning,
1141
+ aggregatedPreviewCSS: finalAggregatedCSS || aggregatedCSS,
1142
+ explanation: finalExplanation,
1143
+ reasoning: finalReasoning,
1047
1144
  } as VisionEditResponse);
1048
1145
  }
1049
1146
 
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.48",
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",