sonance-brand-mcp 1.3.86 → 1.3.88
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
|
-
//
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
const extracted =
|
|
107
|
-
debugLog("Extracted JSON from
|
|
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
|
|
112
|
-
|
|
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
|
}
|
|
@@ -955,6 +981,49 @@ export async function POST(request: Request) {
|
|
|
955
981
|
const phase2aConfirmed = phase2aMatches.length > 0 &&
|
|
956
982
|
focusedElementHints.some(h => phase2aMatches.includes(h.path) && h.score > 0);
|
|
957
983
|
|
|
984
|
+
// Helper: Find best Phase 2a match based on user prompt keywords
|
|
985
|
+
// e.g., "duplicate button" should prefer ProcessRow.tsx over ProcessCatalogueView.tsx
|
|
986
|
+
const findBestPhase2aMatch = (): string | null => {
|
|
987
|
+
if (phase2aMatches.length === 0) return null;
|
|
988
|
+
if (phase2aMatches.length === 1) return phase2aMatches[0];
|
|
989
|
+
|
|
990
|
+
// Extract keywords from user prompt
|
|
991
|
+
const promptLower = userPrompt.toLowerCase();
|
|
992
|
+
const keywords = ['row', 'cell', 'item', 'button', 'action', 'duplicate', 'edit', 'delete'];
|
|
993
|
+
|
|
994
|
+
// Prefer more specific files (Row > View, Cell > Table, etc.)
|
|
995
|
+
const specificity = ['row', 'cell', 'item', 'card', 'modal', 'panel', 'detail'];
|
|
996
|
+
|
|
997
|
+
// Score each Phase 2a match
|
|
998
|
+
let bestMatch = phase2aMatches[0];
|
|
999
|
+
let bestScore = 0;
|
|
1000
|
+
|
|
1001
|
+
for (const match of phase2aMatches) {
|
|
1002
|
+
const matchLower = match.toLowerCase();
|
|
1003
|
+
let score = 0;
|
|
1004
|
+
|
|
1005
|
+
// Bonus for specificity (Row/Cell/Item components)
|
|
1006
|
+
for (const spec of specificity) {
|
|
1007
|
+
if (matchLower.includes(spec)) score += 10;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Bonus for keyword match (prompt mentions button → prefer file with button/action)
|
|
1011
|
+
for (const kw of keywords) {
|
|
1012
|
+
if (promptLower.includes(kw) && matchLower.includes(kw)) score += 20;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Penalty for generic view/container files
|
|
1016
|
+
if (matchLower.includes('view.tsx') && !matchLower.includes('detail')) score -= 5;
|
|
1017
|
+
|
|
1018
|
+
if (score > bestScore) {
|
|
1019
|
+
bestScore = score;
|
|
1020
|
+
bestMatch = match;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return bestMatch;
|
|
1025
|
+
};
|
|
1026
|
+
|
|
958
1027
|
if (phase2aConfirmed) {
|
|
959
1028
|
// PRIORITY 1: Phase 2a match confirmed by element search - highest confidence
|
|
960
1029
|
const confirmedPath = phase2aMatches.find(p =>
|
|
@@ -972,6 +1041,18 @@ export async function POST(request: Request) {
|
|
|
972
1041
|
reason: `Focused element match (score: ${focusedTopScore}, exceeds threshold)`
|
|
973
1042
|
};
|
|
974
1043
|
debugLog("PRIORITY 2: Strong focused element match", recommendedFile);
|
|
1044
|
+
} else if (phase2aMatches.length > 0) {
|
|
1045
|
+
// PRIORITY 2.5: Phase 2a match WITHOUT focused element - still strong signal
|
|
1046
|
+
// Use prompt-aware selection to pick the best match
|
|
1047
|
+
const bestMatch = findBestPhase2aMatch()!;
|
|
1048
|
+
recommendedFile = {
|
|
1049
|
+
path: bestMatch,
|
|
1050
|
+
reason: `Phase 2a component-name match (prompt-aware selection from ${phase2aMatches.length} candidates)`
|
|
1051
|
+
};
|
|
1052
|
+
debugLog("PRIORITY 2.5: Phase 2a match (no focused element)", {
|
|
1053
|
+
selectedPath: bestMatch,
|
|
1054
|
+
allCandidates: phase2aMatches
|
|
1055
|
+
});
|
|
975
1056
|
} else if (smartSearchTopPath) {
|
|
976
1057
|
// PRIORITY 3: Smart search top result - trusted baseline
|
|
977
1058
|
recommendedFile = {
|
|
@@ -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
|
-
//
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
const extracted =
|
|
103
|
-
debugLog("Extracted JSON from
|
|
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
|
|
108
|
-
|
|
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
|
}
|
|
@@ -924,6 +950,49 @@ export async function POST(request: Request) {
|
|
|
924
950
|
const phase2aConfirmed = phase2aMatches.length > 0 &&
|
|
925
951
|
focusedElementHints.some(h => phase2aMatches.includes(h.path) && h.score > 0);
|
|
926
952
|
|
|
953
|
+
// Helper: Find best Phase 2a match based on user prompt keywords
|
|
954
|
+
// e.g., "duplicate button" should prefer ProcessRow.tsx over ProcessCatalogueView.tsx
|
|
955
|
+
const findBestPhase2aMatch = (): string | null => {
|
|
956
|
+
if (phase2aMatches.length === 0) return null;
|
|
957
|
+
if (phase2aMatches.length === 1) return phase2aMatches[0];
|
|
958
|
+
|
|
959
|
+
// Extract keywords from user prompt
|
|
960
|
+
const promptLower = userPrompt.toLowerCase();
|
|
961
|
+
const keywords = ['row', 'cell', 'item', 'button', 'action', 'duplicate', 'edit', 'delete'];
|
|
962
|
+
|
|
963
|
+
// Prefer more specific files (Row > View, Cell > Table, etc.)
|
|
964
|
+
const specificity = ['row', 'cell', 'item', 'card', 'modal', 'panel', 'detail'];
|
|
965
|
+
|
|
966
|
+
// Score each Phase 2a match
|
|
967
|
+
let bestMatch = phase2aMatches[0];
|
|
968
|
+
let bestScore = 0;
|
|
969
|
+
|
|
970
|
+
for (const match of phase2aMatches) {
|
|
971
|
+
const matchLower = match.toLowerCase();
|
|
972
|
+
let score = 0;
|
|
973
|
+
|
|
974
|
+
// Bonus for specificity (Row/Cell/Item components)
|
|
975
|
+
for (const spec of specificity) {
|
|
976
|
+
if (matchLower.includes(spec)) score += 10;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Bonus for keyword match (prompt mentions button → prefer file with button/action)
|
|
980
|
+
for (const kw of keywords) {
|
|
981
|
+
if (promptLower.includes(kw) && matchLower.includes(kw)) score += 20;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Penalty for generic view/container files
|
|
985
|
+
if (matchLower.includes('view.tsx') && !matchLower.includes('detail')) score -= 5;
|
|
986
|
+
|
|
987
|
+
if (score > bestScore) {
|
|
988
|
+
bestScore = score;
|
|
989
|
+
bestMatch = match;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return bestMatch;
|
|
994
|
+
};
|
|
995
|
+
|
|
927
996
|
if (phase2aConfirmed) {
|
|
928
997
|
// PRIORITY 1: Phase 2a match confirmed by element search - highest confidence
|
|
929
998
|
const confirmedPath = phase2aMatches.find(p =>
|
|
@@ -941,6 +1010,18 @@ export async function POST(request: Request) {
|
|
|
941
1010
|
reason: `Focused element match (score: ${focusedTopScore}, exceeds threshold)`
|
|
942
1011
|
};
|
|
943
1012
|
debugLog("PRIORITY 2: Strong focused element match", recommendedFile);
|
|
1013
|
+
} else if (phase2aMatches.length > 0) {
|
|
1014
|
+
// PRIORITY 2.5: Phase 2a match WITHOUT focused element - still strong signal
|
|
1015
|
+
// Use prompt-aware selection to pick the best match
|
|
1016
|
+
const bestMatch = findBestPhase2aMatch()!;
|
|
1017
|
+
recommendedFile = {
|
|
1018
|
+
path: bestMatch,
|
|
1019
|
+
reason: `Phase 2a component-name match (prompt-aware selection from ${phase2aMatches.length} candidates)`
|
|
1020
|
+
};
|
|
1021
|
+
debugLog("PRIORITY 2.5: Phase 2a match (no focused element)", {
|
|
1022
|
+
selectedPath: bestMatch,
|
|
1023
|
+
allCandidates: phase2aMatches
|
|
1024
|
+
});
|
|
944
1025
|
} else if (smartSearchTopPath) {
|
|
945
1026
|
// PRIORITY 3: Smart search top result - trusted baseline
|
|
946
1027
|
recommendedFile = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.88",
|
|
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",
|