sonance-brand-mcp 1.3.45 → 1.3.47
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.
|
@@ -475,7 +475,7 @@ Return search/replace patches (NOT full files). The system applies your patches
|
|
|
475
475
|
- Blaze Blue: #00A3E1
|
|
476
476
|
|
|
477
477
|
**RESPONSE FORMAT:**
|
|
478
|
-
Return ONLY
|
|
478
|
+
CRITICAL: Return ONLY the JSON object below. Do NOT include any text, explanation, or thinking before or after the JSON. No preamble. No "Looking at the screenshot..." No markdown code blocks. Just raw JSON:
|
|
479
479
|
{
|
|
480
480
|
"reasoning": "What you understood from the request and your plan",
|
|
481
481
|
"modifications": [
|
|
@@ -863,6 +863,15 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
|
|
|
863
863
|
}
|
|
864
864
|
|
|
865
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);
|
|
873
|
+
}
|
|
874
|
+
|
|
866
875
|
aiResponse = JSON.parse(jsonText);
|
|
867
876
|
} catch {
|
|
868
877
|
console.error("Failed to parse AI response:", textResponse.text);
|
|
@@ -1744,32 +1753,58 @@ function searchFilesForKeywords(
|
|
|
1744
1753
|
// Cache for tsconfig path aliases
|
|
1745
1754
|
let cachedPathAliases: Map<string, string> | null = null;
|
|
1746
1755
|
let cachedProjectRoot: string | null = null;
|
|
1756
|
+
let cachedTsconfigMtime: number | null = null;
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* Clean tsconfig.json content to make it valid JSON
|
|
1760
|
+
* tsconfig.json allows comments and trailing commas which JSON.parse doesn't support
|
|
1761
|
+
*/
|
|
1762
|
+
function cleanTsconfigContent(content: string): string {
|
|
1763
|
+
return content
|
|
1764
|
+
// Remove single-line comments
|
|
1765
|
+
.replace(/\/\/.*$/gm, "")
|
|
1766
|
+
// Remove multi-line comments
|
|
1767
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
1768
|
+
// Remove trailing commas before } or ]
|
|
1769
|
+
.replace(/,(\s*[}\]])/g, "$1")
|
|
1770
|
+
// Handle potential issues with escaped characters in strings
|
|
1771
|
+
.replace(/\r\n/g, "\n")
|
|
1772
|
+
// Remove any BOM
|
|
1773
|
+
.replace(/^\uFEFF/, "");
|
|
1774
|
+
}
|
|
1747
1775
|
|
|
1748
1776
|
/**
|
|
1749
1777
|
* Read and parse tsconfig.json to get path aliases
|
|
1750
1778
|
*/
|
|
1751
1779
|
function getPathAliases(projectRoot: string): Map<string, string> {
|
|
1752
|
-
|
|
1780
|
+
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
1781
|
+
|
|
1782
|
+
// Check cache validity - also check file modification time
|
|
1753
1783
|
if (cachedPathAliases && cachedProjectRoot === projectRoot) {
|
|
1784
|
+
try {
|
|
1785
|
+
const stat = fs.statSync(tsconfigPath);
|
|
1786
|
+
if (cachedTsconfigMtime === stat.mtimeMs) {
|
|
1754
1787
|
return cachedPathAliases;
|
|
1788
|
+
}
|
|
1789
|
+
} catch {
|
|
1790
|
+
// File doesn't exist or can't be read, continue with fresh parse
|
|
1791
|
+
}
|
|
1755
1792
|
}
|
|
1756
1793
|
|
|
1757
1794
|
const aliases = new Map<string, string>();
|
|
1795
|
+
let parsedSuccessfully = false;
|
|
1758
1796
|
|
|
1759
1797
|
// Try to read tsconfig.json
|
|
1760
|
-
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
1761
1798
|
if (fs.existsSync(tsconfigPath)) {
|
|
1762
1799
|
try {
|
|
1763
1800
|
const content = fs.readFileSync(tsconfigPath, "utf-8");
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
1768
|
-
.replace(/,\s*([\]}])/g, "$1");
|
|
1801
|
+
const cleanContent = cleanTsconfigContent(content);
|
|
1802
|
+
|
|
1803
|
+
// Try to parse the cleaned content
|
|
1769
1804
|
const tsconfig = JSON.parse(cleanContent);
|
|
1805
|
+
parsedSuccessfully = true;
|
|
1770
1806
|
|
|
1771
1807
|
const paths = tsconfig.compilerOptions?.paths || {};
|
|
1772
|
-
const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
|
|
1773
1808
|
|
|
1774
1809
|
// Parse path mappings
|
|
1775
1810
|
for (const [alias, targets] of Object.entries(paths)) {
|
|
@@ -1782,8 +1817,30 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
1782
1817
|
}
|
|
1783
1818
|
|
|
1784
1819
|
debugLog("[apply] Loaded tsconfig path aliases", { aliases: Object.fromEntries(aliases) });
|
|
1820
|
+
|
|
1821
|
+
// Update cache with mtime
|
|
1822
|
+
try {
|
|
1823
|
+
const stat = fs.statSync(tsconfigPath);
|
|
1824
|
+
cachedTsconfigMtime = stat.mtimeMs;
|
|
1825
|
+
} catch {
|
|
1826
|
+
cachedTsconfigMtime = null;
|
|
1827
|
+
}
|
|
1785
1828
|
} catch (e) {
|
|
1786
|
-
|
|
1829
|
+
// Log the error with more context for debugging
|
|
1830
|
+
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)}"`;
|
|
1837
|
+
}
|
|
1838
|
+
debugLog("[apply] Failed to parse tsconfig.json", { error: errorStr, context });
|
|
1839
|
+
|
|
1840
|
+
// Clear cache on error so we retry next time
|
|
1841
|
+
cachedPathAliases = null;
|
|
1842
|
+
cachedProjectRoot = null;
|
|
1843
|
+
cachedTsconfigMtime = null;
|
|
1787
1844
|
}
|
|
1788
1845
|
}
|
|
1789
1846
|
|
|
@@ -1801,8 +1858,12 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
1801
1858
|
debugLog("[apply] Using default @/ alias", { alias: aliases.get("@/") });
|
|
1802
1859
|
}
|
|
1803
1860
|
|
|
1861
|
+
// Only cache if we parsed successfully or there's no tsconfig
|
|
1862
|
+
if (parsedSuccessfully || !fs.existsSync(tsconfigPath)) {
|
|
1804
1863
|
cachedPathAliases = aliases;
|
|
1805
1864
|
cachedProjectRoot = projectRoot;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1806
1867
|
return aliases;
|
|
1807
1868
|
}
|
|
1808
1869
|
|
|
@@ -1916,6 +1977,105 @@ interface ApplyPatchesResult {
|
|
|
1916
1977
|
failedPatches: { patch: Patch; error: string }[];
|
|
1917
1978
|
}
|
|
1918
1979
|
|
|
1980
|
+
/**
|
|
1981
|
+
* Normalize whitespace in a string for comparison
|
|
1982
|
+
* Collapses all whitespace runs to single spaces and trims
|
|
1983
|
+
*/
|
|
1984
|
+
function normalizeWhitespace(str: string): string {
|
|
1985
|
+
return str.replace(/\s+/g, " ").trim();
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* Find a fuzzy match for the search string in content
|
|
1990
|
+
* Returns the actual matched substring from content, or null if not found
|
|
1991
|
+
*/
|
|
1992
|
+
function findFuzzyMatch(search: string, content: string): { start: number; end: number; matched: string } | null {
|
|
1993
|
+
// Strategy 1: Try line-by-line matching with flexible indentation
|
|
1994
|
+
const searchLines = search.split("\n").map(l => l.trim()).filter(l => l.length > 0);
|
|
1995
|
+
if (searchLines.length === 0) return null;
|
|
1996
|
+
|
|
1997
|
+
// Find the first non-empty line in content
|
|
1998
|
+
const contentLines = content.split("\n");
|
|
1999
|
+
const firstSearchLine = searchLines[0];
|
|
2000
|
+
|
|
2001
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
2002
|
+
const contentLineTrimmed = contentLines[i].trim();
|
|
2003
|
+
|
|
2004
|
+
// Check if this line matches the first search line
|
|
2005
|
+
if (contentLineTrimmed === firstSearchLine) {
|
|
2006
|
+
// Try to match all subsequent lines
|
|
2007
|
+
let matched = true;
|
|
2008
|
+
let searchLineIdx = 1;
|
|
2009
|
+
let contentLineIdx = i + 1;
|
|
2010
|
+
|
|
2011
|
+
while (searchLineIdx < searchLines.length && contentLineIdx < contentLines.length) {
|
|
2012
|
+
const searchLineTrimmed = searchLines[searchLineIdx];
|
|
2013
|
+
const contentLineTrimmedNext = contentLines[contentLineIdx].trim();
|
|
2014
|
+
|
|
2015
|
+
// Skip empty lines in content
|
|
2016
|
+
if (contentLineTrimmedNext === "" && searchLineTrimmed !== "") {
|
|
2017
|
+
contentLineIdx++;
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
if (contentLineTrimmedNext !== searchLineTrimmed) {
|
|
2022
|
+
matched = false;
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
searchLineIdx++;
|
|
2027
|
+
contentLineIdx++;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
if (matched && searchLineIdx === searchLines.length) {
|
|
2031
|
+
// Found a match! Calculate the actual positions
|
|
2032
|
+
let start = 0;
|
|
2033
|
+
for (let j = 0; j < i; j++) {
|
|
2034
|
+
start += contentLines[j].length + 1; // +1 for newline
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
let end = start;
|
|
2038
|
+
for (let j = i; j < contentLineIdx; j++) {
|
|
2039
|
+
end += contentLines[j].length + (j < contentLineIdx - 1 ? 1 : 0);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// Include trailing newline if the search had one
|
|
2043
|
+
if (search.endsWith("\n") && end < content.length && content[end] === "\n") {
|
|
2044
|
+
end++;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
return {
|
|
2048
|
+
start,
|
|
2049
|
+
end,
|
|
2050
|
+
matched: content.substring(start, end)
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// Strategy 2: Normalized whitespace comparison
|
|
2057
|
+
const normalizedSearch = normalizeWhitespace(search);
|
|
2058
|
+
|
|
2059
|
+
// Sliding window approach - find a substring that when normalized matches
|
|
2060
|
+
for (let windowStart = 0; windowStart < content.length; windowStart++) {
|
|
2061
|
+
// Find a reasonable end point (look for similar length with some tolerance)
|
|
2062
|
+
for (let windowEnd = windowStart + search.length - 20; windowEnd <= Math.min(content.length, windowStart + search.length + 50); windowEnd++) {
|
|
2063
|
+
if (windowEnd <= windowStart) continue;
|
|
2064
|
+
|
|
2065
|
+
const candidate = content.substring(windowStart, windowEnd);
|
|
2066
|
+
if (normalizeWhitespace(candidate) === normalizedSearch) {
|
|
2067
|
+
return {
|
|
2068
|
+
start: windowStart,
|
|
2069
|
+
end: windowEnd,
|
|
2070
|
+
matched: candidate
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
return null;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
1919
2079
|
/**
|
|
1920
2080
|
* Apply search/replace patches to file content
|
|
1921
2081
|
* This is the core of the patch-based editing system
|
|
@@ -1930,33 +2090,70 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
|
|
|
1930
2090
|
const normalizedSearch = patch.search.replace(/\\n/g, "\n");
|
|
1931
2091
|
const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
|
|
1932
2092
|
|
|
1933
|
-
//
|
|
1934
|
-
if (
|
|
1935
|
-
// Try with different whitespace normalization
|
|
1936
|
-
const flexibleSearch = normalizedSearch.replace(/\s+/g, "\\s+");
|
|
1937
|
-
const regex = new RegExp(flexibleSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\s\+/g, "\\s+"));
|
|
1938
|
-
|
|
1939
|
-
if (!regex.test(content)) {
|
|
1940
|
-
failedPatches.push({
|
|
1941
|
-
patch,
|
|
1942
|
-
error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
|
|
1943
|
-
});
|
|
1944
|
-
continue;
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
// If regex matched, use regex replace
|
|
1948
|
-
content = content.replace(regex, normalizedReplace);
|
|
1949
|
-
appliedPatches++;
|
|
1950
|
-
} else {
|
|
1951
|
-
// Exact match found - apply the replacement
|
|
1952
|
-
// Only replace the first occurrence to be safe
|
|
2093
|
+
// Strategy 1: Exact match
|
|
2094
|
+
if (content.includes(normalizedSearch)) {
|
|
1953
2095
|
const index = content.indexOf(normalizedSearch);
|
|
1954
2096
|
content =
|
|
1955
2097
|
content.substring(0, index) +
|
|
1956
2098
|
normalizedReplace +
|
|
1957
2099
|
content.substring(index + normalizedSearch.length);
|
|
1958
2100
|
appliedPatches++;
|
|
2101
|
+
debugLog("Patch applied (exact match)", {
|
|
2102
|
+
searchPreview: normalizedSearch.substring(0, 50)
|
|
2103
|
+
});
|
|
2104
|
+
continue;
|
|
1959
2105
|
}
|
|
2106
|
+
|
|
2107
|
+
// Strategy 2: Fuzzy match (handles indentation differences)
|
|
2108
|
+
const fuzzyMatch = findFuzzyMatch(normalizedSearch, content);
|
|
2109
|
+
if (fuzzyMatch) {
|
|
2110
|
+
// Apply the replacement, preserving the indentation from the original
|
|
2111
|
+
const originalIndent = fuzzyMatch.matched.match(/^(\s*)/)?.[1] || "";
|
|
2112
|
+
const replaceIndent = normalizedReplace.match(/^(\s*)/)?.[1] || "";
|
|
2113
|
+
|
|
2114
|
+
// If indentation differs, adjust the replacement to match original
|
|
2115
|
+
let adjustedReplace = normalizedReplace;
|
|
2116
|
+
if (originalIndent !== replaceIndent) {
|
|
2117
|
+
// Get the indentation difference
|
|
2118
|
+
const originalLines = fuzzyMatch.matched.split("\n");
|
|
2119
|
+
const replaceLines = normalizedReplace.split("\n");
|
|
2120
|
+
|
|
2121
|
+
if (originalLines.length > 0 && replaceLines.length > 0) {
|
|
2122
|
+
const baseIndent = originalLines[0].match(/^(\s*)/)?.[1] || "";
|
|
2123
|
+
const searchBaseIndent = normalizedSearch.split("\n")[0].match(/^(\s*)/)?.[1] || "";
|
|
2124
|
+
|
|
2125
|
+
// Adjust each line's indentation
|
|
2126
|
+
adjustedReplace = replaceLines.map((line, idx) => {
|
|
2127
|
+
if (idx === 0 || line.trim() === "") return line;
|
|
2128
|
+
const lineIndent = line.match(/^(\s*)/)?.[1] || "";
|
|
2129
|
+
const relativeIndent = lineIndent.length - (searchBaseIndent?.length || 0);
|
|
2130
|
+
const newIndent = baseIndent + " ".repeat(Math.max(0, relativeIndent));
|
|
2131
|
+
return newIndent + line.trim();
|
|
2132
|
+
}).join("\n");
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
content =
|
|
2137
|
+
content.substring(0, fuzzyMatch.start) +
|
|
2138
|
+
adjustedReplace +
|
|
2139
|
+
content.substring(fuzzyMatch.end);
|
|
2140
|
+
appliedPatches++;
|
|
2141
|
+
debugLog("Patch applied (fuzzy match)", {
|
|
2142
|
+
searchPreview: normalizedSearch.substring(0, 50),
|
|
2143
|
+
matchedPreview: fuzzyMatch.matched.substring(0, 50)
|
|
2144
|
+
});
|
|
2145
|
+
continue;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// No match found
|
|
2149
|
+
failedPatches.push({
|
|
2150
|
+
patch,
|
|
2151
|
+
error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
|
|
2152
|
+
});
|
|
2153
|
+
debugLog("Patch failed - no match found", {
|
|
2154
|
+
searchPreview: normalizedSearch.substring(0, 100),
|
|
2155
|
+
searchLength: normalizedSearch.length
|
|
2156
|
+
});
|
|
1960
2157
|
}
|
|
1961
2158
|
|
|
1962
2159
|
return {
|
|
@@ -473,7 +473,7 @@ Return search/replace patches (NOT full files). The system applies your patches
|
|
|
473
473
|
- Blaze Blue: #00A3E1
|
|
474
474
|
|
|
475
475
|
**RESPONSE FORMAT:**
|
|
476
|
-
Return ONLY
|
|
476
|
+
CRITICAL: Return ONLY the JSON object below. Do NOT include any text, explanation, or thinking before or after the JSON. No preamble. No "Looking at the screenshot..." No markdown code blocks. Just raw JSON:
|
|
477
477
|
{
|
|
478
478
|
"reasoning": "What you understood from the request and your plan",
|
|
479
479
|
"modifications": [
|
|
@@ -878,6 +878,14 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
|
|
|
878
878
|
// Clean up any remaining whitespace
|
|
879
879
|
jsonText = jsonText.trim();
|
|
880
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);
|
|
887
|
+
}
|
|
888
|
+
|
|
881
889
|
aiResponse = JSON.parse(jsonText);
|
|
882
890
|
} catch {
|
|
883
891
|
console.error("Failed to parse AI response:", textResponse.text);
|
|
@@ -1649,32 +1657,58 @@ function searchFilesForKeywords(
|
|
|
1649
1657
|
// Cache for tsconfig path aliases
|
|
1650
1658
|
let cachedPathAliases: Map<string, string> | null = null;
|
|
1651
1659
|
let cachedProjectRoot: string | null = null;
|
|
1660
|
+
let cachedTsconfigMtime: number | null = null;
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* Clean tsconfig.json content to make it valid JSON
|
|
1664
|
+
* tsconfig.json allows comments and trailing commas which JSON.parse doesn't support
|
|
1665
|
+
*/
|
|
1666
|
+
function cleanTsconfigContent(content: string): string {
|
|
1667
|
+
return content
|
|
1668
|
+
// Remove single-line comments
|
|
1669
|
+
.replace(/\/\/.*$/gm, "")
|
|
1670
|
+
// Remove multi-line comments
|
|
1671
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
1672
|
+
// Remove trailing commas before } or ]
|
|
1673
|
+
.replace(/,(\s*[}\]])/g, "$1")
|
|
1674
|
+
// Handle potential issues with escaped characters in strings
|
|
1675
|
+
.replace(/\r\n/g, "\n")
|
|
1676
|
+
// Remove any BOM
|
|
1677
|
+
.replace(/^\uFEFF/, "");
|
|
1678
|
+
}
|
|
1652
1679
|
|
|
1653
1680
|
/**
|
|
1654
1681
|
* Read and parse tsconfig.json to get path aliases
|
|
1655
1682
|
*/
|
|
1656
1683
|
function getPathAliases(projectRoot: string): Map<string, string> {
|
|
1657
|
-
|
|
1684
|
+
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
1685
|
+
|
|
1686
|
+
// Check cache validity - also check file modification time
|
|
1658
1687
|
if (cachedPathAliases && cachedProjectRoot === projectRoot) {
|
|
1688
|
+
try {
|
|
1689
|
+
const stat = fs.statSync(tsconfigPath);
|
|
1690
|
+
if (cachedTsconfigMtime === stat.mtimeMs) {
|
|
1659
1691
|
return cachedPathAliases;
|
|
1692
|
+
}
|
|
1693
|
+
} catch {
|
|
1694
|
+
// File doesn't exist or can't be read, continue with fresh parse
|
|
1695
|
+
}
|
|
1660
1696
|
}
|
|
1661
1697
|
|
|
1662
1698
|
const aliases = new Map<string, string>();
|
|
1699
|
+
let parsedSuccessfully = false;
|
|
1663
1700
|
|
|
1664
1701
|
// Try to read tsconfig.json
|
|
1665
|
-
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
1666
1702
|
if (fs.existsSync(tsconfigPath)) {
|
|
1667
1703
|
try {
|
|
1668
1704
|
const content = fs.readFileSync(tsconfigPath, "utf-8");
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
1673
|
-
.replace(/,\s*([\]}])/g, "$1");
|
|
1705
|
+
const cleanContent = cleanTsconfigContent(content);
|
|
1706
|
+
|
|
1707
|
+
// Try to parse the cleaned content
|
|
1674
1708
|
const tsconfig = JSON.parse(cleanContent);
|
|
1709
|
+
parsedSuccessfully = true;
|
|
1675
1710
|
|
|
1676
1711
|
const paths = tsconfig.compilerOptions?.paths || {};
|
|
1677
|
-
const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
|
|
1678
1712
|
|
|
1679
1713
|
// Parse path mappings
|
|
1680
1714
|
for (const [alias, targets] of Object.entries(paths)) {
|
|
@@ -1687,8 +1721,30 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
1687
1721
|
}
|
|
1688
1722
|
|
|
1689
1723
|
debugLog("[edit] Loaded tsconfig path aliases", { aliases: Object.fromEntries(aliases) });
|
|
1724
|
+
|
|
1725
|
+
// Update cache with mtime
|
|
1726
|
+
try {
|
|
1727
|
+
const stat = fs.statSync(tsconfigPath);
|
|
1728
|
+
cachedTsconfigMtime = stat.mtimeMs;
|
|
1729
|
+
} catch {
|
|
1730
|
+
cachedTsconfigMtime = null;
|
|
1731
|
+
}
|
|
1690
1732
|
} catch (e) {
|
|
1691
|
-
|
|
1733
|
+
// Log the error with more context for debugging
|
|
1734
|
+
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)}"`;
|
|
1741
|
+
}
|
|
1742
|
+
debugLog("[edit] Failed to parse tsconfig.json", { error: errorStr, context });
|
|
1743
|
+
|
|
1744
|
+
// Clear cache on error so we retry next time
|
|
1745
|
+
cachedPathAliases = null;
|
|
1746
|
+
cachedProjectRoot = null;
|
|
1747
|
+
cachedTsconfigMtime = null;
|
|
1692
1748
|
}
|
|
1693
1749
|
}
|
|
1694
1750
|
|
|
@@ -1706,8 +1762,12 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
1706
1762
|
debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
|
|
1707
1763
|
}
|
|
1708
1764
|
|
|
1765
|
+
// Only cache if we parsed successfully or there's no tsconfig
|
|
1766
|
+
if (parsedSuccessfully || !fs.existsSync(tsconfigPath)) {
|
|
1709
1767
|
cachedPathAliases = aliases;
|
|
1710
1768
|
cachedProjectRoot = projectRoot;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1711
1771
|
return aliases;
|
|
1712
1772
|
}
|
|
1713
1773
|
|
|
@@ -1827,6 +1887,105 @@ interface ApplyPatchesResult {
|
|
|
1827
1887
|
failedPatches: { patch: Patch; error: string }[];
|
|
1828
1888
|
}
|
|
1829
1889
|
|
|
1890
|
+
/**
|
|
1891
|
+
* Normalize whitespace in a string for comparison
|
|
1892
|
+
* Collapses all whitespace runs to single spaces and trims
|
|
1893
|
+
*/
|
|
1894
|
+
function normalizeWhitespace(str: string): string {
|
|
1895
|
+
return str.replace(/\s+/g, " ").trim();
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/**
|
|
1899
|
+
* Find a fuzzy match for the search string in content
|
|
1900
|
+
* Returns the actual matched substring from content, or null if not found
|
|
1901
|
+
*/
|
|
1902
|
+
function findFuzzyMatch(search: string, content: string): { start: number; end: number; matched: string } | null {
|
|
1903
|
+
// Strategy 1: Try line-by-line matching with flexible indentation
|
|
1904
|
+
const searchLines = search.split("\n").map(l => l.trim()).filter(l => l.length > 0);
|
|
1905
|
+
if (searchLines.length === 0) return null;
|
|
1906
|
+
|
|
1907
|
+
// Find the first non-empty line in content
|
|
1908
|
+
const contentLines = content.split("\n");
|
|
1909
|
+
const firstSearchLine = searchLines[0];
|
|
1910
|
+
|
|
1911
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
1912
|
+
const contentLineTrimmed = contentLines[i].trim();
|
|
1913
|
+
|
|
1914
|
+
// Check if this line matches the first search line
|
|
1915
|
+
if (contentLineTrimmed === firstSearchLine) {
|
|
1916
|
+
// Try to match all subsequent lines
|
|
1917
|
+
let matched = true;
|
|
1918
|
+
let searchLineIdx = 1;
|
|
1919
|
+
let contentLineIdx = i + 1;
|
|
1920
|
+
|
|
1921
|
+
while (searchLineIdx < searchLines.length && contentLineIdx < contentLines.length) {
|
|
1922
|
+
const searchLineTrimmed = searchLines[searchLineIdx];
|
|
1923
|
+
const contentLineTrimmedNext = contentLines[contentLineIdx].trim();
|
|
1924
|
+
|
|
1925
|
+
// Skip empty lines in content
|
|
1926
|
+
if (contentLineTrimmedNext === "" && searchLineTrimmed !== "") {
|
|
1927
|
+
contentLineIdx++;
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
if (contentLineTrimmedNext !== searchLineTrimmed) {
|
|
1932
|
+
matched = false;
|
|
1933
|
+
break;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
searchLineIdx++;
|
|
1937
|
+
contentLineIdx++;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
if (matched && searchLineIdx === searchLines.length) {
|
|
1941
|
+
// Found a match! Calculate the actual positions
|
|
1942
|
+
let start = 0;
|
|
1943
|
+
for (let j = 0; j < i; j++) {
|
|
1944
|
+
start += contentLines[j].length + 1; // +1 for newline
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
let end = start;
|
|
1948
|
+
for (let j = i; j < contentLineIdx; j++) {
|
|
1949
|
+
end += contentLines[j].length + (j < contentLineIdx - 1 ? 1 : 0);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// Include trailing newline if the search had one
|
|
1953
|
+
if (search.endsWith("\n") && end < content.length && content[end] === "\n") {
|
|
1954
|
+
end++;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
return {
|
|
1958
|
+
start,
|
|
1959
|
+
end,
|
|
1960
|
+
matched: content.substring(start, end)
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// Strategy 2: Normalized whitespace comparison
|
|
1967
|
+
const normalizedSearch = normalizeWhitespace(search);
|
|
1968
|
+
|
|
1969
|
+
// Sliding window approach - find a substring that when normalized matches
|
|
1970
|
+
for (let windowStart = 0; windowStart < content.length; windowStart++) {
|
|
1971
|
+
// Find a reasonable end point (look for similar length with some tolerance)
|
|
1972
|
+
for (let windowEnd = windowStart + search.length - 20; windowEnd <= Math.min(content.length, windowStart + search.length + 50); windowEnd++) {
|
|
1973
|
+
if (windowEnd <= windowStart) continue;
|
|
1974
|
+
|
|
1975
|
+
const candidate = content.substring(windowStart, windowEnd);
|
|
1976
|
+
if (normalizeWhitespace(candidate) === normalizedSearch) {
|
|
1977
|
+
return {
|
|
1978
|
+
start: windowStart,
|
|
1979
|
+
end: windowEnd,
|
|
1980
|
+
matched: candidate
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1830
1989
|
/**
|
|
1831
1990
|
* Apply search/replace patches to file content
|
|
1832
1991
|
* This is the core of the patch-based editing system
|
|
@@ -1841,33 +2000,70 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
|
|
|
1841
2000
|
const normalizedSearch = patch.search.replace(/\\n/g, "\n");
|
|
1842
2001
|
const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
|
|
1843
2002
|
|
|
1844
|
-
//
|
|
1845
|
-
if (
|
|
1846
|
-
// Try with different whitespace normalization
|
|
1847
|
-
const flexibleSearch = normalizedSearch.replace(/\s+/g, "\\s+");
|
|
1848
|
-
const regex = new RegExp(flexibleSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\s\+/g, "\\s+"));
|
|
1849
|
-
|
|
1850
|
-
if (!regex.test(content)) {
|
|
1851
|
-
failedPatches.push({
|
|
1852
|
-
patch,
|
|
1853
|
-
error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
|
|
1854
|
-
});
|
|
1855
|
-
continue;
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
// If regex matched, use regex replace
|
|
1859
|
-
content = content.replace(regex, normalizedReplace);
|
|
1860
|
-
appliedPatches++;
|
|
1861
|
-
} else {
|
|
1862
|
-
// Exact match found - apply the replacement
|
|
1863
|
-
// Only replace the first occurrence to be safe
|
|
2003
|
+
// Strategy 1: Exact match
|
|
2004
|
+
if (content.includes(normalizedSearch)) {
|
|
1864
2005
|
const index = content.indexOf(normalizedSearch);
|
|
1865
2006
|
content =
|
|
1866
2007
|
content.substring(0, index) +
|
|
1867
2008
|
normalizedReplace +
|
|
1868
2009
|
content.substring(index + normalizedSearch.length);
|
|
1869
2010
|
appliedPatches++;
|
|
2011
|
+
debugLog("Patch applied (exact match)", {
|
|
2012
|
+
searchPreview: normalizedSearch.substring(0, 50)
|
|
2013
|
+
});
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// Strategy 2: Fuzzy match (handles indentation differences)
|
|
2018
|
+
const fuzzyMatch = findFuzzyMatch(normalizedSearch, content);
|
|
2019
|
+
if (fuzzyMatch) {
|
|
2020
|
+
// Apply the replacement, preserving the indentation from the original
|
|
2021
|
+
const originalIndent = fuzzyMatch.matched.match(/^(\s*)/)?.[1] || "";
|
|
2022
|
+
const replaceIndent = normalizedReplace.match(/^(\s*)/)?.[1] || "";
|
|
2023
|
+
|
|
2024
|
+
// If indentation differs, adjust the replacement to match original
|
|
2025
|
+
let adjustedReplace = normalizedReplace;
|
|
2026
|
+
if (originalIndent !== replaceIndent) {
|
|
2027
|
+
// Get the indentation difference
|
|
2028
|
+
const originalLines = fuzzyMatch.matched.split("\n");
|
|
2029
|
+
const replaceLines = normalizedReplace.split("\n");
|
|
2030
|
+
|
|
2031
|
+
if (originalLines.length > 0 && replaceLines.length > 0) {
|
|
2032
|
+
const baseIndent = originalLines[0].match(/^(\s*)/)?.[1] || "";
|
|
2033
|
+
const searchBaseIndent = normalizedSearch.split("\n")[0].match(/^(\s*)/)?.[1] || "";
|
|
2034
|
+
|
|
2035
|
+
// Adjust each line's indentation
|
|
2036
|
+
adjustedReplace = replaceLines.map((line, idx) => {
|
|
2037
|
+
if (idx === 0 || line.trim() === "") return line;
|
|
2038
|
+
const lineIndent = line.match(/^(\s*)/)?.[1] || "";
|
|
2039
|
+
const relativeIndent = lineIndent.length - (searchBaseIndent?.length || 0);
|
|
2040
|
+
const newIndent = baseIndent + " ".repeat(Math.max(0, relativeIndent));
|
|
2041
|
+
return newIndent + line.trim();
|
|
2042
|
+
}).join("\n");
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
content =
|
|
2047
|
+
content.substring(0, fuzzyMatch.start) +
|
|
2048
|
+
adjustedReplace +
|
|
2049
|
+
content.substring(fuzzyMatch.end);
|
|
2050
|
+
appliedPatches++;
|
|
2051
|
+
debugLog("Patch applied (fuzzy match)", {
|
|
2052
|
+
searchPreview: normalizedSearch.substring(0, 50),
|
|
2053
|
+
matchedPreview: fuzzyMatch.matched.substring(0, 50)
|
|
2054
|
+
});
|
|
2055
|
+
continue;
|
|
1870
2056
|
}
|
|
2057
|
+
|
|
2058
|
+
// No match found
|
|
2059
|
+
failedPatches.push({
|
|
2060
|
+
patch,
|
|
2061
|
+
error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
|
|
2062
|
+
});
|
|
2063
|
+
debugLog("Patch failed - no match found", {
|
|
2064
|
+
searchPreview: normalizedSearch.substring(0, 100),
|
|
2065
|
+
searchLength: normalizedSearch.length
|
|
2066
|
+
});
|
|
1871
2067
|
}
|
|
1872
2068
|
|
|
1873
2069
|
return {
|
|
@@ -2370,7 +2370,7 @@ export function SonanceDevTools() {
|
|
|
2370
2370
|
<div
|
|
2371
2371
|
ref={headerRef}
|
|
2372
2372
|
className={cn(
|
|
2373
|
-
"flex items-center justify-between px-
|
|
2373
|
+
"flex items-center justify-between px-3 py-2 border-b border-gray-200 bg-[#333F48]",
|
|
2374
2374
|
"cursor-move touch-none"
|
|
2375
2375
|
)}
|
|
2376
2376
|
onPointerDown={handleDragStart}
|
|
@@ -2380,10 +2380,10 @@ export function SonanceDevTools() {
|
|
|
2380
2380
|
onDoubleClick={handleResetPosition}
|
|
2381
2381
|
title="Drag to move • Double-click to reset position"
|
|
2382
2382
|
>
|
|
2383
|
-
<div className="flex items-center gap-
|
|
2384
|
-
<GripHorizontal className="h-
|
|
2385
|
-
<Palette className="h-
|
|
2386
|
-
<span id="span-sonance-devtools" className="text-
|
|
2383
|
+
<div className="flex items-center gap-1.5">
|
|
2384
|
+
<GripHorizontal className="h-3.5 w-3.5 text-white/50" />
|
|
2385
|
+
<Palette className="h-4 w-4 text-[#00A3E1]" />
|
|
2386
|
+
<span id="span-sonance-devtools" className="text-xs font-semibold text-white whitespace-nowrap">
|
|
2387
2387
|
Sonance DevTools
|
|
2388
2388
|
</span>
|
|
2389
2389
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.47",
|
|
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",
|