sonance-brand-mcp 1.3.19 → 1.3.20
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.
- package/dist/assets/api/sonance-ai-edit/route.ts +485 -0
- package/dist/assets/api/sonance-component-source/route.ts +78 -0
- package/dist/assets/api/sonance-find-component/route.ts +174 -0
- package/dist/assets/api/sonance-save-colors/route.ts +181 -0
- package/dist/assets/api/sonance-vision-apply/route.ts +652 -0
- package/dist/assets/api/sonance-vision-edit/route.ts +532 -0
- package/dist/index.js +67 -0
- package/package.json +1 -1
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sonance DevTools API - Vision Mode Editor
|
|
8
|
+
*
|
|
9
|
+
* Uses Claude's vision capabilities to understand page screenshots and modify
|
|
10
|
+
* multiple files based on natural language requests.
|
|
11
|
+
*
|
|
12
|
+
* Supports actions:
|
|
13
|
+
* - "edit": Analyze screenshot + code context, generate multi-file modifications
|
|
14
|
+
* - "save": Write all modified files to disk
|
|
15
|
+
*
|
|
16
|
+
* DEVELOPMENT ONLY.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface VisionFocusedElement {
|
|
20
|
+
name: string;
|
|
21
|
+
type: string;
|
|
22
|
+
variantId?: string;
|
|
23
|
+
coordinates: {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
};
|
|
29
|
+
description?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface VisionFileModification {
|
|
33
|
+
filePath: string;
|
|
34
|
+
originalContent: string;
|
|
35
|
+
modifiedContent: string;
|
|
36
|
+
diff: string;
|
|
37
|
+
explanation: string;
|
|
38
|
+
previewCSS?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface VisionEditRequest {
|
|
42
|
+
action: "edit" | "save";
|
|
43
|
+
screenshot?: string;
|
|
44
|
+
pageRoute: string;
|
|
45
|
+
userPrompt: string;
|
|
46
|
+
focusedElements?: VisionFocusedElement[];
|
|
47
|
+
modifications?: VisionFileModification[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface VisionEditResponse {
|
|
51
|
+
success: boolean;
|
|
52
|
+
modifications?: VisionFileModification[];
|
|
53
|
+
aggregatedPreviewCSS?: string;
|
|
54
|
+
explanation?: string;
|
|
55
|
+
reasoning?: string;
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const VISION_SYSTEM_PROMPT = `You are a React/Tailwind CSS expert with vision capabilities for the Sonance brand system.
|
|
60
|
+
|
|
61
|
+
You can see screenshots of web pages and understand their visual layout, then modify code to implement requested changes.
|
|
62
|
+
|
|
63
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
64
|
+
CRITICAL RULES
|
|
65
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
66
|
+
|
|
67
|
+
**ANALYSIS:**
|
|
68
|
+
1. CAREFULLY analyze the screenshot to understand the current visual state
|
|
69
|
+
2. Identify elements mentioned in the user's request
|
|
70
|
+
3. Understand the current styling and layout
|
|
71
|
+
4. Consider how changes will affect the overall design
|
|
72
|
+
|
|
73
|
+
**PRESERVATION RULES:**
|
|
74
|
+
5. NEVER delete or remove existing content, children, or JSX elements
|
|
75
|
+
6. NEVER change component structure unless specifically requested
|
|
76
|
+
7. NEVER modify TypeScript types, imports, or exports unless necessary
|
|
77
|
+
8. NEVER remove data-sonance-* attributes
|
|
78
|
+
|
|
79
|
+
**CHANGE RULES:**
|
|
80
|
+
9. Make ONLY the changes requested by the user
|
|
81
|
+
10. Modify the minimum amount of code necessary
|
|
82
|
+
11. Use semantic Tailwind classes (bg-primary, text-foreground, etc.)
|
|
83
|
+
12. Maintain dark mode compatibility with CSS variables
|
|
84
|
+
13. Keep the cn() utility for className merging
|
|
85
|
+
|
|
86
|
+
**SONANCE BRAND COLORS:**
|
|
87
|
+
- Charcoal: #333F48, #343D46 (primary)
|
|
88
|
+
- Silver: #E2E2E2, #D1D1D6 (secondary)
|
|
89
|
+
- IPORT Orange: #FC4C02
|
|
90
|
+
- IPORT Dark: #0F161D
|
|
91
|
+
- Blaze Blue: #00A3E1
|
|
92
|
+
- Blaze Red: #C02B0A
|
|
93
|
+
|
|
94
|
+
**RESPONSE FORMAT:**
|
|
95
|
+
Return ONLY a valid JSON object. Do not include any conversational text before or after the JSON.
|
|
96
|
+
The JSON must include:
|
|
97
|
+
- "reasoning": Brief explanation of what you see in the screenshot and your plan
|
|
98
|
+
- "modifications": Array of file modifications, each with:
|
|
99
|
+
- "filePath": Path to the file
|
|
100
|
+
- "modifiedContent": Complete updated file content
|
|
101
|
+
- "explanation": What changed in this file
|
|
102
|
+
- "previewCSS": CSS for live preview (use [data-sonance-name="ComponentName"] selectors)
|
|
103
|
+
- "aggregatedPreviewCSS": Combined CSS for all changes
|
|
104
|
+
- "explanation": Overall summary of changes`;
|
|
105
|
+
|
|
106
|
+
export async function POST(request: Request) {
|
|
107
|
+
// Only allow in development
|
|
108
|
+
if (process.env.NODE_ENV !== "development") {
|
|
109
|
+
return NextResponse.json(
|
|
110
|
+
{ error: "Vision edit is only available in development mode" },
|
|
111
|
+
{ status: 403 }
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const body: VisionEditRequest = await request.json();
|
|
117
|
+
const { action, screenshot, pageRoute, userPrompt, focusedElements, modifications } = body;
|
|
118
|
+
|
|
119
|
+
const projectRoot = process.cwd();
|
|
120
|
+
|
|
121
|
+
// Handle save action - write all modified files
|
|
122
|
+
if (action === "save") {
|
|
123
|
+
if (!modifications || modifications.length === 0) {
|
|
124
|
+
return NextResponse.json(
|
|
125
|
+
{ error: "modifications array is required for save action" },
|
|
126
|
+
{ status: 400 }
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Create backups first
|
|
131
|
+
const backups: { filePath: string; content: string }[] = [];
|
|
132
|
+
for (const mod of modifications) {
|
|
133
|
+
const fullPath = path.join(projectRoot, mod.filePath);
|
|
134
|
+
if (fs.existsSync(fullPath)) {
|
|
135
|
+
backups.push({
|
|
136
|
+
filePath: mod.filePath,
|
|
137
|
+
content: fs.readFileSync(fullPath, "utf-8"),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Write all files
|
|
144
|
+
for (const mod of modifications) {
|
|
145
|
+
const fullPath = path.join(projectRoot, mod.filePath);
|
|
146
|
+
|
|
147
|
+
// Validate path is within project
|
|
148
|
+
if (!fullPath.startsWith(projectRoot)) {
|
|
149
|
+
throw new Error(`Invalid file path: ${mod.filePath}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fs.writeFileSync(fullPath, mod.modifiedContent, "utf-8");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return NextResponse.json({
|
|
156
|
+
success: true,
|
|
157
|
+
message: `Successfully updated ${modifications.length} file(s)`,
|
|
158
|
+
});
|
|
159
|
+
} catch (writeError) {
|
|
160
|
+
// Rollback on error
|
|
161
|
+
for (const backup of backups) {
|
|
162
|
+
try {
|
|
163
|
+
const fullPath = path.join(projectRoot, backup.filePath);
|
|
164
|
+
fs.writeFileSync(fullPath, backup.content, "utf-8");
|
|
165
|
+
} catch {
|
|
166
|
+
console.error(`Failed to rollback ${backup.filePath}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw writeError;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle edit action (Vision AI modification)
|
|
174
|
+
if (action === "edit") {
|
|
175
|
+
if (!userPrompt) {
|
|
176
|
+
return NextResponse.json(
|
|
177
|
+
{ error: "userPrompt is required for edit action" },
|
|
178
|
+
{ status: 400 }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for API key
|
|
183
|
+
const apiKey = process.env.NEXT_PUBLIC_CLAUDE_API_KEY;
|
|
184
|
+
if (!apiKey) {
|
|
185
|
+
return NextResponse.json(
|
|
186
|
+
{ error: "NEXT_PUBLIC_CLAUDE_API_KEY not configured. Add it to your .env.local file." },
|
|
187
|
+
{ status: 500 }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Gather page context
|
|
192
|
+
const pageContext = gatherPageContext(pageRoute, projectRoot);
|
|
193
|
+
|
|
194
|
+
// Build user message with vision
|
|
195
|
+
const messageContent: Anthropic.MessageCreateParams["messages"][0]["content"] = [];
|
|
196
|
+
|
|
197
|
+
// Add screenshot if provided
|
|
198
|
+
if (screenshot) {
|
|
199
|
+
const base64Data = screenshot.split(",")[1] || screenshot;
|
|
200
|
+
messageContent.push({
|
|
201
|
+
type: "image",
|
|
202
|
+
source: {
|
|
203
|
+
type: "base64",
|
|
204
|
+
media_type: "image/png",
|
|
205
|
+
data: base64Data,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Build text content
|
|
211
|
+
let textContent = `VISION MODE EDIT REQUEST
|
|
212
|
+
|
|
213
|
+
Page Route: ${pageRoute}
|
|
214
|
+
User Request: "${userPrompt}"
|
|
215
|
+
|
|
216
|
+
`;
|
|
217
|
+
|
|
218
|
+
if (focusedElements && focusedElements.length > 0) {
|
|
219
|
+
textContent += `FOCUSED ELEMENTS (user clicked on these):
|
|
220
|
+
${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
221
|
+
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
textContent += `PAGE CONTEXT:
|
|
226
|
+
|
|
227
|
+
Page File: ${pageContext.pageFile || "Not found"}
|
|
228
|
+
${pageContext.pageContent ? `\`\`\`tsx\n${pageContext.pageContent}\n\`\`\`` : ""}
|
|
229
|
+
|
|
230
|
+
`;
|
|
231
|
+
|
|
232
|
+
if (pageContext.componentSources.length > 0) {
|
|
233
|
+
textContent += `IMPORTED COMPONENTS:\n`;
|
|
234
|
+
for (const comp of pageContext.componentSources) {
|
|
235
|
+
textContent += `
|
|
236
|
+
File: ${comp.path}
|
|
237
|
+
\`\`\`tsx
|
|
238
|
+
${comp.content.substring(0, 3000)}${comp.content.length > 3000 ? "\n// ... (truncated)" : ""}
|
|
239
|
+
\`\`\`
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
textContent += `
|
|
245
|
+
GLOBALS.CSS (relevant theme variables):
|
|
246
|
+
\`\`\`css
|
|
247
|
+
${pageContext.globalsCSS.substring(0, 2000)}${pageContext.globalsCSS.length > 2000 ? "\n/* ... (truncated) */" : ""}
|
|
248
|
+
\`\`\`
|
|
249
|
+
|
|
250
|
+
INSTRUCTIONS:
|
|
251
|
+
1. Look at the screenshot and identify elements mentioned in the user's request
|
|
252
|
+
2. Review the code to understand current implementation
|
|
253
|
+
3. Determine which files need modifications
|
|
254
|
+
4. Generate complete modified code for each file
|
|
255
|
+
5. Provide previewCSS for immediate visual feedback
|
|
256
|
+
6. Return as JSON in the specified format`;
|
|
257
|
+
|
|
258
|
+
messageContent.push({
|
|
259
|
+
type: "text",
|
|
260
|
+
text: textContent,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Call Claude Vision API
|
|
264
|
+
const anthropic = new Anthropic({ apiKey });
|
|
265
|
+
|
|
266
|
+
const response = await anthropic.messages.create({
|
|
267
|
+
model: "claude-sonnet-4-20250514",
|
|
268
|
+
max_tokens: 16384,
|
|
269
|
+
messages: [
|
|
270
|
+
{
|
|
271
|
+
role: "user",
|
|
272
|
+
content: messageContent,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
system: VISION_SYSTEM_PROMPT,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Extract text content from response
|
|
279
|
+
const textResponse = response.content.find((block) => block.type === "text");
|
|
280
|
+
if (!textResponse || textResponse.type !== "text") {
|
|
281
|
+
return NextResponse.json(
|
|
282
|
+
{ error: "No text response from AI" },
|
|
283
|
+
{ status: 500 }
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Parse AI response
|
|
288
|
+
let aiResponse: {
|
|
289
|
+
reasoning?: string;
|
|
290
|
+
modifications: Array<{
|
|
291
|
+
filePath: string;
|
|
292
|
+
modifiedContent: string;
|
|
293
|
+
explanation: string;
|
|
294
|
+
previewCSS?: string;
|
|
295
|
+
}>;
|
|
296
|
+
aggregatedPreviewCSS?: string;
|
|
297
|
+
explanation?: string;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
let jsonText = textResponse.text.trim();
|
|
302
|
+
|
|
303
|
+
// Try to extract JSON from markdown code blocks
|
|
304
|
+
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
305
|
+
jsonText.match(/```\n([\s\S]*?)\n```/);
|
|
306
|
+
|
|
307
|
+
if (jsonMatch) {
|
|
308
|
+
jsonText = jsonMatch[1];
|
|
309
|
+
} else if (jsonText.includes("```json")) {
|
|
310
|
+
// Fallback for cases where regex might miss due to newlines
|
|
311
|
+
const start = jsonText.indexOf("```json") + 7;
|
|
312
|
+
const end = jsonText.lastIndexOf("```");
|
|
313
|
+
if (end > start) {
|
|
314
|
+
jsonText = jsonText.substring(start, end);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Clean up any remaining whitespace
|
|
319
|
+
jsonText = jsonText.trim();
|
|
320
|
+
|
|
321
|
+
aiResponse = JSON.parse(jsonText);
|
|
322
|
+
} catch {
|
|
323
|
+
console.error("Failed to parse AI response:", textResponse.text);
|
|
324
|
+
return NextResponse.json(
|
|
325
|
+
{ error: "Failed to parse AI response. Please try again." },
|
|
326
|
+
{ status: 500 }
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Read original content and generate diffs for each modification
|
|
331
|
+
const modificationsWithOriginals: VisionFileModification[] = [];
|
|
332
|
+
for (const mod of aiResponse.modifications || []) {
|
|
333
|
+
const fullPath = path.join(projectRoot, mod.filePath);
|
|
334
|
+
let originalContent = "";
|
|
335
|
+
if (fs.existsSync(fullPath)) {
|
|
336
|
+
originalContent = fs.readFileSync(fullPath, "utf-8");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
modificationsWithOriginals.push({
|
|
340
|
+
filePath: mod.filePath,
|
|
341
|
+
originalContent,
|
|
342
|
+
modifiedContent: mod.modifiedContent,
|
|
343
|
+
diff: generateSimpleDiff(originalContent, mod.modifiedContent),
|
|
344
|
+
explanation: mod.explanation,
|
|
345
|
+
previewCSS: mod.previewCSS,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Aggregate preview CSS
|
|
350
|
+
const aggregatedCSS = modificationsWithOriginals
|
|
351
|
+
.filter((m) => m.previewCSS)
|
|
352
|
+
.map((m) => `/* ${m.filePath} */\n${m.previewCSS}`)
|
|
353
|
+
.join("\n\n");
|
|
354
|
+
|
|
355
|
+
return NextResponse.json({
|
|
356
|
+
success: true,
|
|
357
|
+
modifications: modificationsWithOriginals,
|
|
358
|
+
aggregatedPreviewCSS: aiResponse.aggregatedPreviewCSS || aggregatedCSS,
|
|
359
|
+
explanation: aiResponse.explanation,
|
|
360
|
+
reasoning: aiResponse.reasoning,
|
|
361
|
+
} as VisionEditResponse);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return NextResponse.json(
|
|
365
|
+
{ error: "Invalid action. Use 'edit' or 'save'." },
|
|
366
|
+
{ status: 400 }
|
|
367
|
+
);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.error("Vision edit error:", error);
|
|
370
|
+
return NextResponse.json(
|
|
371
|
+
{ error: "Failed to process request", details: String(error) },
|
|
372
|
+
{ status: 500 }
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Gather context about the current page for AI analysis
|
|
379
|
+
*/
|
|
380
|
+
function gatherPageContext(
|
|
381
|
+
pageRoute: string,
|
|
382
|
+
projectRoot: string
|
|
383
|
+
): {
|
|
384
|
+
pageFile: string | null;
|
|
385
|
+
pageContent: string;
|
|
386
|
+
componentSources: { path: string; content: string }[];
|
|
387
|
+
globalsCSS: string;
|
|
388
|
+
} {
|
|
389
|
+
// Map route to page file
|
|
390
|
+
const pageFile = discoverPageFile(pageRoute, projectRoot);
|
|
391
|
+
let pageContent = "";
|
|
392
|
+
const componentSources: { path: string; content: string }[] = [];
|
|
393
|
+
|
|
394
|
+
if (pageFile) {
|
|
395
|
+
const fullPath = path.join(projectRoot, pageFile);
|
|
396
|
+
if (fs.existsSync(fullPath)) {
|
|
397
|
+
pageContent = fs.readFileSync(fullPath, "utf-8");
|
|
398
|
+
|
|
399
|
+
// Extract imports and read component files
|
|
400
|
+
const imports = extractImports(pageContent);
|
|
401
|
+
for (const importPath of imports) {
|
|
402
|
+
const resolvedPath = resolveImportPath(importPath, pageFile, projectRoot);
|
|
403
|
+
if (resolvedPath && fs.existsSync(path.join(projectRoot, resolvedPath))) {
|
|
404
|
+
try {
|
|
405
|
+
const content = fs.readFileSync(path.join(projectRoot, resolvedPath), "utf-8");
|
|
406
|
+
componentSources.push({ path: resolvedPath, content });
|
|
407
|
+
} catch {
|
|
408
|
+
// Skip files that can't be read
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Read globals.css
|
|
416
|
+
let globalsCSS = "";
|
|
417
|
+
const globalsPath = path.join(projectRoot, "src/app/globals.css");
|
|
418
|
+
if (fs.existsSync(globalsPath)) {
|
|
419
|
+
globalsCSS = fs.readFileSync(globalsPath, "utf-8");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return { pageFile, pageContent, componentSources, globalsCSS };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Discover the page file for a given route
|
|
427
|
+
*/
|
|
428
|
+
function discoverPageFile(route: string, projectRoot: string): string | null {
|
|
429
|
+
// Handle root route
|
|
430
|
+
if (route === "/" || route === "") {
|
|
431
|
+
const rootPage = "src/app/page.tsx";
|
|
432
|
+
if (fs.existsSync(path.join(projectRoot, rootPage))) {
|
|
433
|
+
return rootPage;
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Remove leading slash
|
|
439
|
+
const cleanRoute = route.replace(/^\//, "");
|
|
440
|
+
|
|
441
|
+
// Try different patterns
|
|
442
|
+
const patterns = [
|
|
443
|
+
`src/app/${cleanRoute}/page.tsx`,
|
|
444
|
+
`src/app/${cleanRoute}/page.jsx`,
|
|
445
|
+
`src/app/${cleanRoute}.tsx`,
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
for (const pattern of patterns) {
|
|
449
|
+
if (fs.existsSync(path.join(projectRoot, pattern))) {
|
|
450
|
+
return pattern;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Extract import paths from file content
|
|
459
|
+
*/
|
|
460
|
+
function extractImports(content: string): string[] {
|
|
461
|
+
const importRegex = /import\s+.*?\s+from\s+["'](@\/components\/[^"']+|\.\.?\/[^"']+)["']/g;
|
|
462
|
+
const imports: string[] = [];
|
|
463
|
+
let match;
|
|
464
|
+
|
|
465
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
466
|
+
imports.push(match[1]);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return imports;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Resolve an import path to a file system path
|
|
474
|
+
*/
|
|
475
|
+
function resolveImportPath(
|
|
476
|
+
importPath: string,
|
|
477
|
+
fromFile: string,
|
|
478
|
+
projectRoot: string
|
|
479
|
+
): string | null {
|
|
480
|
+
// Handle @ alias (maps to src/)
|
|
481
|
+
if (importPath.startsWith("@/")) {
|
|
482
|
+
const resolved = importPath.replace("@/", "src/");
|
|
483
|
+
// Try with .tsx extension
|
|
484
|
+
const withExt = resolved.endsWith(".tsx") ? resolved : `${resolved}.tsx`;
|
|
485
|
+
if (fs.existsSync(path.join(projectRoot, withExt))) {
|
|
486
|
+
return withExt;
|
|
487
|
+
}
|
|
488
|
+
// Try as directory with index
|
|
489
|
+
const indexPath = `${resolved}/index.tsx`;
|
|
490
|
+
if (fs.existsSync(path.join(projectRoot, indexPath))) {
|
|
491
|
+
return indexPath;
|
|
492
|
+
}
|
|
493
|
+
return withExt; // Return even if not found for context
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Handle relative imports
|
|
497
|
+
if (importPath.startsWith(".")) {
|
|
498
|
+
const dir = path.dirname(fromFile);
|
|
499
|
+
const resolved = path.join(dir, importPath);
|
|
500
|
+
const withExt = resolved.endsWith(".tsx") ? resolved : `${resolved}.tsx`;
|
|
501
|
+
return withExt;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Generate a simple unified diff between two strings
|
|
509
|
+
*/
|
|
510
|
+
function generateSimpleDiff(original: string, modified: string): string {
|
|
511
|
+
const originalLines = original.split("\n");
|
|
512
|
+
const modifiedLines = modified.split("\n");
|
|
513
|
+
|
|
514
|
+
const diff: string[] = [];
|
|
515
|
+
const maxLines = Math.max(originalLines.length, modifiedLines.length);
|
|
516
|
+
|
|
517
|
+
for (let i = 0; i < maxLines; i++) {
|
|
518
|
+
const origLine = originalLines[i];
|
|
519
|
+
const modLine = modifiedLines[i];
|
|
520
|
+
|
|
521
|
+
if (origLine === undefined && modLine !== undefined) {
|
|
522
|
+
diff.push(`+ ${modLine}`);
|
|
523
|
+
} else if (origLine !== undefined && modLine === undefined) {
|
|
524
|
+
diff.push(`- ${origLine}`);
|
|
525
|
+
} else if (origLine !== modLine) {
|
|
526
|
+
diff.push(`- ${origLine}`);
|
|
527
|
+
diff.push(`+ ${modLine}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return diff.join("\n");
|
|
532
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -356,6 +356,12 @@ function runDevToolsInstaller() {
|
|
|
356
356
|
const apiAssetsDir = path.join(targetDir, baseDir, "app/api/sonance-assets");
|
|
357
357
|
const apiInjectIdDir = path.join(targetDir, baseDir, "app/api/sonance-inject-id");
|
|
358
358
|
const apiAnalyzeDir = path.join(targetDir, baseDir, "app/api/sonance-analyze");
|
|
359
|
+
const apiVisionApplyDir = path.join(targetDir, baseDir, "app/api/sonance-vision-apply");
|
|
360
|
+
const apiVisionEditDir = path.join(targetDir, baseDir, "app/api/sonance-vision-edit");
|
|
361
|
+
const apiAiEditDir = path.join(targetDir, baseDir, "app/api/sonance-ai-edit");
|
|
362
|
+
const apiSaveColorsDir = path.join(targetDir, baseDir, "app/api/sonance-save-colors");
|
|
363
|
+
const apiComponentSourceDir = path.join(targetDir, baseDir, "app/api/sonance-component-source");
|
|
364
|
+
const apiFindComponentDir = path.join(targetDir, baseDir, "app/api/sonance-find-component");
|
|
359
365
|
const themeDir = path.join(targetDir, baseDir, "theme");
|
|
360
366
|
// Source resolution
|
|
361
367
|
let sourceBrandSystem;
|
|
@@ -369,6 +375,12 @@ function runDevToolsInstaller() {
|
|
|
369
375
|
let sourceApiAssets;
|
|
370
376
|
let sourceApiInjectId;
|
|
371
377
|
let sourceApiAnalyze;
|
|
378
|
+
let sourceApiVisionApply;
|
|
379
|
+
let sourceApiVisionEdit;
|
|
380
|
+
let sourceApiAiEdit;
|
|
381
|
+
let sourceApiSaveColors;
|
|
382
|
+
let sourceApiComponentSource;
|
|
383
|
+
let sourceApiFindComponent;
|
|
372
384
|
if (IS_BUNDLED) {
|
|
373
385
|
sourceBrandSystem = path.join(BUNDLED_ASSETS, "brand-system.ts");
|
|
374
386
|
sourceBrandContext = path.join(BUNDLED_ASSETS, "brand-context.tsx");
|
|
@@ -381,6 +393,12 @@ function runDevToolsInstaller() {
|
|
|
381
393
|
sourceApiAssets = path.join(BUNDLED_ASSETS, "api/sonance-assets/route.ts");
|
|
382
394
|
sourceApiInjectId = path.join(BUNDLED_ASSETS, "api/sonance-inject-id/route.ts");
|
|
383
395
|
sourceApiAnalyze = path.join(BUNDLED_ASSETS, "api/sonance-analyze/route.ts");
|
|
396
|
+
sourceApiVisionApply = path.join(BUNDLED_ASSETS, "api/sonance-vision-apply/route.ts");
|
|
397
|
+
sourceApiVisionEdit = path.join(BUNDLED_ASSETS, "api/sonance-vision-edit/route.ts");
|
|
398
|
+
sourceApiAiEdit = path.join(BUNDLED_ASSETS, "api/sonance-ai-edit/route.ts");
|
|
399
|
+
sourceApiSaveColors = path.join(BUNDLED_ASSETS, "api/sonance-save-colors/route.ts");
|
|
400
|
+
sourceApiComponentSource = path.join(BUNDLED_ASSETS, "api/sonance-component-source/route.ts");
|
|
401
|
+
sourceApiFindComponent = path.join(BUNDLED_ASSETS, "api/sonance-find-component/route.ts");
|
|
384
402
|
}
|
|
385
403
|
else {
|
|
386
404
|
sourceBrandSystem = path.join(DEV_PROJECT_ROOT, "src/lib/brand-system.ts");
|
|
@@ -394,6 +412,12 @@ function runDevToolsInstaller() {
|
|
|
394
412
|
sourceApiAssets = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-assets/route.ts");
|
|
395
413
|
sourceApiInjectId = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-inject-id/route.ts");
|
|
396
414
|
sourceApiAnalyze = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-analyze/route.ts");
|
|
415
|
+
sourceApiVisionApply = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-vision-apply/route.ts");
|
|
416
|
+
sourceApiVisionEdit = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-vision-edit/route.ts");
|
|
417
|
+
sourceApiAiEdit = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-ai-edit/route.ts");
|
|
418
|
+
sourceApiSaveColors = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-save-colors/route.ts");
|
|
419
|
+
sourceApiComponentSource = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-component-source/route.ts");
|
|
420
|
+
sourceApiFindComponent = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-find-component/route.ts");
|
|
397
421
|
}
|
|
398
422
|
// Verify sources exist
|
|
399
423
|
if (!fs.existsSync(sourceBrandSystem)) {
|
|
@@ -530,6 +554,49 @@ function runDevToolsInstaller() {
|
|
|
530
554
|
fs.copyFileSync(sourceApiAnalyze, path.join(apiAnalyzeDir, "route.ts"));
|
|
531
555
|
createdFiles.push(`${pathPrefix}app/api/sonance-analyze/route.ts`);
|
|
532
556
|
console.log(` ✓ Created ${pathPrefix}app/api/sonance-analyze/route.ts`);
|
|
557
|
+
// 10. Install Vision and AI API routes
|
|
558
|
+
if (!fs.existsSync(apiVisionApplyDir)) {
|
|
559
|
+
fs.mkdirSync(apiVisionApplyDir, { recursive: true });
|
|
560
|
+
}
|
|
561
|
+
createdDirectories.push(`${pathPrefix}app/api/sonance-vision-apply`);
|
|
562
|
+
fs.copyFileSync(sourceApiVisionApply, path.join(apiVisionApplyDir, "route.ts"));
|
|
563
|
+
createdFiles.push(`${pathPrefix}app/api/sonance-vision-apply/route.ts`);
|
|
564
|
+
console.log(` ✓ Created ${pathPrefix}app/api/sonance-vision-apply/route.ts`);
|
|
565
|
+
if (!fs.existsSync(apiVisionEditDir)) {
|
|
566
|
+
fs.mkdirSync(apiVisionEditDir, { recursive: true });
|
|
567
|
+
}
|
|
568
|
+
createdDirectories.push(`${pathPrefix}app/api/sonance-vision-edit`);
|
|
569
|
+
fs.copyFileSync(sourceApiVisionEdit, path.join(apiVisionEditDir, "route.ts"));
|
|
570
|
+
createdFiles.push(`${pathPrefix}app/api/sonance-vision-edit/route.ts`);
|
|
571
|
+
console.log(` ✓ Created ${pathPrefix}app/api/sonance-vision-edit/route.ts`);
|
|
572
|
+
if (!fs.existsSync(apiAiEditDir)) {
|
|
573
|
+
fs.mkdirSync(apiAiEditDir, { recursive: true });
|
|
574
|
+
}
|
|
575
|
+
createdDirectories.push(`${pathPrefix}app/api/sonance-ai-edit`);
|
|
576
|
+
fs.copyFileSync(sourceApiAiEdit, path.join(apiAiEditDir, "route.ts"));
|
|
577
|
+
createdFiles.push(`${pathPrefix}app/api/sonance-ai-edit/route.ts`);
|
|
578
|
+
console.log(` ✓ Created ${pathPrefix}app/api/sonance-ai-edit/route.ts`);
|
|
579
|
+
if (!fs.existsSync(apiSaveColorsDir)) {
|
|
580
|
+
fs.mkdirSync(apiSaveColorsDir, { recursive: true });
|
|
581
|
+
}
|
|
582
|
+
createdDirectories.push(`${pathPrefix}app/api/sonance-save-colors`);
|
|
583
|
+
fs.copyFileSync(sourceApiSaveColors, path.join(apiSaveColorsDir, "route.ts"));
|
|
584
|
+
createdFiles.push(`${pathPrefix}app/api/sonance-save-colors/route.ts`);
|
|
585
|
+
console.log(` ✓ Created ${pathPrefix}app/api/sonance-save-colors/route.ts`);
|
|
586
|
+
if (!fs.existsSync(apiComponentSourceDir)) {
|
|
587
|
+
fs.mkdirSync(apiComponentSourceDir, { recursive: true });
|
|
588
|
+
}
|
|
589
|
+
createdDirectories.push(`${pathPrefix}app/api/sonance-component-source`);
|
|
590
|
+
fs.copyFileSync(sourceApiComponentSource, path.join(apiComponentSourceDir, "route.ts"));
|
|
591
|
+
createdFiles.push(`${pathPrefix}app/api/sonance-component-source/route.ts`);
|
|
592
|
+
console.log(` ✓ Created ${pathPrefix}app/api/sonance-component-source/route.ts`);
|
|
593
|
+
if (!fs.existsSync(apiFindComponentDir)) {
|
|
594
|
+
fs.mkdirSync(apiFindComponentDir, { recursive: true });
|
|
595
|
+
}
|
|
596
|
+
createdDirectories.push(`${pathPrefix}app/api/sonance-find-component`);
|
|
597
|
+
fs.copyFileSync(sourceApiFindComponent, path.join(apiFindComponentDir, "route.ts"));
|
|
598
|
+
createdFiles.push(`${pathPrefix}app/api/sonance-find-component/route.ts`);
|
|
599
|
+
console.log(` ✓ Created ${pathPrefix}app/api/sonance-find-component/route.ts`);
|
|
533
600
|
// 11. Install brand-overrides.css for production logo sizing
|
|
534
601
|
if (!fs.existsSync(stylesDir)) {
|
|
535
602
|
fs.mkdirSync(stylesDir, { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.20",
|
|
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",
|