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.
@@ -0,0 +1,485 @@
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 - AI Component Editor
8
+ *
9
+ * Uses Claude to modify component source files based on natural language requests.
10
+ * Supports two actions:
11
+ * - "edit": Generate modified code and diff (preview)
12
+ * - "save": Write the modified code to the source file
13
+ *
14
+ * DEVELOPMENT ONLY.
15
+ */
16
+
17
+ interface VariantStyles {
18
+ backgroundColor: string;
19
+ color: string;
20
+ borderColor: string;
21
+ borderRadius: string;
22
+ borderWidth: string;
23
+ padding: string;
24
+ fontSize: string;
25
+ fontWeight: string;
26
+ boxShadow: string;
27
+ }
28
+
29
+ interface AIEditRequest {
30
+ action: "edit" | "save";
31
+ componentType: string;
32
+ filePath: string;
33
+ currentCode?: string;
34
+ modifiedCode?: string;
35
+ userRequest?: string;
36
+ // Variant-scoped editing
37
+ editScope?: "component" | "variant";
38
+ variantId?: string;
39
+ variantStyles?: VariantStyles;
40
+ }
41
+
42
+ interface AIEditResponse {
43
+ success: boolean;
44
+ modifiedCode?: string;
45
+ diff?: string;
46
+ explanation?: string;
47
+ error?: string;
48
+ // AI-generated CSS for live preview (targets data-sonance-variant selectors)
49
+ previewCSS?: string;
50
+ }
51
+
52
+ const SYSTEM_PROMPT = `You are a React/Tailwind CSS expert modifying Sonance brand components.
53
+
54
+ ═══════════════════════════════════════════════════════════════════════════════
55
+ CRITICAL RULES - FOLLOW EXACTLY
56
+ ═══════════════════════════════════════════════════════════════════════════════
57
+
58
+ **PRESERVATION RULES (NEVER VIOLATE):**
59
+ 1. NEVER delete, remove, or modify ANY existing content, children, or JSX elements
60
+ 2. NEVER remove existing className values - only ADD or MODIFY specific classes
61
+ 3. NEVER change component structure, nesting, or element hierarchy
62
+ 4. NEVER modify props interfaces, TypeScript types, imports, or exports
63
+ 5. NEVER remove data-sonance-* attributes - these are required for DevTools
64
+ 6. NEVER change any text content, children, or slot content
65
+ 7. NEVER remove existing functionality, event handlers, or logic
66
+
67
+ **CHANGE RULES:**
68
+ 8. ONLY modify the specific styling aspect mentioned in the user's request
69
+ 9. When adding border-radius, ONLY add rounded-* classes - don't change background, padding, etc.
70
+ 10. When changing colors, ONLY change the relevant color class - preserve all other styles
71
+ 11. When adjusting spacing, ONLY modify padding/margin - preserve everything else
72
+ 12. Make the MINIMUM possible change to achieve the requested result
73
+ 13. If unsure whether to change something, DON'T change it
74
+
75
+ **TECHNICAL RULES:**
76
+ 14. Return the COMPLETE modified file, not just changed parts
77
+ 15. Use semantic Tailwind classes (bg-primary, text-foreground, etc.) when available
78
+ 16. Maintain dark mode compatibility - prefer CSS variables over hardcoded colors
79
+ 17. Keep the cn() utility for className merging
80
+ 18. For hover effects, use Tailwind's hover: prefix
81
+ 19. For transitions, use transition-all duration-200
82
+
83
+ **EXAMPLE - User says "make the borders round":**
84
+ CORRECT: Only add rounded-lg or rounded-xl to the className
85
+ WRONG: Adding rounded-lg AND changing bg color AND removing children
86
+
87
+ **EXAMPLE - User says "change the background to blue":**
88
+ CORRECT: Only change bg-white to bg-blue-500 (or appropriate brand color)
89
+ WRONG: Changing background AND modifying padding AND restructuring content
90
+
91
+ VARIANT-SCOPED EDITING:
92
+ When editScope is "variant", you are targeting ONLY components that match specific computed styles.
93
+ - Add a new variant prop or conditional class that applies styles when the component matches the variant criteria
94
+ - Use data attributes like data-variant="variantId" for targeting
95
+ - Or create a new variant in the component's variant system (if using cva or similar)
96
+ - Do NOT change the default appearance of other component instances
97
+
98
+ ═══════════════════════════════════════════════════════════════════════════════
99
+ SONANCE BRAND FAMILY - COMPLETE REFERENCE
100
+ ═══════════════════════════════════════════════════════════════════════════════
101
+
102
+ BRAND QUICK REFERENCE:
103
+ | Brand | Primary Color | Accent Color | Theme Preference |
104
+ |----------|-------------------|---------------------------|------------------|
105
+ | Sonance | Charcoal #333F48 | Cyan "The Beam" #00A3E1 | Light preferred |
106
+ | IPORT | Black #0E1114 | Orange #FC4C02 | Dark preferred |
107
+ | Blaze | Black #1A1A1C | Blue #00A3E1, Red #C02B0A | Dark preferred |
108
+
109
+ ───────────────────────────────────────────────────────────────────────────────
110
+ SONANCE COLORS
111
+ ───────────────────────────────────────────────────────────────────────────────
112
+ | Color | Hex | CSS Variable | Tailwind Class |
113
+ |-------------|-----------|----------------------|----------------------|
114
+ | Charcoal | #333F48 | --sonance-charcoal | bg-sonance-charcoal |
115
+ | The Beam | #00A3E1 | --sonance-blue | bg-sonance-blue |
116
+ | Light Gray | #D9D9D6 | --sonance-light-gray | bg-sonance-light-gray|
117
+ | White | #FFFFFF | --sonance-white | bg-sonance-white |
118
+
119
+ Sonance Gray Scale:
120
+ - Gray 50: #f8f9fa, Gray 100: #f0f2f3, Gray 200: #D9D9D6
121
+ - Gray 300: #b8bfc4, Gray 400: #8f999f, Gray 500: #6b7780
122
+ - Gray 600: #515c64, Gray 700: #424c54, Gray 800: #3a444c, Gray 900: #333F48
123
+
124
+ ───────────────────────────────────────────────────────────────────────────────
125
+ IPORT COLORS (Dark Theme)
126
+ ───────────────────────────────────────────────────────────────────────────────
127
+ | Color | Hex | CSS Variable | Tailwind Class |
128
+ |--------------|-----------|------------------------|------------------------|
129
+ | Orange | #FC4C02 | --iport-orange | bg-iport-orange |
130
+ | Black | #0E1114 | --iport-black | bg-iport-black |
131
+ | Dark | #0F161D | --iport-dark | bg-iport-dark |
132
+ | Dark Gray | #1C1E20 | --iport-dark-gray | bg-iport-dark-gray |
133
+ | Medium Gray | #25282A | --iport-medium-gray | bg-iport-medium-gray |
134
+ | Gray | #3A3D3F | --iport-gray | bg-iport-gray |
135
+ | Light Gray | #CED6DB | --iport-light-gray | bg-iport-light-gray |
136
+ | White | #FFFFFF | --iport-white | bg-iport-white |
137
+
138
+ ───────────────────────────────────────────────────────────────────────────────
139
+ BLAZE AUDIO COLORS (Dark Theme)
140
+ ───────────────────────────────────────────────────────────────────────────────
141
+ | Color | Hex | CSS Variable | Tailwind Class |
142
+ |--------------|-----------|------------------------|------------------------|
143
+ | Blue | #00A3E1 | --blaze-blue | bg-blaze-blue |
144
+ | Red | #C02B0A | --blaze-red | bg-blaze-red |
145
+ | Black | #1A1A1C | --blaze-black | bg-blaze-black |
146
+ | Dark Gray | #28282B | --blaze-dark-gray | bg-blaze-dark-gray |
147
+ | Gray | #313131 | --blaze-gray | bg-blaze-gray |
148
+ | Medium Gray | #575757 | --blaze-medium-gray | bg-blaze-medium-gray |
149
+ | White | #FFFFFF | --blaze-white | bg-blaze-white |
150
+
151
+ ───────────────────────────────────────────────────────────────────────────────
152
+ SEMANTIC THEME VARIABLES (use these for theme-aware components)
153
+ ───────────────────────────────────────────────────────────────────────────────
154
+ Backgrounds: --background, --background-secondary, --card
155
+ Text: --foreground, --foreground-secondary, --foreground-muted
156
+ Interactive: --primary, --primary-foreground, --border
157
+ Feedback: --success, --error, --warning, --info
158
+
159
+ Tailwind: bg-background, bg-card, text-foreground, text-foreground-secondary,
160
+ bg-primary, text-primary-foreground, border-border
161
+
162
+ ───────────────────────────────────────────────────────────────────────────────
163
+ TYPOGRAPHY (All Brands use Montserrat)
164
+ ───────────────────────────────────────────────────────────────────────────────
165
+ Font: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif
166
+
167
+ Font Weights:
168
+ - 300 Light: Headlines, display text
169
+ - 400 Regular: Body text, paragraphs
170
+ - 500 Medium: Emphasis, subheadings, buttons
171
+ - 600 Semibold: Strong emphasis
172
+ - 700 Bold: CTAs, buttons (SMALL TEXT ONLY - never for large headlines)
173
+
174
+ Headlines:
175
+ - Weight: Light (300) or Medium (500)
176
+ - Letter-spacing: -0.02em (tracking-tight)
177
+ - Line-height: 1.2
178
+ - NEVER use bold (700) for large display text
179
+
180
+ Body Text:
181
+ - Weight: Regular (400)
182
+ - Line-height: 1.6 to 1.75
183
+ - Max-width: 65-75 characters per line
184
+
185
+ All Caps Text:
186
+ - Letter-spacing: 0.08em to 0.1em (tracking-wide or tracking-widest)
187
+ - Use for: headlines, product names, labels, buttons
188
+
189
+ ───────────────────────────────────────────────────────────────────────────────
190
+ COMPONENT PATTERNS
191
+ ───────────────────────────────────────────────────────────────────────────────
192
+
193
+ PRIMARY BUTTONS:
194
+ - Sonance: bg-sonance-charcoal text-white hover:bg-sonance-gray-800
195
+ - IPORT: bg-iport-orange text-white hover:opacity-90
196
+ - Blaze: bg-blaze-blue text-white hover:opacity-90
197
+ - All: px-6 py-3 text-sm font-medium tracking-wide uppercase transition-colors
198
+
199
+ SECONDARY BUTTONS:
200
+ - border border-border bg-transparent hover:bg-secondary-hover
201
+ - text-foreground px-6 py-3 text-sm font-medium tracking-wide uppercase
202
+
203
+ CARDS:
204
+ - Light: bg-white border border-sonance-light-gray rounded-sm p-6 shadow-sm
205
+ - Dark: bg-iport-dark-gray border border-iport-gray rounded-sm p-6
206
+
207
+ SECTION HEADERS:
208
+ - Category: text-xs font-medium uppercase tracking-widest text-foreground-muted mb-2
209
+ - Headline: text-4xl font-light text-foreground tracking-tight
210
+ - Description: text-lg text-foreground-secondary mt-4 max-w-2xl
211
+
212
+ ───────────────────────────────────────────────────────────────────────────────
213
+ DESIGN PRINCIPLES
214
+ ───────────────────────────────────────────────────────────────────────────────
215
+ - Sharp corners: rounded-sm or rounded-none preferred
216
+ - Subtle shadows: shadow-sm, shadow-md (not aggressive drop shadows)
217
+ - Generous whitespace: layouts should feel "breathable"
218
+ - Minimal borders: use subtle dividers, not heavy borders
219
+ - Premium feel: every element should feel high-end and intentional
220
+ - Photo-realistic imagery only (no cartoons or illustrations)
221
+
222
+ ═══════════════════════════════════════════════════════════════════════════════
223
+
224
+ RESPOND WITH VALID JSON ONLY:
225
+ {
226
+ "modifiedCode": "// Complete file contents here...",
227
+ "explanation": "Brief description of what was changed"
228
+ }
229
+
230
+ Do not include markdown code fences or any text outside the JSON object.`;
231
+
232
+ export async function POST(request: Request) {
233
+ // Security: Only allow in development
234
+ if (process.env.NODE_ENV !== "development") {
235
+ return NextResponse.json(
236
+ { error: "This endpoint is only available in development mode." },
237
+ { status: 403 }
238
+ );
239
+ }
240
+
241
+ try {
242
+ const body: AIEditRequest = await request.json();
243
+ const { action, componentType, filePath, currentCode, modifiedCode, userRequest, editScope, variantId, variantStyles } = body;
244
+
245
+ const projectRoot = process.cwd();
246
+
247
+ // Handle save action
248
+ if (action === "save") {
249
+ if (!filePath || !modifiedCode) {
250
+ return NextResponse.json(
251
+ { error: "filePath and modifiedCode are required for save action" },
252
+ { status: 400 }
253
+ );
254
+ }
255
+
256
+ const fullPath = path.join(projectRoot, filePath);
257
+
258
+ // Validate file path is within project
259
+ if (!fullPath.startsWith(projectRoot)) {
260
+ return NextResponse.json(
261
+ { error: "Invalid file path" },
262
+ { status: 400 }
263
+ );
264
+ }
265
+
266
+ // Write the file
267
+ fs.writeFileSync(fullPath, modifiedCode, "utf-8");
268
+
269
+ return NextResponse.json({
270
+ success: true,
271
+ message: `Successfully updated ${filePath}`,
272
+ });
273
+ }
274
+
275
+ // Handle edit action (AI modification)
276
+ if (action === "edit") {
277
+ if (!filePath || !userRequest) {
278
+ return NextResponse.json(
279
+ { error: "filePath and userRequest are required for edit action" },
280
+ { status: 400 }
281
+ );
282
+ }
283
+
284
+ // Read current file if not provided
285
+ let sourceCode = currentCode;
286
+ if (!sourceCode) {
287
+ const fullPath = path.join(projectRoot, filePath);
288
+ if (!fs.existsSync(fullPath)) {
289
+ return NextResponse.json(
290
+ { error: `File not found: ${filePath}` },
291
+ { status: 404 }
292
+ );
293
+ }
294
+ sourceCode = fs.readFileSync(fullPath, "utf-8");
295
+ }
296
+
297
+ // Check for API key
298
+ const apiKey = process.env.NEXT_PUBLIC_CLAUDE_API_KEY;
299
+ if (!apiKey) {
300
+ return NextResponse.json(
301
+ { error: "NEXT_PUBLIC_CLAUDE_API_KEY not configured. Add it to your .env.local file." },
302
+ { status: 500 }
303
+ );
304
+ }
305
+
306
+ // Call Claude API
307
+ const anthropic = new Anthropic({ apiKey });
308
+
309
+ // Build context for variant-scoped editing
310
+ let variantContext = "";
311
+ if (editScope === "variant" && variantId && variantStyles) {
312
+ variantContext = `
313
+ VARIANT-SCOPED EDIT:
314
+ This change should ONLY affect components matching this specific variant (ID: ${variantId}).
315
+ The variant has these computed styles:
316
+ - Background: ${variantStyles.backgroundColor}
317
+ - Text color: ${variantStyles.color}
318
+ - Border: ${variantStyles.borderWidth} ${variantStyles.borderColor}
319
+ - Border radius: ${variantStyles.borderRadius}
320
+ - Padding: ${variantStyles.padding}
321
+ - Font size: ${variantStyles.fontSize}
322
+ - Font weight: ${variantStyles.fontWeight}
323
+ - Box shadow: ${variantStyles.boxShadow}
324
+
325
+ Create a conditional style or variant prop that targets ONLY these components.
326
+ Do NOT change the default appearance of other instances of this component.
327
+ `;
328
+ }
329
+
330
+ const userMessage = `Component type: ${componentType}
331
+ File: ${filePath}
332
+ Edit scope: ${editScope === "variant" ? "VARIANT ONLY" : "ALL INSTANCES"}
333
+ ${variantContext}
334
+ Current code:
335
+ \`\`\`tsx
336
+ ${sourceCode}
337
+ \`\`\`
338
+
339
+ User request: "${userRequest}"
340
+
341
+ IMPORTANT REMINDERS:
342
+ - Make ONLY the change requested above - nothing else
343
+ - Do NOT remove or modify any existing content, children, or JSX elements
344
+ - Do NOT change any styles that weren't specifically requested
345
+ - Preserve ALL existing className values - only add/modify what's needed for the request
346
+ - If the user says "round the borders", ONLY add a rounded-* class
347
+ - If the user says "change the color", ONLY change the relevant color class
348
+
349
+ Return the complete modified file as JSON with these keys:
350
+ - "modifiedCode": The complete modified file content
351
+ - "explanation": Brief description of changes made
352
+ - "previewCSS": CSS rules that will visually show the changes. Use selector [data-sonance-variant="${variantId || "selected"}"] to target the specific element(s). Example: "[data-sonance-variant='abc123'] { border-radius: 0.5rem; background-color: white; }"`;
353
+
354
+ const response = await anthropic.messages.create({
355
+ model: "claude-sonnet-4-20250514",
356
+ max_tokens: 8192,
357
+ messages: [
358
+ {
359
+ role: "user",
360
+ content: userMessage,
361
+ },
362
+ ],
363
+ system: SYSTEM_PROMPT,
364
+ });
365
+
366
+ // Extract text content from response
367
+ const textContent = response.content.find(block => block.type === "text");
368
+ if (!textContent || textContent.type !== "text") {
369
+ return NextResponse.json(
370
+ { error: "No text response from AI" },
371
+ { status: 500 }
372
+ );
373
+ }
374
+
375
+ // Parse AI response
376
+ let aiResponse: { modifiedCode: string; explanation: string; previewCSS?: string };
377
+ try {
378
+ // Clean up response - try to extract JSON from markdown code blocks
379
+ let jsonText = textContent.text.trim();
380
+
381
+ const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
382
+ jsonText.match(/```\n([\s\S]*?)\n```/);
383
+
384
+ if (jsonMatch) {
385
+ jsonText = jsonMatch[1];
386
+ } else if (jsonText.includes("```json")) {
387
+ // Fallback for cases where regex might miss due to newlines
388
+ const start = jsonText.indexOf("```json") + 7;
389
+ const end = jsonText.lastIndexOf("```");
390
+ if (end > start) {
391
+ jsonText = jsonText.substring(start, end);
392
+ }
393
+ }
394
+
395
+ // Clean up any remaining whitespace
396
+ jsonText = jsonText.trim();
397
+
398
+ aiResponse = JSON.parse(jsonText);
399
+ } catch {
400
+ console.error("Failed to parse AI response:", textContent.text);
401
+ return NextResponse.json(
402
+ { error: "Failed to parse AI response. Please try again." },
403
+ { status: 500 }
404
+ );
405
+ }
406
+
407
+ // Generate simple diff
408
+ const diff = generateSimpleDiff(sourceCode, aiResponse.modifiedCode);
409
+
410
+ return NextResponse.json({
411
+ success: true,
412
+ modifiedCode: aiResponse.modifiedCode,
413
+ diff,
414
+ explanation: aiResponse.explanation,
415
+ previewCSS: aiResponse.previewCSS || "",
416
+ } as AIEditResponse);
417
+ }
418
+
419
+ return NextResponse.json(
420
+ { error: "Invalid action. Use 'edit' or 'save'." },
421
+ { status: 400 }
422
+ );
423
+ } catch (error) {
424
+ console.error("AI edit error:", error);
425
+ return NextResponse.json(
426
+ { error: "Failed to process request", details: String(error) },
427
+ { status: 500 }
428
+ );
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Generate a simple unified diff between two strings
434
+ */
435
+ function generateSimpleDiff(original: string, modified: string): string {
436
+ const originalLines = original.split("\n");
437
+ const modifiedLines = modified.split("\n");
438
+
439
+ const diff: string[] = [];
440
+ const maxLines = Math.max(originalLines.length, modifiedLines.length);
441
+
442
+ let inChange = false;
443
+ let changeStart = -1;
444
+ const changes: { start: number; origLines: string[]; modLines: string[] }[] = [];
445
+
446
+ // Find changed regions
447
+ for (let i = 0; i < maxLines; i++) {
448
+ const origLine = originalLines[i] || "";
449
+ const modLine = modifiedLines[i] || "";
450
+
451
+ if (origLine !== modLine) {
452
+ if (!inChange) {
453
+ inChange = true;
454
+ changeStart = i;
455
+ changes.push({ start: i, origLines: [], modLines: [] });
456
+ }
457
+ if (originalLines[i] !== undefined) {
458
+ changes[changes.length - 1].origLines.push(origLine);
459
+ }
460
+ if (modifiedLines[i] !== undefined) {
461
+ changes[changes.length - 1].modLines.push(modLine);
462
+ }
463
+ } else {
464
+ inChange = false;
465
+ }
466
+ }
467
+
468
+ // Format diff output
469
+ for (const change of changes) {
470
+ diff.push(`@@ Line ${change.start + 1} @@`);
471
+ for (const line of change.origLines) {
472
+ diff.push(`- ${line}`);
473
+ }
474
+ for (const line of change.modLines) {
475
+ diff.push(`+ ${line}`);
476
+ }
477
+ diff.push("");
478
+ }
479
+
480
+ if (diff.length === 0) {
481
+ return "No changes detected";
482
+ }
483
+
484
+ return diff.join("\n");
485
+ }
@@ -0,0 +1,78 @@
1
+ import { NextResponse } from "next/server";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ /**
6
+ * Sonance DevTools API - Component Source Reader
7
+ *
8
+ * Returns the source code of a component file for AI editing.
9
+ *
10
+ * DEVELOPMENT ONLY.
11
+ */
12
+
13
+ export async function GET(request: Request) {
14
+ // Security: Only allow in development
15
+ if (process.env.NODE_ENV !== "development") {
16
+ return NextResponse.json(
17
+ { error: "This endpoint is only available in development mode." },
18
+ { status: 403 }
19
+ );
20
+ }
21
+
22
+ try {
23
+ const { searchParams } = new URL(request.url);
24
+ const filePath = searchParams.get("file");
25
+
26
+ if (!filePath) {
27
+ return NextResponse.json(
28
+ { error: "file parameter is required" },
29
+ { status: 400 }
30
+ );
31
+ }
32
+
33
+ const projectRoot = process.cwd();
34
+ const fullPath = path.join(projectRoot, filePath);
35
+
36
+ // Validate file path is within project (security)
37
+ const normalizedPath = path.normalize(fullPath);
38
+ if (!normalizedPath.startsWith(projectRoot)) {
39
+ return NextResponse.json(
40
+ { error: "Invalid file path - must be within project" },
41
+ { status: 400 }
42
+ );
43
+ }
44
+
45
+ // Check file exists
46
+ if (!fs.existsSync(fullPath)) {
47
+ return NextResponse.json(
48
+ { error: `File not found: ${filePath}` },
49
+ { status: 404 }
50
+ );
51
+ }
52
+
53
+ // Read file contents
54
+ const content = fs.readFileSync(fullPath, "utf-8");
55
+ const lines = content.split("\n");
56
+
57
+ // Extract component name from file (simple heuristic)
58
+ let componentName = path.basename(filePath, path.extname(filePath));
59
+ // Convert kebab-case to PascalCase
60
+ componentName = componentName
61
+ .split("-")
62
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
63
+ .join("");
64
+
65
+ return NextResponse.json({
66
+ filePath,
67
+ content,
68
+ componentName,
69
+ lineCount: lines.length,
70
+ });
71
+ } catch (error) {
72
+ console.error("Error reading component source:", error);
73
+ return NextResponse.json(
74
+ { error: "Failed to read file", details: String(error) },
75
+ { status: 500 }
76
+ );
77
+ }
78
+ }