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 valid JSON:
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
- // Return cached if same project
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
- // Remove comments (tsconfig allows them) and trailing commas (valid in tsconfig but not JSON)
1765
- const cleanContent = content
1766
- .replace(/\/\/.*$/gm, "")
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
- 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;
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
- // Check if search string exists in content
1934
- if (!content.includes(normalizedSearch)) {
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 valid JSON:
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
- // Return cached if same project
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
- // Remove comments (tsconfig allows them) and trailing commas (valid in tsconfig but not JSON)
1670
- const cleanContent = content
1671
- .replace(/\/\/.*$/gm, "")
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
- 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;
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
- // Check if search string exists in content
1845
- if (!content.includes(normalizedSearch)) {
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-4 py-3 border-b border-gray-200 bg-[#333F48]",
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-2">
2384
- <GripHorizontal className="h-4 w-4 text-white/50" />
2385
- <Palette className="h-5 w-5 text-[#00A3E1]" />
2386
- <span id="span-sonance-devtools" className="text-sm font-semibold text-white">
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.45",
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",