sonance-brand-mcp 1.3.46 → 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.
|
@@ -1753,32 +1753,58 @@ function searchFilesForKeywords(
|
|
|
1753
1753
|
// Cache for tsconfig path aliases
|
|
1754
1754
|
let cachedPathAliases: Map<string, string> | null = null;
|
|
1755
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
|
+
}
|
|
1756
1775
|
|
|
1757
1776
|
/**
|
|
1758
1777
|
* Read and parse tsconfig.json to get path aliases
|
|
1759
1778
|
*/
|
|
1760
1779
|
function getPathAliases(projectRoot: string): Map<string, string> {
|
|
1761
|
-
|
|
1780
|
+
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
1781
|
+
|
|
1782
|
+
// Check cache validity - also check file modification time
|
|
1762
1783
|
if (cachedPathAliases && cachedProjectRoot === projectRoot) {
|
|
1784
|
+
try {
|
|
1785
|
+
const stat = fs.statSync(tsconfigPath);
|
|
1786
|
+
if (cachedTsconfigMtime === stat.mtimeMs) {
|
|
1763
1787
|
return cachedPathAliases;
|
|
1788
|
+
}
|
|
1789
|
+
} catch {
|
|
1790
|
+
// File doesn't exist or can't be read, continue with fresh parse
|
|
1791
|
+
}
|
|
1764
1792
|
}
|
|
1765
1793
|
|
|
1766
1794
|
const aliases = new Map<string, string>();
|
|
1795
|
+
let parsedSuccessfully = false;
|
|
1767
1796
|
|
|
1768
1797
|
// Try to read tsconfig.json
|
|
1769
|
-
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
1770
1798
|
if (fs.existsSync(tsconfigPath)) {
|
|
1771
1799
|
try {
|
|
1772
1800
|
const content = fs.readFileSync(tsconfigPath, "utf-8");
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
1777
|
-
.replace(/,\s*([\]}])/g, "$1");
|
|
1801
|
+
const cleanContent = cleanTsconfigContent(content);
|
|
1802
|
+
|
|
1803
|
+
// Try to parse the cleaned content
|
|
1778
1804
|
const tsconfig = JSON.parse(cleanContent);
|
|
1805
|
+
parsedSuccessfully = true;
|
|
1779
1806
|
|
|
1780
1807
|
const paths = tsconfig.compilerOptions?.paths || {};
|
|
1781
|
-
const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
|
|
1782
1808
|
|
|
1783
1809
|
// Parse path mappings
|
|
1784
1810
|
for (const [alias, targets] of Object.entries(paths)) {
|
|
@@ -1791,8 +1817,30 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
1791
1817
|
}
|
|
1792
1818
|
|
|
1793
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
|
+
}
|
|
1794
1828
|
} catch (e) {
|
|
1795
|
-
|
|
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;
|
|
1796
1844
|
}
|
|
1797
1845
|
}
|
|
1798
1846
|
|
|
@@ -1810,8 +1858,12 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
1810
1858
|
debugLog("[apply] Using default @/ alias", { alias: aliases.get("@/") });
|
|
1811
1859
|
}
|
|
1812
1860
|
|
|
1861
|
+
// Only cache if we parsed successfully or there's no tsconfig
|
|
1862
|
+
if (parsedSuccessfully || !fs.existsSync(tsconfigPath)) {
|
|
1813
1863
|
cachedPathAliases = aliases;
|
|
1814
1864
|
cachedProjectRoot = projectRoot;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1815
1867
|
return aliases;
|
|
1816
1868
|
}
|
|
1817
1869
|
|
|
@@ -1925,6 +1977,105 @@ interface ApplyPatchesResult {
|
|
|
1925
1977
|
failedPatches: { patch: Patch; error: string }[];
|
|
1926
1978
|
}
|
|
1927
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
|
+
|
|
1928
2079
|
/**
|
|
1929
2080
|
* Apply search/replace patches to file content
|
|
1930
2081
|
* This is the core of the patch-based editing system
|
|
@@ -1939,33 +2090,70 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
|
|
|
1939
2090
|
const normalizedSearch = patch.search.replace(/\\n/g, "\n");
|
|
1940
2091
|
const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
|
|
1941
2092
|
|
|
1942
|
-
//
|
|
1943
|
-
if (
|
|
1944
|
-
// Try with different whitespace normalization
|
|
1945
|
-
const flexibleSearch = normalizedSearch.replace(/\s+/g, "\\s+");
|
|
1946
|
-
const regex = new RegExp(flexibleSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\s\+/g, "\\s+"));
|
|
1947
|
-
|
|
1948
|
-
if (!regex.test(content)) {
|
|
1949
|
-
failedPatches.push({
|
|
1950
|
-
patch,
|
|
1951
|
-
error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
|
|
1952
|
-
});
|
|
1953
|
-
continue;
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
// If regex matched, use regex replace
|
|
1957
|
-
content = content.replace(regex, normalizedReplace);
|
|
1958
|
-
appliedPatches++;
|
|
1959
|
-
} else {
|
|
1960
|
-
// Exact match found - apply the replacement
|
|
1961
|
-
// Only replace the first occurrence to be safe
|
|
2093
|
+
// Strategy 1: Exact match
|
|
2094
|
+
if (content.includes(normalizedSearch)) {
|
|
1962
2095
|
const index = content.indexOf(normalizedSearch);
|
|
1963
2096
|
content =
|
|
1964
2097
|
content.substring(0, index) +
|
|
1965
2098
|
normalizedReplace +
|
|
1966
2099
|
content.substring(index + normalizedSearch.length);
|
|
1967
2100
|
appliedPatches++;
|
|
2101
|
+
debugLog("Patch applied (exact match)", {
|
|
2102
|
+
searchPreview: normalizedSearch.substring(0, 50)
|
|
2103
|
+
});
|
|
2104
|
+
continue;
|
|
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;
|
|
1968
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
|
+
});
|
|
1969
2157
|
}
|
|
1970
2158
|
|
|
1971
2159
|
return {
|
|
@@ -1657,32 +1657,58 @@ function searchFilesForKeywords(
|
|
|
1657
1657
|
// Cache for tsconfig path aliases
|
|
1658
1658
|
let cachedPathAliases: Map<string, string> | null = null;
|
|
1659
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
|
+
}
|
|
1660
1679
|
|
|
1661
1680
|
/**
|
|
1662
1681
|
* Read and parse tsconfig.json to get path aliases
|
|
1663
1682
|
*/
|
|
1664
1683
|
function getPathAliases(projectRoot: string): Map<string, string> {
|
|
1665
|
-
|
|
1684
|
+
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
1685
|
+
|
|
1686
|
+
// Check cache validity - also check file modification time
|
|
1666
1687
|
if (cachedPathAliases && cachedProjectRoot === projectRoot) {
|
|
1688
|
+
try {
|
|
1689
|
+
const stat = fs.statSync(tsconfigPath);
|
|
1690
|
+
if (cachedTsconfigMtime === stat.mtimeMs) {
|
|
1667
1691
|
return cachedPathAliases;
|
|
1692
|
+
}
|
|
1693
|
+
} catch {
|
|
1694
|
+
// File doesn't exist or can't be read, continue with fresh parse
|
|
1695
|
+
}
|
|
1668
1696
|
}
|
|
1669
1697
|
|
|
1670
1698
|
const aliases = new Map<string, string>();
|
|
1699
|
+
let parsedSuccessfully = false;
|
|
1671
1700
|
|
|
1672
1701
|
// Try to read tsconfig.json
|
|
1673
|
-
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
1674
1702
|
if (fs.existsSync(tsconfigPath)) {
|
|
1675
1703
|
try {
|
|
1676
1704
|
const content = fs.readFileSync(tsconfigPath, "utf-8");
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
1681
|
-
.replace(/,\s*([\]}])/g, "$1");
|
|
1705
|
+
const cleanContent = cleanTsconfigContent(content);
|
|
1706
|
+
|
|
1707
|
+
// Try to parse the cleaned content
|
|
1682
1708
|
const tsconfig = JSON.parse(cleanContent);
|
|
1709
|
+
parsedSuccessfully = true;
|
|
1683
1710
|
|
|
1684
1711
|
const paths = tsconfig.compilerOptions?.paths || {};
|
|
1685
|
-
const baseUrl = tsconfig.compilerOptions?.baseUrl || ".";
|
|
1686
1712
|
|
|
1687
1713
|
// Parse path mappings
|
|
1688
1714
|
for (const [alias, targets] of Object.entries(paths)) {
|
|
@@ -1695,8 +1721,30 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
1695
1721
|
}
|
|
1696
1722
|
|
|
1697
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
|
+
}
|
|
1698
1732
|
} catch (e) {
|
|
1699
|
-
|
|
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;
|
|
1700
1748
|
}
|
|
1701
1749
|
}
|
|
1702
1750
|
|
|
@@ -1714,8 +1762,12 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
1714
1762
|
debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
|
|
1715
1763
|
}
|
|
1716
1764
|
|
|
1765
|
+
// Only cache if we parsed successfully or there's no tsconfig
|
|
1766
|
+
if (parsedSuccessfully || !fs.existsSync(tsconfigPath)) {
|
|
1717
1767
|
cachedPathAliases = aliases;
|
|
1718
1768
|
cachedProjectRoot = projectRoot;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1719
1771
|
return aliases;
|
|
1720
1772
|
}
|
|
1721
1773
|
|
|
@@ -1835,6 +1887,105 @@ interface ApplyPatchesResult {
|
|
|
1835
1887
|
failedPatches: { patch: Patch; error: string }[];
|
|
1836
1888
|
}
|
|
1837
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
|
+
|
|
1838
1989
|
/**
|
|
1839
1990
|
* Apply search/replace patches to file content
|
|
1840
1991
|
* This is the core of the patch-based editing system
|
|
@@ -1849,33 +2000,70 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
|
|
|
1849
2000
|
const normalizedSearch = patch.search.replace(/\\n/g, "\n");
|
|
1850
2001
|
const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
|
|
1851
2002
|
|
|
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
|
|
2003
|
+
// Strategy 1: Exact match
|
|
2004
|
+
if (content.includes(normalizedSearch)) {
|
|
1872
2005
|
const index = content.indexOf(normalizedSearch);
|
|
1873
2006
|
content =
|
|
1874
2007
|
content.substring(0, index) +
|
|
1875
2008
|
normalizedReplace +
|
|
1876
2009
|
content.substring(index + normalizedSearch.length);
|
|
1877
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;
|
|
1878
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
|
+
});
|
|
1879
2067
|
}
|
|
1880
2068
|
|
|
1881
2069
|
return {
|
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",
|