sonance-brand-mcp 1.3.44 → 1.3.46
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.
|
@@ -168,6 +168,73 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Phase 3: Ask LLM to select the best file from actual file list
|
|
173
|
+
* This replaces guessing - the LLM sees the real filenames and picks one
|
|
174
|
+
*/
|
|
175
|
+
async function selectBestFileFromList(
|
|
176
|
+
screenshot: string,
|
|
177
|
+
userPrompt: string,
|
|
178
|
+
candidateFiles: string[],
|
|
179
|
+
apiKey: string
|
|
180
|
+
): Promise<string | null> {
|
|
181
|
+
if (candidateFiles.length === 0) return null;
|
|
182
|
+
|
|
183
|
+
const anthropic = new Anthropic({ apiKey });
|
|
184
|
+
const base64Data = screenshot.split(",")[1] || screenshot;
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const response = await anthropic.messages.create({
|
|
188
|
+
model: "claude-sonnet-4-20250514",
|
|
189
|
+
max_tokens: 256,
|
|
190
|
+
messages: [{
|
|
191
|
+
role: "user",
|
|
192
|
+
content: [
|
|
193
|
+
{
|
|
194
|
+
type: "image",
|
|
195
|
+
source: { type: "base64", media_type: "image/png", data: base64Data }
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: "text",
|
|
199
|
+
text: `The user wants to: "${userPrompt}"
|
|
200
|
+
|
|
201
|
+
Here are the actual component files found in this codebase:
|
|
202
|
+
${candidateFiles.map((f, i) => `${i + 1}. ${f}`).join('\n')}
|
|
203
|
+
|
|
204
|
+
Looking at the screenshot, which file MOST LIKELY contains the UI elements the user wants to modify?
|
|
205
|
+
|
|
206
|
+
IMPORTANT: Return ONLY the exact file path from the list above (e.g., "components/ProcessCatalogue/ProcessDetailPanel.tsx").
|
|
207
|
+
Do not add any explanation or other text.`
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}]
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const textBlock = response.content.find(b => b.type === "text");
|
|
214
|
+
if (!textBlock || textBlock.type !== "text") return null;
|
|
215
|
+
|
|
216
|
+
const selectedPath = textBlock.text.trim().replace(/^["']|["']$/g, ''); // Remove quotes if present
|
|
217
|
+
|
|
218
|
+
// Validate it's in our list (exact match or ends with the path)
|
|
219
|
+
const matchedFile = candidateFiles.find(f =>
|
|
220
|
+
f === selectedPath ||
|
|
221
|
+
f.endsWith(selectedPath) ||
|
|
222
|
+
selectedPath.endsWith(f)
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
debugLog("Phase 3: LLM selected file from list", {
|
|
226
|
+
selectedPath,
|
|
227
|
+
matchedFile,
|
|
228
|
+
candidateCount: candidateFiles.length
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return matchedFile || null;
|
|
232
|
+
} catch (e) {
|
|
233
|
+
debugLog("Phase 3: Failed to select file from list", { error: String(e) });
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
171
238
|
/**
|
|
172
239
|
* Search for component files by name across the project
|
|
173
240
|
* Returns the file path if found, null otherwise
|
|
@@ -408,7 +475,7 @@ Return search/replace patches (NOT full files). The system applies your patches
|
|
|
408
475
|
- Blaze Blue: #00A3E1
|
|
409
476
|
|
|
410
477
|
**RESPONSE FORMAT:**
|
|
411
|
-
Return ONLY
|
|
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:
|
|
412
479
|
{
|
|
413
480
|
"reasoning": "What you understood from the request and your plan",
|
|
414
481
|
"modifications": [
|
|
@@ -519,37 +586,23 @@ export async function POST(request: Request) {
|
|
|
519
586
|
const searchResults = searchFilesSmart(analysis, projectRoot, 10);
|
|
520
587
|
smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
|
|
521
588
|
|
|
522
|
-
//
|
|
589
|
+
// PHASE 3: Ask LLM to pick the best file from actual file list
|
|
523
590
|
if (searchResults.length > 0) {
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
591
|
+
const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
|
|
592
|
+
const selectedFile = await selectBestFileFromList(
|
|
593
|
+
screenshot,
|
|
594
|
+
userPrompt,
|
|
595
|
+
candidateFiles,
|
|
596
|
+
apiKey
|
|
597
|
+
);
|
|
529
598
|
|
|
530
|
-
|
|
531
|
-
const filenameMatch = searchResults.find(r => {
|
|
532
|
-
const filename = getFilenameWithoutExt(r.path);
|
|
533
|
-
return analysis.componentNames.some(name => {
|
|
534
|
-
const nameLower = name.toLowerCase();
|
|
535
|
-
// Exact match: ProcessDetailPanel === processdetailpanel
|
|
536
|
-
// Or filename contains the component name
|
|
537
|
-
return filename === nameLower || filename.includes(nameLower);
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
if (filenameMatch) {
|
|
542
|
-
const matchedName = analysis.componentNames.find(name => {
|
|
543
|
-
const filename = getFilenameWithoutExt(filenameMatch.path);
|
|
544
|
-
const nameLower = name.toLowerCase();
|
|
545
|
-
return filename === nameLower || filename.includes(nameLower);
|
|
546
|
-
});
|
|
599
|
+
if (selectedFile) {
|
|
547
600
|
recommendedFile = {
|
|
548
|
-
path:
|
|
549
|
-
reason: `
|
|
601
|
+
path: selectedFile,
|
|
602
|
+
reason: `LLM selected from ${candidateFiles.length} candidate files`
|
|
550
603
|
};
|
|
551
604
|
} else {
|
|
552
|
-
//
|
|
605
|
+
// Fallback to highest score if LLM selection fails
|
|
553
606
|
recommendedFile = {
|
|
554
607
|
path: searchResults[0].path,
|
|
555
608
|
reason: `Highest content match score (${searchResults[0].score} points)`
|
|
@@ -810,6 +863,15 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
|
|
|
810
863
|
}
|
|
811
864
|
|
|
812
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
|
+
|
|
813
875
|
aiResponse = JSON.parse(jsonText);
|
|
814
876
|
} catch {
|
|
815
877
|
console.error("Failed to parse AI response:", textResponse.text);
|
|
@@ -166,6 +166,73 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Phase 3: Ask LLM to select the best file from actual file list
|
|
171
|
+
* This replaces guessing - the LLM sees the real filenames and picks one
|
|
172
|
+
*/
|
|
173
|
+
async function selectBestFileFromList(
|
|
174
|
+
screenshot: string,
|
|
175
|
+
userPrompt: string,
|
|
176
|
+
candidateFiles: string[],
|
|
177
|
+
apiKey: string
|
|
178
|
+
): Promise<string | null> {
|
|
179
|
+
if (candidateFiles.length === 0) return null;
|
|
180
|
+
|
|
181
|
+
const anthropic = new Anthropic({ apiKey });
|
|
182
|
+
const base64Data = screenshot.split(",")[1] || screenshot;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const response = await anthropic.messages.create({
|
|
186
|
+
model: "claude-sonnet-4-20250514",
|
|
187
|
+
max_tokens: 256,
|
|
188
|
+
messages: [{
|
|
189
|
+
role: "user",
|
|
190
|
+
content: [
|
|
191
|
+
{
|
|
192
|
+
type: "image",
|
|
193
|
+
source: { type: "base64", media_type: "image/png", data: base64Data }
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
type: "text",
|
|
197
|
+
text: `The user wants to: "${userPrompt}"
|
|
198
|
+
|
|
199
|
+
Here are the actual component files found in this codebase:
|
|
200
|
+
${candidateFiles.map((f, i) => `${i + 1}. ${f}`).join('\n')}
|
|
201
|
+
|
|
202
|
+
Looking at the screenshot, which file MOST LIKELY contains the UI elements the user wants to modify?
|
|
203
|
+
|
|
204
|
+
IMPORTANT: Return ONLY the exact file path from the list above (e.g., "components/ProcessCatalogue/ProcessDetailPanel.tsx").
|
|
205
|
+
Do not add any explanation or other text.`
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}]
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const textBlock = response.content.find(b => b.type === "text");
|
|
212
|
+
if (!textBlock || textBlock.type !== "text") return null;
|
|
213
|
+
|
|
214
|
+
const selectedPath = textBlock.text.trim().replace(/^["']|["']$/g, ''); // Remove quotes if present
|
|
215
|
+
|
|
216
|
+
// Validate it's in our list (exact match or ends with the path)
|
|
217
|
+
const matchedFile = candidateFiles.find(f =>
|
|
218
|
+
f === selectedPath ||
|
|
219
|
+
f.endsWith(selectedPath) ||
|
|
220
|
+
selectedPath.endsWith(f)
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
debugLog("Phase 3: LLM selected file from list", {
|
|
224
|
+
selectedPath,
|
|
225
|
+
matchedFile,
|
|
226
|
+
candidateCount: candidateFiles.length
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return matchedFile || null;
|
|
230
|
+
} catch (e) {
|
|
231
|
+
debugLog("Phase 3: Failed to select file from list", { error: String(e) });
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
169
236
|
/**
|
|
170
237
|
* Search for component files by name across the project (for smart search)
|
|
171
238
|
* Returns the file path if found, null otherwise
|
|
@@ -406,7 +473,7 @@ Return search/replace patches (NOT full files). The system applies your patches
|
|
|
406
473
|
- Blaze Blue: #00A3E1
|
|
407
474
|
|
|
408
475
|
**RESPONSE FORMAT:**
|
|
409
|
-
Return ONLY
|
|
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:
|
|
410
477
|
{
|
|
411
478
|
"reasoning": "What you understood from the request and your plan",
|
|
412
479
|
"modifications": [
|
|
@@ -529,37 +596,23 @@ export async function POST(request: Request) {
|
|
|
529
596
|
const searchResults = searchFilesSmart(analysis, projectRoot, 10);
|
|
530
597
|
smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
|
|
531
598
|
|
|
532
|
-
//
|
|
599
|
+
// PHASE 3: Ask LLM to pick the best file from actual file list
|
|
533
600
|
if (searchResults.length > 0) {
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
const filenameMatch = searchResults.find(r => {
|
|
542
|
-
const filename = getFilenameWithoutExt(r.path);
|
|
543
|
-
return analysis.componentNames.some(name => {
|
|
544
|
-
const nameLower = name.toLowerCase();
|
|
545
|
-
// Exact match: ProcessDetailPanel === processdetailpanel
|
|
546
|
-
// Or filename contains the component name
|
|
547
|
-
return filename === nameLower || filename.includes(nameLower);
|
|
548
|
-
});
|
|
549
|
-
});
|
|
601
|
+
const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
|
|
602
|
+
const selectedFile = await selectBestFileFromList(
|
|
603
|
+
screenshot,
|
|
604
|
+
userPrompt,
|
|
605
|
+
candidateFiles,
|
|
606
|
+
apiKey
|
|
607
|
+
);
|
|
550
608
|
|
|
551
|
-
if (
|
|
552
|
-
const matchedName = analysis.componentNames.find(name => {
|
|
553
|
-
const filename = getFilenameWithoutExt(filenameMatch.path);
|
|
554
|
-
const nameLower = name.toLowerCase();
|
|
555
|
-
return filename === nameLower || filename.includes(nameLower);
|
|
556
|
-
});
|
|
609
|
+
if (selectedFile) {
|
|
557
610
|
recommendedFile = {
|
|
558
|
-
path:
|
|
559
|
-
reason: `
|
|
611
|
+
path: selectedFile,
|
|
612
|
+
reason: `LLM selected from ${candidateFiles.length} candidate files`
|
|
560
613
|
};
|
|
561
614
|
} else {
|
|
562
|
-
//
|
|
615
|
+
// Fallback to highest score if LLM selection fails
|
|
563
616
|
recommendedFile = {
|
|
564
617
|
path: searchResults[0].path,
|
|
565
618
|
reason: `Highest content match score (${searchResults[0].score} points)`
|
|
@@ -825,6 +878,14 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
|
|
|
825
878
|
// Clean up any remaining whitespace
|
|
826
879
|
jsonText = jsonText.trim();
|
|
827
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
|
+
|
|
828
889
|
aiResponse = JSON.parse(jsonText);
|
|
829
890
|
} catch {
|
|
830
891
|
console.error("Failed to parse AI response:", textResponse.text);
|
|
@@ -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-
|
|
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-
|
|
2384
|
-
<GripHorizontal className="h-
|
|
2385
|
-
<Palette className="h-
|
|
2386
|
-
<span id="span-sonance-devtools" className="text-
|
|
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.
|
|
3
|
+
"version": "1.3.46",
|
|
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",
|