sonance-brand-mcp 1.3.85 → 1.3.87

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.
@@ -94,22 +94,36 @@ function debugLog(message: string, data?: unknown) {
94
94
  /**
95
95
  * Extract JSON from LLM response that may contain preamble text
96
96
  * Handles: pure JSON, markdown code fences, and text with embedded JSON
97
+ *
98
+ * PRIORITY ORDER:
99
+ * 1. Direct JSON (starts with {)
100
+ * 2. Explicit ```json fence (most reliable)
101
+ * 3. Raw JSON object in text (handles prose + JSON responses)
102
+ * 4. Generic code fence (last resort, may match tsx/js fences incorrectly)
97
103
  */
98
104
  function extractJsonFromResponse(text: string): string {
99
105
  // Try direct parse first - if it starts with {, it's likely pure JSON
100
106
  const trimmed = text.trim();
101
107
  if (trimmed.startsWith('{')) return trimmed;
102
108
 
103
- // Extract from markdown code fence (```json ... ``` or ``` ... ```)
104
- const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
105
- if (fenceMatch) {
106
- const extracted = fenceMatch[1].trim();
107
- debugLog("Extracted JSON from markdown fence", { previewLength: extracted.length });
109
+ // PRIORITY 1: Look specifically for ```json fence (most reliable)
110
+ const jsonFenceMatch = text.match(/```json\s*([\s\S]*?)```/);
111
+ if (jsonFenceMatch) {
112
+ const extracted = jsonFenceMatch[1].trim();
113
+ debugLog("Extracted JSON from explicit json fence", { previewLength: extracted.length });
108
114
  return extracted;
109
115
  }
110
116
 
111
- // Find the JSON object in the text (for responses with preamble)
112
- const jsonStart = text.indexOf('{');
117
+ // PRIORITY 2: Find raw JSON object in text (handles prose + JSON at end)
118
+ // Look for {"modifications" pattern which is our expected response format
119
+ const modMatch = text.match(/(\{"modifications"\s*:\s*\[[\s\S]*\](?:\s*,\s*"explanation"\s*:\s*"[^"]*")?\s*\})/);
120
+ if (modMatch) {
121
+ debugLog("Extracted JSON via modifications pattern", { length: modMatch[1].length });
122
+ return modMatch[1];
123
+ }
124
+
125
+ // PRIORITY 3: Find any JSON object starting with {"
126
+ const jsonStart = text.indexOf('{"');
113
127
  const jsonEnd = text.lastIndexOf('}');
114
128
  if (jsonStart !== -1 && jsonEnd > jsonStart) {
115
129
  const extracted = text.slice(jsonStart, jsonEnd + 1);
@@ -120,6 +134,18 @@ function extractJsonFromResponse(text: string): string {
120
134
  return extracted;
121
135
  }
122
136
 
137
+ // PRIORITY 4 (last resort): Try generic code fence
138
+ // This may incorrectly match tsx/js fences, so it's lowest priority
139
+ const fenceMatch = text.match(/```\s*([\s\S]*?)```/);
140
+ if (fenceMatch) {
141
+ const extracted = fenceMatch[1].trim();
142
+ // Only use if it looks like JSON
143
+ if (extracted.startsWith('{')) {
144
+ debugLog("Extracted JSON from generic fence", { previewLength: extracted.length });
145
+ return extracted;
146
+ }
147
+ }
148
+
123
149
  // Return as-is if no JSON structure found
124
150
  return text;
125
151
  }
@@ -944,41 +970,41 @@ export async function POST(request: Request) {
944
970
  });
945
971
  }
946
972
 
947
- // CURSOR-STYLE: If focused element search has high confidence, use it directly
948
- // Skip the LLM guessing phase - we found the file that contains the code
949
- const HIGH_CONFIDENCE_THRESHOLD = 50; // Score of 50+ means we found exact matches
950
-
951
- if (focusedElementHints.length > 0 && focusedElementHints[0].score >= HIGH_CONFIDENCE_THRESHOLD) {
952
- // HIGH CONFIDENCE: Use focused element match directly
973
+ // IMPROVED: Relative scoring - compare focused element vs smart search
974
+ // This prevents false positives from generic element IDs in wrong files
975
+ const smartSearchTopScore = searchResults[0]?.score || 0;
976
+ const smartSearchTopPath = searchResults[0]?.path || null;
977
+ const focusedTopScore = focusedElementHints[0]?.score || 0;
978
+ const focusedTopPath = focusedElementHints[0]?.path || null;
979
+
980
+ // Check if Phase 2a match (component-name) is confirmed by focused element search
981
+ const phase2aConfirmed = phase2aMatches.length > 0 &&
982
+ focusedElementHints.some(h => phase2aMatches.includes(h.path) && h.score > 0);
983
+
984
+ if (phase2aConfirmed) {
985
+ // PRIORITY 1: Phase 2a match confirmed by element search - highest confidence
986
+ const confirmedPath = phase2aMatches.find(p =>
987
+ focusedElementHints.some(h => h.path === p && h.score > 0)
988
+ );
953
989
  recommendedFile = {
954
- path: focusedElementHints[0].path,
955
- reason: `Content search found element (score: ${focusedElementHints[0].score}, matches: ${focusedElementHints[0].matches.join(', ')})`
990
+ path: confirmedPath!,
991
+ reason: `Phase 2a component-name match confirmed by element search`
956
992
  };
957
- debugLog("HIGH CONFIDENCE: Using focused element match directly", recommendedFile);
958
- } else if (searchResults.length > 0) {
959
- // LOW CONFIDENCE: Fall back to LLM selection
960
- const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
961
- const selectedFile = await selectBestFileFromList(
962
- screenshot,
963
- userPrompt,
964
- candidateFiles,
965
- apiKey,
966
- focusedElementHints
967
- );
968
-
969
- if (selectedFile) {
970
- recommendedFile = {
971
- path: selectedFile,
972
- reason: `LLM selected from ${candidateFiles.length} candidate files`
973
- };
974
- } else {
975
- // Fallback to highest score if LLM selection fails
976
- recommendedFile = {
977
- path: searchResults[0].path,
978
- reason: `Highest content match score (${searchResults[0].score} points)`
979
- };
980
- }
981
- debugLog("Recommended file for editing", recommendedFile);
993
+ debugLog("PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
994
+ } else if (focusedTopScore > smartSearchTopScore * 0.5 && focusedTopScore >= 400) {
995
+ // PRIORITY 2: Focused element has strong absolute score AND relative to smart search
996
+ recommendedFile = {
997
+ path: focusedTopPath!,
998
+ reason: `Focused element match (score: ${focusedTopScore}, exceeds threshold)`
999
+ };
1000
+ debugLog("PRIORITY 2: Strong focused element match", recommendedFile);
1001
+ } else if (smartSearchTopPath) {
1002
+ // PRIORITY 3: Smart search top result - trusted baseline
1003
+ recommendedFile = {
1004
+ path: smartSearchTopPath,
1005
+ reason: `Smart search top result (score: ${smartSearchTopScore})`
1006
+ };
1007
+ debugLog("PRIORITY 3: Using smart search top result", recommendedFile);
982
1008
  }
983
1009
 
984
1010
  debugLog("Phase 1+2 complete", {
@@ -90,22 +90,36 @@ function debugLog(message: string, data?: unknown) {
90
90
  /**
91
91
  * Extract JSON from LLM response that may contain preamble text
92
92
  * Handles: pure JSON, markdown code fences, and text with embedded JSON
93
+ *
94
+ * PRIORITY ORDER:
95
+ * 1. Direct JSON (starts with {)
96
+ * 2. Explicit ```json fence (most reliable)
97
+ * 3. Raw JSON object in text (handles prose + JSON responses)
98
+ * 4. Generic code fence (last resort, may match tsx/js fences incorrectly)
93
99
  */
94
100
  function extractJsonFromResponse(text: string): string {
95
101
  // Try direct parse first - if it starts with {, it's likely pure JSON
96
102
  const trimmed = text.trim();
97
103
  if (trimmed.startsWith('{')) return trimmed;
98
104
 
99
- // Extract from markdown code fence (```json ... ``` or ``` ... ```)
100
- const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
101
- if (fenceMatch) {
102
- const extracted = fenceMatch[1].trim();
103
- debugLog("Extracted JSON from markdown fence", { previewLength: extracted.length });
105
+ // PRIORITY 1: Look specifically for ```json fence (most reliable)
106
+ const jsonFenceMatch = text.match(/```json\s*([\s\S]*?)```/);
107
+ if (jsonFenceMatch) {
108
+ const extracted = jsonFenceMatch[1].trim();
109
+ debugLog("Extracted JSON from explicit json fence", { previewLength: extracted.length });
104
110
  return extracted;
105
111
  }
106
112
 
107
- // Find the JSON object in the text (for responses with preamble)
108
- const jsonStart = text.indexOf('{');
113
+ // PRIORITY 2: Find raw JSON object in text (handles prose + JSON at end)
114
+ // Look for {"modifications" pattern which is our expected response format
115
+ const modMatch = text.match(/(\{"modifications"\s*:\s*\[[\s\S]*\](?:\s*,\s*"explanation"\s*:\s*"[^"]*")?\s*\})/);
116
+ if (modMatch) {
117
+ debugLog("Extracted JSON via modifications pattern", { length: modMatch[1].length });
118
+ return modMatch[1];
119
+ }
120
+
121
+ // PRIORITY 3: Find any JSON object starting with {"
122
+ const jsonStart = text.indexOf('{"');
109
123
  const jsonEnd = text.lastIndexOf('}');
110
124
  if (jsonStart !== -1 && jsonEnd > jsonStart) {
111
125
  const extracted = text.slice(jsonStart, jsonEnd + 1);
@@ -116,6 +130,18 @@ function extractJsonFromResponse(text: string): string {
116
130
  return extracted;
117
131
  }
118
132
 
133
+ // PRIORITY 4 (last resort): Try generic code fence
134
+ // This may incorrectly match tsx/js fences, so it's lowest priority
135
+ const fenceMatch = text.match(/```\s*([\s\S]*?)```/);
136
+ if (fenceMatch) {
137
+ const extracted = fenceMatch[1].trim();
138
+ // Only use if it looks like JSON
139
+ if (extracted.startsWith('{')) {
140
+ debugLog("Extracted JSON from generic fence", { previewLength: extracted.length });
141
+ return extracted;
142
+ }
143
+ }
144
+
119
145
  // Return as-is if no JSON structure found
120
146
  return text;
121
147
  }
@@ -913,41 +939,41 @@ export async function POST(request: Request) {
913
939
  });
914
940
  }
915
941
 
916
- // CURSOR-STYLE: If focused element search has high confidence, use it directly
917
- // Skip the LLM guessing phase - we found the file that contains the code
918
- const HIGH_CONFIDENCE_THRESHOLD = 50; // Score of 50+ means we found exact matches
919
-
920
- if (focusedElementHints.length > 0 && focusedElementHints[0].score >= HIGH_CONFIDENCE_THRESHOLD) {
921
- // HIGH CONFIDENCE: Use focused element match directly
942
+ // IMPROVED: Relative scoring - compare focused element vs smart search
943
+ // This prevents false positives from generic element IDs in wrong files
944
+ const smartSearchTopScore = searchResults[0]?.score || 0;
945
+ const smartSearchTopPath = searchResults[0]?.path || null;
946
+ const focusedTopScore = focusedElementHints[0]?.score || 0;
947
+ const focusedTopPath = focusedElementHints[0]?.path || null;
948
+
949
+ // Check if Phase 2a match (component-name) is confirmed by focused element search
950
+ const phase2aConfirmed = phase2aMatches.length > 0 &&
951
+ focusedElementHints.some(h => phase2aMatches.includes(h.path) && h.score > 0);
952
+
953
+ if (phase2aConfirmed) {
954
+ // PRIORITY 1: Phase 2a match confirmed by element search - highest confidence
955
+ const confirmedPath = phase2aMatches.find(p =>
956
+ focusedElementHints.some(h => h.path === p && h.score > 0)
957
+ );
922
958
  recommendedFile = {
923
- path: focusedElementHints[0].path,
924
- reason: `Content search found element (score: ${focusedElementHints[0].score}, matches: ${focusedElementHints[0].matches.join(', ')})`
959
+ path: confirmedPath!,
960
+ reason: `Phase 2a component-name match confirmed by element search`
925
961
  };
926
- debugLog("HIGH CONFIDENCE: Using focused element match directly", recommendedFile);
927
- } else if (searchResults.length > 0) {
928
- // LOW CONFIDENCE: Fall back to LLM selection
929
- const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
930
- const selectedFile = await selectBestFileFromList(
931
- screenshot,
932
- userPrompt,
933
- candidateFiles,
934
- apiKey,
935
- focusedElementHints
936
- );
937
-
938
- if (selectedFile) {
939
- recommendedFile = {
940
- path: selectedFile,
941
- reason: `LLM selected from ${candidateFiles.length} candidate files`
942
- };
943
- } else {
944
- // Fallback to highest score if LLM selection fails
945
- recommendedFile = {
946
- path: searchResults[0].path,
947
- reason: `Highest content match score (${searchResults[0].score} points)`
948
- };
949
- }
950
- debugLog("Recommended file for editing", recommendedFile);
962
+ debugLog("PRIORITY 1: Phase 2a confirmed by element search", recommendedFile);
963
+ } else if (focusedTopScore > smartSearchTopScore * 0.5 && focusedTopScore >= 400) {
964
+ // PRIORITY 2: Focused element has strong absolute score AND relative to smart search
965
+ recommendedFile = {
966
+ path: focusedTopPath!,
967
+ reason: `Focused element match (score: ${focusedTopScore}, exceeds threshold)`
968
+ };
969
+ debugLog("PRIORITY 2: Strong focused element match", recommendedFile);
970
+ } else if (smartSearchTopPath) {
971
+ // PRIORITY 3: Smart search top result - trusted baseline
972
+ recommendedFile = {
973
+ path: smartSearchTopPath,
974
+ reason: `Smart search top result (score: ${smartSearchTopScore})`
975
+ };
976
+ debugLog("PRIORITY 3: Using smart search top result", recommendedFile);
951
977
  }
952
978
 
953
979
  debugLog("Phase 1+2 complete", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.85",
3
+ "version": "1.3.87",
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",