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
- // Return cached if same project
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
- // Remove comments (tsconfig allows them) and trailing commas (valid in tsconfig but not JSON)
1774
- const cleanContent = content
1775
- .replace(/\/\/.*$/gm, "")
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
- debugLog("[apply] Failed to parse tsconfig.json", { error: String(e) });
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
- // Check if search string exists in content
1943
- if (!content.includes(normalizedSearch)) {
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
- // Return cached if same project
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
- // Remove comments (tsconfig allows them) and trailing commas (valid in tsconfig but not JSON)
1678
- const cleanContent = content
1679
- .replace(/\/\/.*$/gm, "")
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
- debugLog("[edit] Failed to parse tsconfig.json", { error: String(e) });
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
- // Check if search string exists in content
1853
- if (!content.includes(normalizedSearch)) {
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.46",
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",