sonance-brand-mcp 1.3.22 → 1.3.25
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.
|
@@ -55,50 +55,63 @@ interface BackupManifest {
|
|
|
55
55
|
|
|
56
56
|
const BACKUP_ROOT = ".sonance-backups";
|
|
57
57
|
|
|
58
|
-
const VISION_SYSTEM_PROMPT = `You are
|
|
58
|
+
const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
61
|
+
UNDERSTAND THE USER'S REQUEST NATURALLY
|
|
62
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
63
|
+
|
|
64
|
+
Read the user's request and do exactly what they ask. You know React, TypeScript, Tailwind CSS, and JSX - use that knowledge naturally.
|
|
65
|
+
|
|
66
|
+
- If they say "add X", ADD X to the code (don't replace or remove anything else)
|
|
67
|
+
- If they say "change X to Y", find X and change it to Y
|
|
68
|
+
- If they say "make X bigger/smaller/different color", adjust the relevant properties
|
|
69
|
+
- If they say "remove X", remove X
|
|
70
|
+
- If they say "wrap X with Y", add Y as a parent around X
|
|
71
|
+
|
|
72
|
+
For any change:
|
|
73
|
+
1. Read the code context provided
|
|
74
|
+
2. Understand what the user wants
|
|
75
|
+
3. Generate patches that accomplish exactly that
|
|
76
|
+
4. If adding a component/icon requires an import, include a patch for the import too
|
|
77
|
+
|
|
78
|
+
Don't overthink - just make the change the user requested.
|
|
61
79
|
|
|
62
80
|
═══════════════════════════════════════════════════════════════════════════════
|
|
63
|
-
|
|
81
|
+
PATCH FORMAT
|
|
64
82
|
═══════════════════════════════════════════════════════════════════════════════
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
5. NEVER delete or remove existing content, children, or JSX elements
|
|
74
|
-
6. NEVER change component structure unless specifically requested
|
|
75
|
-
7. NEVER modify TypeScript types, imports, or exports unless necessary
|
|
76
|
-
8. NEVER remove data-sonance-* attributes
|
|
77
|
-
|
|
78
|
-
**CHANGE RULES:**
|
|
79
|
-
9. Make ONLY the changes requested by the user
|
|
80
|
-
10. Modify the minimum amount of code necessary
|
|
81
|
-
11. Use semantic Tailwind classes (bg-primary, text-foreground, etc.)
|
|
82
|
-
12. Maintain dark mode compatibility with CSS variables
|
|
83
|
-
13. Keep the cn() utility for className merging
|
|
84
|
+
Return search/replace patches (NOT full files). The system applies your patches to the original.
|
|
85
|
+
|
|
86
|
+
**Patch Rules:**
|
|
87
|
+
- "search" must match the original code EXACTLY (including whitespace/indentation)
|
|
88
|
+
- "replace" contains your modified version
|
|
89
|
+
- Include 2-4 lines of context in "search" to make it unique
|
|
90
|
+
- You may ONLY edit files provided in the PAGE CONTEXT section
|
|
84
91
|
|
|
85
92
|
**SONANCE BRAND COLORS:**
|
|
86
|
-
- Charcoal: #333F48
|
|
87
|
-
- Silver: #E2E2E2, #D1D1D6 (secondary)
|
|
93
|
+
- Charcoal: #333F48 (primary text)
|
|
88
94
|
- IPORT Orange: #FC4C02
|
|
89
|
-
- IPORT Dark: #0F161D
|
|
90
95
|
- Blaze Blue: #00A3E1
|
|
91
|
-
- Blaze Red: #C02B0A
|
|
92
96
|
|
|
93
97
|
**RESPONSE FORMAT:**
|
|
94
|
-
Return ONLY
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
Return ONLY valid JSON:
|
|
99
|
+
{
|
|
100
|
+
"reasoning": "What you understood from the request and your plan",
|
|
101
|
+
"modifications": [
|
|
102
|
+
{
|
|
103
|
+
"filePath": "path/to/file.tsx",
|
|
104
|
+
"patches": [
|
|
105
|
+
{
|
|
106
|
+
"search": "exact code to find",
|
|
107
|
+
"replace": "the replacement code",
|
|
108
|
+
"explanation": "what this patch does"
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"explanation": "summary of changes made"
|
|
114
|
+
}`;
|
|
102
115
|
|
|
103
116
|
export async function POST(request: Request) {
|
|
104
117
|
// Only allow in development
|
|
@@ -234,12 +247,19 @@ GLOBALS.CSS (relevant theme variables):
|
|
|
234
247
|
${pageContext.globalsCSS.substring(0, 2000)}${pageContext.globalsCSS.length > 2000 ? "\n/* ... (truncated) */" : ""}
|
|
235
248
|
\`\`\`
|
|
236
249
|
|
|
250
|
+
VALID FILES YOU MAY EDIT:
|
|
251
|
+
${pageContext.pageFile ? `- ${pageContext.pageFile}` : ""}
|
|
252
|
+
${pageContext.componentSources.map((c) => `- ${c.path}`).join("\n")}
|
|
253
|
+
|
|
237
254
|
INSTRUCTIONS:
|
|
238
255
|
1. Look at the screenshot and identify elements mentioned in the user's request
|
|
239
256
|
2. Review the code to understand current implementation
|
|
240
|
-
3.
|
|
241
|
-
4.
|
|
242
|
-
5. Return
|
|
257
|
+
3. Make SURGICAL EDITS - change only the specific lines needed
|
|
258
|
+
4. PRESERVE all existing logic, hooks, API calls, and error handling
|
|
259
|
+
5. Return the FULL file content (no truncation, no "// ... existing ..." comments)
|
|
260
|
+
6. Only use file paths from the VALID FILES list above
|
|
261
|
+
|
|
262
|
+
CRITICAL: Your modified file should have approximately the same number of lines as the original. If the original has 200 lines and your output has 50 lines, you have made a mistake.`;
|
|
243
263
|
|
|
244
264
|
messageContent.push({
|
|
245
265
|
type: "text",
|
|
@@ -270,13 +290,15 @@ INSTRUCTIONS:
|
|
|
270
290
|
);
|
|
271
291
|
}
|
|
272
292
|
|
|
273
|
-
// Parse AI response
|
|
293
|
+
// Parse AI response - now expecting patches instead of full file content
|
|
274
294
|
let aiResponse: {
|
|
275
295
|
reasoning?: string;
|
|
276
296
|
modifications: Array<{
|
|
277
297
|
filePath: string;
|
|
278
|
-
|
|
279
|
-
|
|
298
|
+
patches?: Patch[];
|
|
299
|
+
// Legacy support for modifiedContent (will be deprecated)
|
|
300
|
+
modifiedContent?: string;
|
|
301
|
+
explanation?: string;
|
|
280
302
|
}>;
|
|
281
303
|
explanation?: string;
|
|
282
304
|
};
|
|
@@ -318,8 +340,10 @@ INSTRUCTIONS:
|
|
|
318
340
|
});
|
|
319
341
|
}
|
|
320
342
|
|
|
321
|
-
//
|
|
343
|
+
// Process modifications - apply patches to get modified content
|
|
322
344
|
const modifications: VisionFileModification[] = [];
|
|
345
|
+
const patchErrors: string[] = [];
|
|
346
|
+
|
|
323
347
|
for (const mod of aiResponse.modifications) {
|
|
324
348
|
const fullPath = path.join(projectRoot, mod.filePath);
|
|
325
349
|
let originalContent = "";
|
|
@@ -327,15 +351,77 @@ INSTRUCTIONS:
|
|
|
327
351
|
originalContent = fs.readFileSync(fullPath, "utf-8");
|
|
328
352
|
}
|
|
329
353
|
|
|
354
|
+
let modifiedContent: string;
|
|
355
|
+
let explanation = mod.explanation || "";
|
|
356
|
+
|
|
357
|
+
// Check if AI returned patches (new format) or modifiedContent (legacy)
|
|
358
|
+
if (mod.patches && mod.patches.length > 0) {
|
|
359
|
+
// New patch-based approach
|
|
360
|
+
console.log(`[Apply-First] Applying ${mod.patches.length} patches to ${mod.filePath}`);
|
|
361
|
+
|
|
362
|
+
const patchResult = applyPatches(originalContent, mod.patches);
|
|
363
|
+
|
|
364
|
+
if (!patchResult.success) {
|
|
365
|
+
const failedMessages = patchResult.failedPatches.map(
|
|
366
|
+
(f) => `Patch failed: ${f.error}`
|
|
367
|
+
).join("\n");
|
|
368
|
+
patchErrors.push(`${mod.filePath}:\n${failedMessages}`);
|
|
369
|
+
|
|
370
|
+
// If some patches succeeded, use partial result
|
|
371
|
+
if (patchResult.appliedPatches > 0) {
|
|
372
|
+
console.warn(`[Apply-First] ${patchResult.appliedPatches}/${mod.patches.length} patches applied to ${mod.filePath}`);
|
|
373
|
+
modifiedContent = patchResult.modifiedContent;
|
|
374
|
+
explanation += ` (${patchResult.appliedPatches}/${mod.patches.length} patches applied)`;
|
|
375
|
+
} else {
|
|
376
|
+
continue; // Skip this file entirely if no patches worked
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
modifiedContent = patchResult.modifiedContent;
|
|
380
|
+
console.log(`[Apply-First] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
|
|
381
|
+
}
|
|
382
|
+
} else if (mod.modifiedContent) {
|
|
383
|
+
// Legacy: AI returned full file content
|
|
384
|
+
console.warn(`[Apply-First] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
|
|
385
|
+
modifiedContent = mod.modifiedContent;
|
|
386
|
+
|
|
387
|
+
// Validate the modification using legacy validation
|
|
388
|
+
const validation = validateModification(originalContent, modifiedContent, mod.filePath);
|
|
389
|
+
if (!validation.valid) {
|
|
390
|
+
patchErrors.push(`${mod.filePath}: ${validation.error}`);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
// No patches and no modifiedContent - skip
|
|
395
|
+
console.warn(`[Apply-First] No patches or modifiedContent for ${mod.filePath}`);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
330
399
|
modifications.push({
|
|
331
400
|
filePath: mod.filePath,
|
|
332
401
|
originalContent,
|
|
333
|
-
modifiedContent
|
|
334
|
-
diff: generateSimpleDiff(originalContent,
|
|
335
|
-
explanation
|
|
402
|
+
modifiedContent,
|
|
403
|
+
diff: generateSimpleDiff(originalContent, modifiedContent),
|
|
404
|
+
explanation,
|
|
336
405
|
});
|
|
337
406
|
}
|
|
338
407
|
|
|
408
|
+
// If all modifications failed, return error
|
|
409
|
+
if (patchErrors.length > 0 && modifications.length === 0) {
|
|
410
|
+
console.error("All AI patches failed:", patchErrors);
|
|
411
|
+
return NextResponse.json(
|
|
412
|
+
{
|
|
413
|
+
success: false,
|
|
414
|
+
error: `Patch application failed:\n\n${patchErrors.join("\n\n")}`,
|
|
415
|
+
},
|
|
416
|
+
{ status: 400 }
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Log patch errors as warnings if some modifications succeeded
|
|
421
|
+
if (patchErrors.length > 0) {
|
|
422
|
+
console.warn("Some patches failed:", patchErrors);
|
|
423
|
+
}
|
|
424
|
+
|
|
339
425
|
// Create backups and apply changes atomically
|
|
340
426
|
const applyResult = await applyChangesWithBackup(
|
|
341
427
|
modifications,
|
|
@@ -650,3 +736,175 @@ function generateSimpleDiff(original: string, modified: string): string {
|
|
|
650
736
|
|
|
651
737
|
return diff.join("\n");
|
|
652
738
|
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Patch interface for search/replace operations
|
|
742
|
+
*/
|
|
743
|
+
interface Patch {
|
|
744
|
+
search: string;
|
|
745
|
+
replace: string;
|
|
746
|
+
explanation: string;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Result of applying patches to a file
|
|
751
|
+
*/
|
|
752
|
+
interface ApplyPatchesResult {
|
|
753
|
+
success: boolean;
|
|
754
|
+
modifiedContent: string;
|
|
755
|
+
appliedPatches: number;
|
|
756
|
+
failedPatches: { patch: Patch; error: string }[];
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Apply search/replace patches to file content
|
|
761
|
+
* This is the core of the patch-based editing system
|
|
762
|
+
*/
|
|
763
|
+
function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesResult {
|
|
764
|
+
let content = originalContent;
|
|
765
|
+
let appliedPatches = 0;
|
|
766
|
+
const failedPatches: { patch: Patch; error: string }[] = [];
|
|
767
|
+
|
|
768
|
+
for (const patch of patches) {
|
|
769
|
+
// Normalize line endings for matching
|
|
770
|
+
const normalizedSearch = patch.search.replace(/\\n/g, "\n");
|
|
771
|
+
const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
|
|
772
|
+
|
|
773
|
+
// Check if search string exists in content
|
|
774
|
+
if (!content.includes(normalizedSearch)) {
|
|
775
|
+
// Try with different whitespace normalization
|
|
776
|
+
const flexibleSearch = normalizedSearch.replace(/\s+/g, "\\s+");
|
|
777
|
+
const regex = new RegExp(flexibleSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\s\+/g, "\\s+"));
|
|
778
|
+
|
|
779
|
+
if (!regex.test(content)) {
|
|
780
|
+
failedPatches.push({
|
|
781
|
+
patch,
|
|
782
|
+
error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
|
|
783
|
+
});
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// If regex matched, use regex replace
|
|
788
|
+
content = content.replace(regex, normalizedReplace);
|
|
789
|
+
appliedPatches++;
|
|
790
|
+
} else {
|
|
791
|
+
// Exact match found - apply the replacement
|
|
792
|
+
// Only replace the first occurrence to be safe
|
|
793
|
+
const index = content.indexOf(normalizedSearch);
|
|
794
|
+
content =
|
|
795
|
+
content.substring(0, index) +
|
|
796
|
+
normalizedReplace +
|
|
797
|
+
content.substring(index + normalizedSearch.length);
|
|
798
|
+
appliedPatches++;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return {
|
|
803
|
+
success: failedPatches.length === 0,
|
|
804
|
+
modifiedContent: content,
|
|
805
|
+
appliedPatches,
|
|
806
|
+
failedPatches,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Validate that AI modifications are surgical edits, not complete rewrites
|
|
812
|
+
*/
|
|
813
|
+
interface ValidationResult {
|
|
814
|
+
valid: boolean;
|
|
815
|
+
error?: string;
|
|
816
|
+
warnings: string[];
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function validateModification(
|
|
820
|
+
originalContent: string,
|
|
821
|
+
modifiedContent: string,
|
|
822
|
+
filePath: string
|
|
823
|
+
): ValidationResult {
|
|
824
|
+
const warnings: string[] = [];
|
|
825
|
+
|
|
826
|
+
// Skip validation for new files (no original content)
|
|
827
|
+
if (!originalContent || originalContent.trim() === "") {
|
|
828
|
+
return { valid: true, warnings: ["New file - no original to compare"] };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const originalLines = originalContent.split("\n");
|
|
832
|
+
const modifiedLines = modifiedContent.split("\n");
|
|
833
|
+
|
|
834
|
+
// Check 1: Truncation detection - look for placeholder comments
|
|
835
|
+
const truncationPatterns = [
|
|
836
|
+
/\/\/\s*\.\.\.\s*existing/i,
|
|
837
|
+
/\/\/\s*\.\.\.\s*rest\s*of/i,
|
|
838
|
+
/\/\/\s*\.\.\.\s*more\s*code/i,
|
|
839
|
+
/\/\*\s*\.\.\.\s*\*\//,
|
|
840
|
+
/\/\/\s*\.\.\./,
|
|
841
|
+
];
|
|
842
|
+
|
|
843
|
+
for (const pattern of truncationPatterns) {
|
|
844
|
+
if (pattern.test(modifiedContent)) {
|
|
845
|
+
return {
|
|
846
|
+
valid: false,
|
|
847
|
+
error: `File ${filePath} contains truncation placeholder (e.g., "// ... existing code"). The AI must return the complete file content. Please try again.`,
|
|
848
|
+
warnings,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Check 2: Line count shrinkage - reject if file shrinks by more than 30%
|
|
854
|
+
const lineDelta = modifiedLines.length - originalLines.length;
|
|
855
|
+
const shrinkagePercent = (lineDelta / originalLines.length) * 100;
|
|
856
|
+
|
|
857
|
+
if (shrinkagePercent < -30) {
|
|
858
|
+
return {
|
|
859
|
+
valid: false,
|
|
860
|
+
error: `File ${filePath} shrank from ${originalLines.length} to ${modifiedLines.length} lines (${Math.abs(shrinkagePercent).toFixed(0)}% reduction). This suggests the AI rewrote the file instead of making surgical edits. Please try a more specific request.`,
|
|
861
|
+
warnings,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (shrinkagePercent < -15) {
|
|
866
|
+
warnings.push(`File shrank by ${Math.abs(shrinkagePercent).toFixed(0)}% - review carefully`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Check 3: Change percentage - warn if too many lines are different
|
|
870
|
+
let changedLines = 0;
|
|
871
|
+
const minLines = Math.min(originalLines.length, modifiedLines.length);
|
|
872
|
+
|
|
873
|
+
for (let i = 0; i < minLines; i++) {
|
|
874
|
+
if (originalLines[i] !== modifiedLines[i]) {
|
|
875
|
+
changedLines++;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Add lines that were added or removed
|
|
880
|
+
changedLines += Math.abs(originalLines.length - modifiedLines.length);
|
|
881
|
+
|
|
882
|
+
const changePercent = (changedLines / originalLines.length) * 100;
|
|
883
|
+
|
|
884
|
+
if (changePercent > 50) {
|
|
885
|
+
return {
|
|
886
|
+
valid: false,
|
|
887
|
+
error: `File ${filePath} has ${changePercent.toFixed(0)}% of lines changed. This suggests the AI rewrote the file instead of making surgical edits. For safety, this change has been rejected. Please try a more specific request.`,
|
|
888
|
+
warnings,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (changePercent > 30) {
|
|
893
|
+
warnings.push(`${changePercent.toFixed(0)}% of lines changed - larger than expected for a surgical edit`);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Check 4: Import preservation - ensure imports aren't removed
|
|
897
|
+
const importRegex = /^import\s+/gm;
|
|
898
|
+
const originalImports = (originalContent.match(importRegex) || []).length;
|
|
899
|
+
const modifiedImports = (modifiedContent.match(importRegex) || []).length;
|
|
900
|
+
|
|
901
|
+
if (modifiedImports < originalImports * 0.5 && originalImports > 2) {
|
|
902
|
+
return {
|
|
903
|
+
valid: false,
|
|
904
|
+
error: `File ${filePath} went from ${originalImports} imports to ${modifiedImports}. Imports should not be removed. Please try again.`,
|
|
905
|
+
warnings,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return { valid: true, warnings };
|
|
910
|
+
}
|
|
@@ -56,59 +56,65 @@ interface VisionEditResponse {
|
|
|
56
56
|
error?: string;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const VISION_SYSTEM_PROMPT = `You are
|
|
59
|
+
const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
62
|
+
UNDERSTAND THE USER'S REQUEST NATURALLY
|
|
63
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
64
|
+
|
|
65
|
+
Read the user's request and do exactly what they ask. You know React, TypeScript, Tailwind CSS, and JSX - use that knowledge naturally.
|
|
66
|
+
|
|
67
|
+
- If they say "add X", ADD X to the code (don't replace or remove anything else)
|
|
68
|
+
- If they say "change X to Y", find X and change it to Y
|
|
69
|
+
- If they say "make X bigger/smaller/different color", adjust the relevant properties
|
|
70
|
+
- If they say "remove X", remove X
|
|
71
|
+
- If they say "wrap X with Y", add Y as a parent around X
|
|
72
|
+
|
|
73
|
+
For any change:
|
|
74
|
+
1. Read the code context provided
|
|
75
|
+
2. Understand what the user wants
|
|
76
|
+
3. Generate patches that accomplish exactly that
|
|
77
|
+
4. If adding a component/icon requires an import, include a patch for the import too
|
|
78
|
+
|
|
79
|
+
Don't overthink - just make the change the user requested.
|
|
62
80
|
|
|
63
81
|
═══════════════════════════════════════════════════════════════════════════════
|
|
64
|
-
|
|
82
|
+
PATCH FORMAT
|
|
65
83
|
═══════════════════════════════════════════════════════════════════════════════
|
|
66
84
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
8. Consider how changes will affect the overall design
|
|
78
|
-
|
|
79
|
-
**PRESERVATION RULES:**
|
|
80
|
-
9. NEVER delete or remove existing content, children, or JSX elements
|
|
81
|
-
10. NEVER change component structure unless specifically requested
|
|
82
|
-
11. NEVER modify TypeScript types, imports, or exports unless necessary
|
|
83
|
-
12. NEVER remove data-sonance-* attributes
|
|
84
|
-
|
|
85
|
-
**CHANGE RULES:**
|
|
86
|
-
13. Make ONLY the changes requested by the user
|
|
87
|
-
14. Modify the minimum amount of code necessary
|
|
88
|
-
15. Use semantic Tailwind classes (bg-primary, text-foreground, etc.)
|
|
89
|
-
16. Maintain dark mode compatibility with CSS variables
|
|
90
|
-
17. Keep the cn() utility for className merging
|
|
91
|
-
18. CRITICAL: You MUST return the FULL file content in "modifiedContent". Do NOT use comments like "// ... existing code ..." or "// ... rest of file ...". Return every single line of code, even if unchanged.
|
|
92
|
-
|
|
93
|
-
**SONANCE BRAND COLORS:**
|
|
94
|
-
- Charcoal: #333F48, #343D46 (primary)
|
|
95
|
-
- Silver: #E2E2E2, #D1D1D6 (secondary)
|
|
85
|
+
Return search/replace patches (NOT full files). The system applies your patches to the original.
|
|
86
|
+
|
|
87
|
+
**Patch Rules:**
|
|
88
|
+
- "search" must match the original code EXACTLY (including whitespace/indentation)
|
|
89
|
+
- "replace" contains your modified version
|
|
90
|
+
- Include 2-4 lines of context in "search" to make it unique
|
|
91
|
+
- You may ONLY edit files provided in the PAGE CONTEXT section
|
|
92
|
+
|
|
93
|
+
**SONANCE BRAND COLORS:**
|
|
94
|
+
- Charcoal: #333F48 (primary text)
|
|
96
95
|
- IPORT Orange: #FC4C02
|
|
97
|
-
- IPORT Dark: #0F161D
|
|
98
96
|
- Blaze Blue: #00A3E1
|
|
99
|
-
- Blaze Red: #C02B0A
|
|
100
97
|
|
|
101
98
|
**RESPONSE FORMAT:**
|
|
102
|
-
Return ONLY
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
99
|
+
Return ONLY valid JSON:
|
|
100
|
+
{
|
|
101
|
+
"reasoning": "What you understood from the request and your plan",
|
|
102
|
+
"modifications": [
|
|
103
|
+
{
|
|
104
|
+
"filePath": "path/to/file.tsx",
|
|
105
|
+
"patches": [
|
|
106
|
+
{
|
|
107
|
+
"search": "exact code to find",
|
|
108
|
+
"replace": "the replacement code",
|
|
109
|
+
"explanation": "what this patch does"
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"previewCSS": "optional CSS for live preview"
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"aggregatedPreviewCSS": "combined CSS for all changes",
|
|
116
|
+
"explanation": "summary of changes made"
|
|
117
|
+
}`;
|
|
112
118
|
|
|
113
119
|
export async function POST(request: Request) {
|
|
114
120
|
// Only allow in development
|
|
@@ -297,13 +303,15 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
|
|
|
297
303
|
);
|
|
298
304
|
}
|
|
299
305
|
|
|
300
|
-
// Parse AI response
|
|
306
|
+
// Parse AI response - now expecting patches instead of full file content
|
|
301
307
|
let aiResponse: {
|
|
302
308
|
reasoning?: string;
|
|
303
309
|
modifications: Array<{
|
|
304
310
|
filePath: string;
|
|
305
|
-
|
|
306
|
-
|
|
311
|
+
patches?: Patch[];
|
|
312
|
+
// Legacy support for modifiedContent (will be deprecated)
|
|
313
|
+
modifiedContent?: string;
|
|
314
|
+
explanation?: string;
|
|
307
315
|
previewCSS?: string;
|
|
308
316
|
}>;
|
|
309
317
|
aggregatedPreviewCSS?: string;
|
|
@@ -368,8 +376,10 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
|
|
|
368
376
|
);
|
|
369
377
|
}
|
|
370
378
|
|
|
371
|
-
//
|
|
379
|
+
// Process modifications - apply patches to get modified content
|
|
372
380
|
const modificationsWithOriginals: VisionFileModification[] = [];
|
|
381
|
+
const patchErrors: string[] = [];
|
|
382
|
+
|
|
373
383
|
for (const mod of aiResponse.modifications || []) {
|
|
374
384
|
const fullPath = path.join(projectRoot, mod.filePath);
|
|
375
385
|
let originalContent = "";
|
|
@@ -377,16 +387,78 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
|
|
|
377
387
|
originalContent = fs.readFileSync(fullPath, "utf-8");
|
|
378
388
|
}
|
|
379
389
|
|
|
390
|
+
let modifiedContent: string;
|
|
391
|
+
let explanation = mod.explanation || "";
|
|
392
|
+
|
|
393
|
+
// Check if AI returned patches (new format) or modifiedContent (legacy)
|
|
394
|
+
if (mod.patches && mod.patches.length > 0) {
|
|
395
|
+
// New patch-based approach
|
|
396
|
+
console.log(`[Vision Mode] Applying ${mod.patches.length} patches to ${mod.filePath}`);
|
|
397
|
+
|
|
398
|
+
const patchResult = applyPatches(originalContent, mod.patches);
|
|
399
|
+
|
|
400
|
+
if (!patchResult.success) {
|
|
401
|
+
const failedMessages = patchResult.failedPatches.map(
|
|
402
|
+
(f) => `Patch failed: ${f.error}`
|
|
403
|
+
).join("\n");
|
|
404
|
+
patchErrors.push(`${mod.filePath}:\n${failedMessages}`);
|
|
405
|
+
|
|
406
|
+
// If some patches succeeded, use partial result
|
|
407
|
+
if (patchResult.appliedPatches > 0) {
|
|
408
|
+
console.warn(`[Vision Mode] ${patchResult.appliedPatches}/${mod.patches.length} patches applied to ${mod.filePath}`);
|
|
409
|
+
modifiedContent = patchResult.modifiedContent;
|
|
410
|
+
explanation += ` (${patchResult.appliedPatches}/${mod.patches.length} patches applied)`;
|
|
411
|
+
} else {
|
|
412
|
+
continue; // Skip this file entirely if no patches worked
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
modifiedContent = patchResult.modifiedContent;
|
|
416
|
+
console.log(`[Vision Mode] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
|
|
417
|
+
}
|
|
418
|
+
} else if (mod.modifiedContent) {
|
|
419
|
+
// Legacy: AI returned full file content
|
|
420
|
+
console.warn(`[Vision Mode] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
|
|
421
|
+
modifiedContent = mod.modifiedContent;
|
|
422
|
+
|
|
423
|
+
// Validate the modification using legacy validation
|
|
424
|
+
const validation = validateModification(originalContent, modifiedContent, mod.filePath);
|
|
425
|
+
if (!validation.valid) {
|
|
426
|
+
patchErrors.push(`${mod.filePath}: ${validation.error}`);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
// No patches and no modifiedContent - skip
|
|
431
|
+
console.warn(`[Vision Mode] No patches or modifiedContent for ${mod.filePath}`);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
380
435
|
modificationsWithOriginals.push({
|
|
381
436
|
filePath: mod.filePath,
|
|
382
437
|
originalContent,
|
|
383
|
-
modifiedContent
|
|
384
|
-
diff: generateSimpleDiff(originalContent,
|
|
385
|
-
explanation
|
|
438
|
+
modifiedContent,
|
|
439
|
+
diff: generateSimpleDiff(originalContent, modifiedContent),
|
|
440
|
+
explanation,
|
|
386
441
|
previewCSS: mod.previewCSS,
|
|
387
442
|
});
|
|
388
443
|
}
|
|
389
444
|
|
|
445
|
+
// If all modifications failed, return error
|
|
446
|
+
if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
|
|
447
|
+
console.error("All AI patches failed:", patchErrors);
|
|
448
|
+
return NextResponse.json(
|
|
449
|
+
{
|
|
450
|
+
success: false,
|
|
451
|
+
error: `Patch application failed:\n\n${patchErrors.join("\n\n")}`,
|
|
452
|
+
} as VisionEditResponse,
|
|
453
|
+
{ status: 400 }
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Log patch errors as warnings if some modifications succeeded
|
|
458
|
+
if (patchErrors.length > 0) {
|
|
459
|
+
console.warn("Some patches failed:", patchErrors);
|
|
460
|
+
}
|
|
461
|
+
|
|
390
462
|
// Aggregate preview CSS
|
|
391
463
|
const aggregatedCSS = modificationsWithOriginals
|
|
392
464
|
.filter((m) => m.previewCSS)
|
|
@@ -655,3 +727,175 @@ function generateSimpleDiff(original: string, modified: string): string {
|
|
|
655
727
|
|
|
656
728
|
return diff.join("\n");
|
|
657
729
|
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Patch interface for search/replace operations
|
|
733
|
+
*/
|
|
734
|
+
interface Patch {
|
|
735
|
+
search: string;
|
|
736
|
+
replace: string;
|
|
737
|
+
explanation: string;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Result of applying patches to a file
|
|
742
|
+
*/
|
|
743
|
+
interface ApplyPatchesResult {
|
|
744
|
+
success: boolean;
|
|
745
|
+
modifiedContent: string;
|
|
746
|
+
appliedPatches: number;
|
|
747
|
+
failedPatches: { patch: Patch; error: string }[];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Apply search/replace patches to file content
|
|
752
|
+
* This is the core of the patch-based editing system
|
|
753
|
+
*/
|
|
754
|
+
function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesResult {
|
|
755
|
+
let content = originalContent;
|
|
756
|
+
let appliedPatches = 0;
|
|
757
|
+
const failedPatches: { patch: Patch; error: string }[] = [];
|
|
758
|
+
|
|
759
|
+
for (const patch of patches) {
|
|
760
|
+
// Normalize line endings for matching
|
|
761
|
+
const normalizedSearch = patch.search.replace(/\\n/g, "\n");
|
|
762
|
+
const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
|
|
763
|
+
|
|
764
|
+
// Check if search string exists in content
|
|
765
|
+
if (!content.includes(normalizedSearch)) {
|
|
766
|
+
// Try with different whitespace normalization
|
|
767
|
+
const flexibleSearch = normalizedSearch.replace(/\s+/g, "\\s+");
|
|
768
|
+
const regex = new RegExp(flexibleSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\s\+/g, "\\s+"));
|
|
769
|
+
|
|
770
|
+
if (!regex.test(content)) {
|
|
771
|
+
failedPatches.push({
|
|
772
|
+
patch,
|
|
773
|
+
error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
|
|
774
|
+
});
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// If regex matched, use regex replace
|
|
779
|
+
content = content.replace(regex, normalizedReplace);
|
|
780
|
+
appliedPatches++;
|
|
781
|
+
} else {
|
|
782
|
+
// Exact match found - apply the replacement
|
|
783
|
+
// Only replace the first occurrence to be safe
|
|
784
|
+
const index = content.indexOf(normalizedSearch);
|
|
785
|
+
content =
|
|
786
|
+
content.substring(0, index) +
|
|
787
|
+
normalizedReplace +
|
|
788
|
+
content.substring(index + normalizedSearch.length);
|
|
789
|
+
appliedPatches++;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return {
|
|
794
|
+
success: failedPatches.length === 0,
|
|
795
|
+
modifiedContent: content,
|
|
796
|
+
appliedPatches,
|
|
797
|
+
failedPatches,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Validate that AI modifications are surgical edits, not complete rewrites
|
|
803
|
+
*/
|
|
804
|
+
interface ValidationResult {
|
|
805
|
+
valid: boolean;
|
|
806
|
+
error?: string;
|
|
807
|
+
warnings: string[];
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function validateModification(
|
|
811
|
+
originalContent: string,
|
|
812
|
+
modifiedContent: string,
|
|
813
|
+
filePath: string
|
|
814
|
+
): ValidationResult {
|
|
815
|
+
const warnings: string[] = [];
|
|
816
|
+
|
|
817
|
+
// Skip validation for new files (no original content)
|
|
818
|
+
if (!originalContent || originalContent.trim() === "") {
|
|
819
|
+
return { valid: true, warnings: ["New file - no original to compare"] };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const originalLines = originalContent.split("\n");
|
|
823
|
+
const modifiedLines = modifiedContent.split("\n");
|
|
824
|
+
|
|
825
|
+
// Check 1: Truncation detection - look for placeholder comments
|
|
826
|
+
const truncationPatterns = [
|
|
827
|
+
/\/\/\s*\.\.\.\s*existing/i,
|
|
828
|
+
/\/\/\s*\.\.\.\s*rest\s*of/i,
|
|
829
|
+
/\/\/\s*\.\.\.\s*more\s*code/i,
|
|
830
|
+
/\/\*\s*\.\.\.\s*\*\//,
|
|
831
|
+
/\/\/\s*\.\.\./,
|
|
832
|
+
];
|
|
833
|
+
|
|
834
|
+
for (const pattern of truncationPatterns) {
|
|
835
|
+
if (pattern.test(modifiedContent)) {
|
|
836
|
+
return {
|
|
837
|
+
valid: false,
|
|
838
|
+
error: `File ${filePath} contains truncation placeholder (e.g., "// ... existing code"). The AI must return the complete file content. Please try again.`,
|
|
839
|
+
warnings,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Check 2: Line count shrinkage - reject if file shrinks by more than 30%
|
|
845
|
+
const lineDelta = modifiedLines.length - originalLines.length;
|
|
846
|
+
const shrinkagePercent = (lineDelta / originalLines.length) * 100;
|
|
847
|
+
|
|
848
|
+
if (shrinkagePercent < -30) {
|
|
849
|
+
return {
|
|
850
|
+
valid: false,
|
|
851
|
+
error: `File ${filePath} shrank from ${originalLines.length} to ${modifiedLines.length} lines (${Math.abs(shrinkagePercent).toFixed(0)}% reduction). This suggests the AI rewrote the file instead of making surgical edits. Please try a more specific request.`,
|
|
852
|
+
warnings,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (shrinkagePercent < -15) {
|
|
857
|
+
warnings.push(`File shrank by ${Math.abs(shrinkagePercent).toFixed(0)}% - review carefully`);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Check 3: Change percentage - warn if too many lines are different
|
|
861
|
+
let changedLines = 0;
|
|
862
|
+
const minLines = Math.min(originalLines.length, modifiedLines.length);
|
|
863
|
+
|
|
864
|
+
for (let i = 0; i < minLines; i++) {
|
|
865
|
+
if (originalLines[i] !== modifiedLines[i]) {
|
|
866
|
+
changedLines++;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Add lines that were added or removed
|
|
871
|
+
changedLines += Math.abs(originalLines.length - modifiedLines.length);
|
|
872
|
+
|
|
873
|
+
const changePercent = (changedLines / originalLines.length) * 100;
|
|
874
|
+
|
|
875
|
+
if (changePercent > 50) {
|
|
876
|
+
return {
|
|
877
|
+
valid: false,
|
|
878
|
+
error: `File ${filePath} has ${changePercent.toFixed(0)}% of lines changed. This suggests the AI rewrote the file instead of making surgical edits. For safety, this change has been rejected. Please try a more specific request.`,
|
|
879
|
+
warnings,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (changePercent > 30) {
|
|
884
|
+
warnings.push(`${changePercent.toFixed(0)}% of lines changed - larger than expected for a surgical edit`);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Check 4: Import preservation - ensure imports aren't removed
|
|
888
|
+
const importRegex = /^import\s+/gm;
|
|
889
|
+
const originalImports = (originalContent.match(importRegex) || []).length;
|
|
890
|
+
const modifiedImports = (modifiedContent.match(importRegex) || []).length;
|
|
891
|
+
|
|
892
|
+
if (modifiedImports < originalImports * 0.5 && originalImports > 2) {
|
|
893
|
+
return {
|
|
894
|
+
valid: false,
|
|
895
|
+
error: `File ${filePath} went from ${originalImports} imports to ${modifiedImports}. Imports should not be removed. Please try again.`,
|
|
896
|
+
warnings,
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return { valid: true, warnings };
|
|
901
|
+
}
|
|
@@ -57,6 +57,7 @@ export function ChatInterface({
|
|
|
57
57
|
const [input, setInput] = useState("");
|
|
58
58
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
59
59
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
60
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
60
61
|
|
|
61
62
|
// Scroll to bottom when messages change
|
|
62
63
|
useEffect(() => {
|
|
@@ -126,6 +127,7 @@ export function ChatInterface({
|
|
|
126
127
|
|
|
127
128
|
setMessages((prev) => [...prev, userMessage]);
|
|
128
129
|
setInput("");
|
|
130
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
129
131
|
setIsProcessing(true);
|
|
130
132
|
|
|
131
133
|
try {
|
|
@@ -216,11 +218,14 @@ export function ChatInterface({
|
|
|
216
218
|
};
|
|
217
219
|
|
|
218
220
|
const handleSend = async (prompt: string) => {
|
|
219
|
-
if (
|
|
221
|
+
// Fallback: read from DOM if React state is empty (browser automation compatibility)
|
|
222
|
+
const actualPrompt = prompt || inputRef.current?.value || "";
|
|
223
|
+
|
|
224
|
+
if (!actualPrompt.trim() || isProcessing) return;
|
|
220
225
|
|
|
221
226
|
// Use vision mode handler if vision mode is active
|
|
222
227
|
if (visionMode) {
|
|
223
|
-
return handleVisionEdit(
|
|
228
|
+
return handleVisionEdit(actualPrompt);
|
|
224
229
|
}
|
|
225
230
|
|
|
226
231
|
// If no component is selected, intercept the request
|
|
@@ -228,11 +233,12 @@ export function ChatInterface({
|
|
|
228
233
|
const userMessage: ChatMessage = {
|
|
229
234
|
id: `msg-${Date.now()}`,
|
|
230
235
|
role: "user",
|
|
231
|
-
content:
|
|
236
|
+
content: actualPrompt,
|
|
232
237
|
timestamp: new Date(),
|
|
233
238
|
};
|
|
234
239
|
setMessages((prev) => [...prev, userMessage]);
|
|
235
240
|
setInput("");
|
|
241
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
236
242
|
|
|
237
243
|
setTimeout(() => {
|
|
238
244
|
const assistantMessage: ChatMessage = {
|
|
@@ -249,12 +255,13 @@ export function ChatInterface({
|
|
|
249
255
|
const userMessage: ChatMessage = {
|
|
250
256
|
id: `msg-${Date.now()}`,
|
|
251
257
|
role: "user",
|
|
252
|
-
content:
|
|
258
|
+
content: actualPrompt,
|
|
253
259
|
timestamp: new Date(),
|
|
254
260
|
};
|
|
255
261
|
|
|
256
262
|
setMessages((prev) => [...prev, userMessage]);
|
|
257
263
|
setInput("");
|
|
264
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
258
265
|
setIsProcessing(true);
|
|
259
266
|
|
|
260
267
|
try {
|
|
@@ -285,7 +292,7 @@ export function ChatInterface({
|
|
|
285
292
|
componentType,
|
|
286
293
|
filePath,
|
|
287
294
|
currentCode: sourceData.content,
|
|
288
|
-
userRequest:
|
|
295
|
+
userRequest: actualPrompt,
|
|
289
296
|
// Variant-scoped editing context
|
|
290
297
|
editScope,
|
|
291
298
|
variantId: editScope === "variant" ? variantId : undefined,
|
|
@@ -390,13 +397,14 @@ export function ChatInterface({
|
|
|
390
397
|
{/* Input */}
|
|
391
398
|
<div className="flex gap-2">
|
|
392
399
|
<input
|
|
400
|
+
ref={inputRef}
|
|
393
401
|
type="text"
|
|
394
402
|
value={input}
|
|
395
403
|
onChange={(e) => setInput(e.target.value)}
|
|
396
404
|
onKeyDown={(e) => {
|
|
397
405
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
398
406
|
e.preventDefault();
|
|
399
|
-
handleSend(input);
|
|
407
|
+
handleSend(input || inputRef.current?.value || "");
|
|
400
408
|
}
|
|
401
409
|
}}
|
|
402
410
|
placeholder={
|
|
@@ -418,8 +426,8 @@ export function ChatInterface({
|
|
|
418
426
|
)}
|
|
419
427
|
/>
|
|
420
428
|
<button
|
|
421
|
-
onClick={() => handleSend(input)}
|
|
422
|
-
disabled={isProcessing
|
|
429
|
+
onClick={() => handleSend(input || inputRef.current?.value || "")}
|
|
430
|
+
disabled={isProcessing}
|
|
423
431
|
className={cn(
|
|
424
432
|
"px-3 py-2 rounded transition-colors",
|
|
425
433
|
visionMode
|
|
@@ -582,6 +582,9 @@ export function ComponentsPanel({
|
|
|
582
582
|
)}
|
|
583
583
|
|
|
584
584
|
{/* AI Chat Interface - hide when any pending edit is present */}
|
|
585
|
+
{/* Apply-First mode: Files written immediately with backups for instant HMR preview
|
|
586
|
+
User sees structural + CSS changes live, then clicks Accept or Revert
|
|
587
|
+
Original files are always backed up and can be restored */}
|
|
585
588
|
{!pendingEdit && !visionPendingEdit && !applyFirstSession && (
|
|
586
589
|
<ChatInterface
|
|
587
590
|
componentType={selectedComponentType}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.25",
|
|
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",
|