sonance-brand-mcp 1.3.46 → 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.
|
@@ -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
|
-
//
|
|
672
|
-
// Priority: Recommended file (
|
|
673
|
-
const TOTAL_CONTEXT_BUDGET =
|
|
674
|
-
const MAX_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
|
-
|
|
709
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
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
|
-
|
|
837
|
-
|
|
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
|
-
|
|
846
|
-
|
|
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
|
-
|
|
861
|
-
|
|
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
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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:
|
|
1045
|
-
explanation:
|
|
1046
|
-
reasoning:
|
|
1141
|
+
aggregatedPreviewCSS: finalAggregatedCSS || aggregatedCSS,
|
|
1142
|
+
explanation: finalExplanation,
|
|
1143
|
+
reasoning: finalReasoning,
|
|
1047
1144
|
} as VisionEditResponse);
|
|
1048
1145
|
}
|
|
1049
1146
|
|
|
@@ -1657,32 +1754,58 @@ function searchFilesForKeywords(
|
|
|
1657
1754
|
// Cache for tsconfig path aliases
|
|
1658
1755
|
let cachedPathAliases: Map<string, string> | null = null;
|
|
1659
1756
|
let cachedProjectRoot: string | null = null;
|
|
1757
|
+
let cachedTsconfigMtime: number | null = null;
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* Clean tsconfig.json content to make it valid JSON
|
|
1761
|
+
* tsconfig.json allows comments and trailing commas which JSON.parse doesn't support
|
|
1762
|
+
*/
|
|
1763
|
+
function cleanTsconfigContent(content: string): string {
|
|
1764
|
+
return content
|
|
1765
|
+
// Remove single-line comments
|
|
1766
|
+
.replace(/\/\/.*$/gm, "")
|
|
1767
|
+
// Remove multi-line comments
|
|
1768
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
1769
|
+
// Remove trailing commas before } or ]
|
|
1770
|
+
.replace(/,(\s*[}\]])/g, "$1")
|
|
1771
|
+
// Handle potential issues with escaped characters in strings
|
|
1772
|
+
.replace(/\r\n/g, "\n")
|
|
1773
|
+
// Remove any BOM
|
|
1774
|
+
.replace(/^\uFEFF/, "");
|
|
1775
|
+
}
|
|
1660
1776
|
|
|
1661
1777
|
/**
|
|
1662
1778
|
* Read and parse tsconfig.json to get path aliases
|
|
1663
1779
|
*/
|
|
1664
1780
|
function getPathAliases(projectRoot: string): Map<string, string> {
|
|
1665
|
-
|
|
1781
|
+
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
1782
|
+
|
|
1783
|
+
// Check cache validity - also check file modification time
|
|
1666
1784
|
if (cachedPathAliases && cachedProjectRoot === projectRoot) {
|
|
1785
|
+
try {
|
|
1786
|
+
const stat = fs.statSync(tsconfigPath);
|
|
1787
|
+
if (cachedTsconfigMtime === stat.mtimeMs) {
|
|
1667
1788
|
return cachedPathAliases;
|
|
1789
|
+
}
|
|
1790
|
+
} catch {
|
|
1791
|
+
// File doesn't exist or can't be read, continue with fresh parse
|
|
1792
|
+
}
|
|
1668
1793
|
}
|
|
1669
1794
|
|
|
1670
1795
|
const aliases = new Map<string, string>();
|
|
1796
|
+
let parsedSuccessfully = false;
|
|
1671
1797
|
|
|
1672
1798
|
// Try to read tsconfig.json
|
|
1673
|
-
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
1674
1799
|
if (fs.existsSync(tsconfigPath)) {
|
|
1675
1800
|
try {
|
|
1676
1801
|
const content = fs.readFileSync(tsconfigPath, "utf-8");
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
1681
|
-
.replace(/,\s*([\]}])/g, "$1");
|
|
1802
|
+
const cleanContent = cleanTsconfigContent(content);
|
|
1803
|
+
|
|
1804
|
+
// Try to parse the cleaned content
|
|
1682
1805
|
const tsconfig = JSON.parse(cleanContent);
|
|
1806
|
+
parsedSuccessfully = true;
|
|
1683
1807
|
|
|
1684
1808
|
const paths = tsconfig.compilerOptions?.paths || {};
|
|
1685
|
-
const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
|
|
1686
1809
|
|
|
1687
1810
|
// Parse path mappings
|
|
1688
1811
|
for (const [alias, targets] of Object.entries(paths)) {
|
|
@@ -1695,8 +1818,30 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
1695
1818
|
}
|
|
1696
1819
|
|
|
1697
1820
|
debugLog("[edit] Loaded tsconfig path aliases", { aliases: Object.fromEntries(aliases) });
|
|
1821
|
+
|
|
1822
|
+
// Update cache with mtime
|
|
1823
|
+
try {
|
|
1824
|
+
const stat = fs.statSync(tsconfigPath);
|
|
1825
|
+
cachedTsconfigMtime = stat.mtimeMs;
|
|
1826
|
+
} catch {
|
|
1827
|
+
cachedTsconfigMtime = null;
|
|
1828
|
+
}
|
|
1698
1829
|
} catch (e) {
|
|
1699
|
-
|
|
1830
|
+
// Log the error with more context for debugging
|
|
1831
|
+
const errorStr = String(e);
|
|
1832
|
+
const posMatch = errorStr.match(/position (\d+)/);
|
|
1833
|
+
let context = "";
|
|
1834
|
+
if (posMatch) {
|
|
1835
|
+
const pos = parseInt(posMatch[1], 10);
|
|
1836
|
+
const content = fs.readFileSync(tsconfigPath, "utf-8");
|
|
1837
|
+
context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
|
|
1838
|
+
}
|
|
1839
|
+
debugLog("[edit] Failed to parse tsconfig.json", { error: errorStr, context });
|
|
1840
|
+
|
|
1841
|
+
// Clear cache on error so we retry next time
|
|
1842
|
+
cachedPathAliases = null;
|
|
1843
|
+
cachedProjectRoot = null;
|
|
1844
|
+
cachedTsconfigMtime = null;
|
|
1700
1845
|
}
|
|
1701
1846
|
}
|
|
1702
1847
|
|
|
@@ -1714,8 +1859,12 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
1714
1859
|
debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
|
|
1715
1860
|
}
|
|
1716
1861
|
|
|
1862
|
+
// Only cache if we parsed successfully or there's no tsconfig
|
|
1863
|
+
if (parsedSuccessfully || !fs.existsSync(tsconfigPath)) {
|
|
1717
1864
|
cachedPathAliases = aliases;
|
|
1718
1865
|
cachedProjectRoot = projectRoot;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1719
1868
|
return aliases;
|
|
1720
1869
|
}
|
|
1721
1870
|
|
|
@@ -1835,6 +1984,105 @@ interface ApplyPatchesResult {
|
|
|
1835
1984
|
failedPatches: { patch: Patch; error: string }[];
|
|
1836
1985
|
}
|
|
1837
1986
|
|
|
1987
|
+
/**
|
|
1988
|
+
* Normalize whitespace in a string for comparison
|
|
1989
|
+
* Collapses all whitespace runs to single spaces and trims
|
|
1990
|
+
*/
|
|
1991
|
+
function normalizeWhitespace(str: string): string {
|
|
1992
|
+
return str.replace(/\s+/g, " ").trim();
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
/**
|
|
1996
|
+
* Find a fuzzy match for the search string in content
|
|
1997
|
+
* Returns the actual matched substring from content, or null if not found
|
|
1998
|
+
*/
|
|
1999
|
+
function findFuzzyMatch(search: string, content: string): { start: number; end: number; matched: string } | null {
|
|
2000
|
+
// Strategy 1: Try line-by-line matching with flexible indentation
|
|
2001
|
+
const searchLines = search.split("\n").map(l => l.trim()).filter(l => l.length > 0);
|
|
2002
|
+
if (searchLines.length === 0) return null;
|
|
2003
|
+
|
|
2004
|
+
// Find the first non-empty line in content
|
|
2005
|
+
const contentLines = content.split("\n");
|
|
2006
|
+
const firstSearchLine = searchLines[0];
|
|
2007
|
+
|
|
2008
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
2009
|
+
const contentLineTrimmed = contentLines[i].trim();
|
|
2010
|
+
|
|
2011
|
+
// Check if this line matches the first search line
|
|
2012
|
+
if (contentLineTrimmed === firstSearchLine) {
|
|
2013
|
+
// Try to match all subsequent lines
|
|
2014
|
+
let matched = true;
|
|
2015
|
+
let searchLineIdx = 1;
|
|
2016
|
+
let contentLineIdx = i + 1;
|
|
2017
|
+
|
|
2018
|
+
while (searchLineIdx < searchLines.length && contentLineIdx < contentLines.length) {
|
|
2019
|
+
const searchLineTrimmed = searchLines[searchLineIdx];
|
|
2020
|
+
const contentLineTrimmedNext = contentLines[contentLineIdx].trim();
|
|
2021
|
+
|
|
2022
|
+
// Skip empty lines in content
|
|
2023
|
+
if (contentLineTrimmedNext === "" && searchLineTrimmed !== "") {
|
|
2024
|
+
contentLineIdx++;
|
|
2025
|
+
continue;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
if (contentLineTrimmedNext !== searchLineTrimmed) {
|
|
2029
|
+
matched = false;
|
|
2030
|
+
break;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
searchLineIdx++;
|
|
2034
|
+
contentLineIdx++;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
if (matched && searchLineIdx === searchLines.length) {
|
|
2038
|
+
// Found a match! Calculate the actual positions
|
|
2039
|
+
let start = 0;
|
|
2040
|
+
for (let j = 0; j < i; j++) {
|
|
2041
|
+
start += contentLines[j].length + 1; // +1 for newline
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
let end = start;
|
|
2045
|
+
for (let j = i; j < contentLineIdx; j++) {
|
|
2046
|
+
end += contentLines[j].length + (j < contentLineIdx - 1 ? 1 : 0);
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// Include trailing newline if the search had one
|
|
2050
|
+
if (search.endsWith("\n") && end < content.length && content[end] === "\n") {
|
|
2051
|
+
end++;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
return {
|
|
2055
|
+
start,
|
|
2056
|
+
end,
|
|
2057
|
+
matched: content.substring(start, end)
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// Strategy 2: Normalized whitespace comparison
|
|
2064
|
+
const normalizedSearch = normalizeWhitespace(search);
|
|
2065
|
+
|
|
2066
|
+
// Sliding window approach - find a substring that when normalized matches
|
|
2067
|
+
for (let windowStart = 0; windowStart < content.length; windowStart++) {
|
|
2068
|
+
// Find a reasonable end point (look for similar length with some tolerance)
|
|
2069
|
+
for (let windowEnd = windowStart + search.length - 20; windowEnd <= Math.min(content.length, windowStart + search.length + 50); windowEnd++) {
|
|
2070
|
+
if (windowEnd <= windowStart) continue;
|
|
2071
|
+
|
|
2072
|
+
const candidate = content.substring(windowStart, windowEnd);
|
|
2073
|
+
if (normalizeWhitespace(candidate) === normalizedSearch) {
|
|
2074
|
+
return {
|
|
2075
|
+
start: windowStart,
|
|
2076
|
+
end: windowEnd,
|
|
2077
|
+
matched: candidate
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
return null;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
1838
2086
|
/**
|
|
1839
2087
|
* Apply search/replace patches to file content
|
|
1840
2088
|
* This is the core of the patch-based editing system
|
|
@@ -1849,33 +2097,70 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
|
|
|
1849
2097
|
const normalizedSearch = patch.search.replace(/\\n/g, "\n");
|
|
1850
2098
|
const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
|
|
1851
2099
|
|
|
1852
|
-
//
|
|
1853
|
-
if (
|
|
1854
|
-
// Try with different whitespace normalization
|
|
1855
|
-
const flexibleSearch = normalizedSearch.replace(/\s+/g, "\\s+");
|
|
1856
|
-
const regex = new RegExp(flexibleSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\s\+/g, "\\s+"));
|
|
1857
|
-
|
|
1858
|
-
if (!regex.test(content)) {
|
|
1859
|
-
failedPatches.push({
|
|
1860
|
-
patch,
|
|
1861
|
-
error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
|
|
1862
|
-
});
|
|
1863
|
-
continue;
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
// If regex matched, use regex replace
|
|
1867
|
-
content = content.replace(regex, normalizedReplace);
|
|
1868
|
-
appliedPatches++;
|
|
1869
|
-
} else {
|
|
1870
|
-
// Exact match found - apply the replacement
|
|
1871
|
-
// Only replace the first occurrence to be safe
|
|
2100
|
+
// Strategy 1: Exact match
|
|
2101
|
+
if (content.includes(normalizedSearch)) {
|
|
1872
2102
|
const index = content.indexOf(normalizedSearch);
|
|
1873
2103
|
content =
|
|
1874
2104
|
content.substring(0, index) +
|
|
1875
2105
|
normalizedReplace +
|
|
1876
2106
|
content.substring(index + normalizedSearch.length);
|
|
1877
2107
|
appliedPatches++;
|
|
2108
|
+
debugLog("Patch applied (exact match)", {
|
|
2109
|
+
searchPreview: normalizedSearch.substring(0, 50)
|
|
2110
|
+
});
|
|
2111
|
+
continue;
|
|
1878
2112
|
}
|
|
2113
|
+
|
|
2114
|
+
// Strategy 2: Fuzzy match (handles indentation differences)
|
|
2115
|
+
const fuzzyMatch = findFuzzyMatch(normalizedSearch, content);
|
|
2116
|
+
if (fuzzyMatch) {
|
|
2117
|
+
// Apply the replacement, preserving the indentation from the original
|
|
2118
|
+
const originalIndent = fuzzyMatch.matched.match(/^(\s*)/)?.[1] || "";
|
|
2119
|
+
const replaceIndent = normalizedReplace.match(/^(\s*)/)?.[1] || "";
|
|
2120
|
+
|
|
2121
|
+
// If indentation differs, adjust the replacement to match original
|
|
2122
|
+
let adjustedReplace = normalizedReplace;
|
|
2123
|
+
if (originalIndent !== replaceIndent) {
|
|
2124
|
+
// Get the indentation difference
|
|
2125
|
+
const originalLines = fuzzyMatch.matched.split("\n");
|
|
2126
|
+
const replaceLines = normalizedReplace.split("\n");
|
|
2127
|
+
|
|
2128
|
+
if (originalLines.length > 0 && replaceLines.length > 0) {
|
|
2129
|
+
const baseIndent = originalLines[0].match(/^(\s*)/)?.[1] || "";
|
|
2130
|
+
const searchBaseIndent = normalizedSearch.split("\n")[0].match(/^(\s*)/)?.[1] || "";
|
|
2131
|
+
|
|
2132
|
+
// Adjust each line's indentation
|
|
2133
|
+
adjustedReplace = replaceLines.map((line, idx) => {
|
|
2134
|
+
if (idx === 0 || line.trim() === "") return line;
|
|
2135
|
+
const lineIndent = line.match(/^(\s*)/)?.[1] || "";
|
|
2136
|
+
const relativeIndent = lineIndent.length - (searchBaseIndent?.length || 0);
|
|
2137
|
+
const newIndent = baseIndent + " ".repeat(Math.max(0, relativeIndent));
|
|
2138
|
+
return newIndent + line.trim();
|
|
2139
|
+
}).join("\n");
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
content =
|
|
2144
|
+
content.substring(0, fuzzyMatch.start) +
|
|
2145
|
+
adjustedReplace +
|
|
2146
|
+
content.substring(fuzzyMatch.end);
|
|
2147
|
+
appliedPatches++;
|
|
2148
|
+
debugLog("Patch applied (fuzzy match)", {
|
|
2149
|
+
searchPreview: normalizedSearch.substring(0, 50),
|
|
2150
|
+
matchedPreview: fuzzyMatch.matched.substring(0, 50)
|
|
2151
|
+
});
|
|
2152
|
+
continue;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// No match found
|
|
2156
|
+
failedPatches.push({
|
|
2157
|
+
patch,
|
|
2158
|
+
error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
|
|
2159
|
+
});
|
|
2160
|
+
debugLog("Patch failed - no match found", {
|
|
2161
|
+
searchPreview: normalizedSearch.substring(0, 100),
|
|
2162
|
+
searchLength: normalizedSearch.length
|
|
2163
|
+
});
|
|
1879
2164
|
}
|
|
1880
2165
|
|
|
1881
2166
|
return {
|