sonance-brand-mcp 1.3.26 → 1.3.28

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.
@@ -50,10 +50,34 @@ interface ApplyFirstRequest {
50
50
  interface BackupManifest {
51
51
  sessionId: string;
52
52
  timestamp: number;
53
- files: { original: string; backup: string }[];
53
+ files: {
54
+ original: string;
55
+ backup: string;
56
+ isNewFile: boolean; // Track if file was newly created (needs deletion on revert)
57
+ }[];
54
58
  }
55
59
 
56
60
  const BACKUP_ROOT = ".sonance-backups";
61
+ const DEBUG_LOG_FILE = "sonance-debug.log";
62
+
63
+ /**
64
+ * Debug logging utility - writes to sonance-debug.log in project root
65
+ * This helps diagnose issues with file discovery, validation, and revert
66
+ */
67
+ function debugLog(message: string, data?: unknown) {
68
+ const timestamp = new Date().toISOString();
69
+ const dataStr = data !== undefined ? `\n${JSON.stringify(data, null, 2)}` : "";
70
+ const logEntry = `[${timestamp}] [apply] ${message}${dataStr}\n`;
71
+
72
+ try {
73
+ const logPath = path.join(process.cwd(), DEBUG_LOG_FILE);
74
+ fs.appendFileSync(logPath, logEntry);
75
+ // Also log to console for terminal visibility
76
+ console.log(`[Sonance] ${message}`, data !== undefined ? data : "");
77
+ } catch (e) {
78
+ console.error("[Sonance] Failed to write debug log:", e);
79
+ }
80
+ }
57
81
 
58
82
  const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
59
83
 
@@ -373,6 +397,12 @@ CRITICAL: Your modified file should have approximately the same number of lines
373
397
  validFilePaths.add(comp.path);
374
398
  }
375
399
 
400
+ debugLog("VALIDATION: Valid file paths from page context", {
401
+ pageFile: pageContext.pageFile,
402
+ validFilePaths: Array.from(validFilePaths),
403
+ aiRequestedFiles: aiResponse.modifications.map(m => m.filePath)
404
+ });
405
+
376
406
  // Process modifications - apply patches to get modified content
377
407
  const modifications: VisionFileModification[] = [];
378
408
  const patchErrors: string[] = [];
@@ -380,7 +410,15 @@ CRITICAL: Your modified file should have approximately the same number of lines
380
410
  for (const mod of aiResponse.modifications) {
381
411
  // Validate that the file path is in the page context
382
412
  // This prevents the AI from creating new files
383
- if (!validFilePaths.has(mod.filePath)) {
413
+ const isValidPath = validFilePaths.has(mod.filePath);
414
+ debugLog(`VALIDATION CHECK: ${mod.filePath}`, {
415
+ isValidPath,
416
+ fileExists: fs.existsSync(path.join(projectRoot, mod.filePath)),
417
+ validPaths: Array.from(validFilePaths)
418
+ });
419
+
420
+ if (!isValidPath) {
421
+ debugLog(`REJECTED: File not in page context`, { filePath: mod.filePath });
384
422
  console.warn(`[Apply-First] Rejected modification to unknown file: ${mod.filePath}`);
385
423
  console.warn(`[Apply-First] Valid files are: ${Array.from(validFilePaths).join(", ")}`);
386
424
  patchErrors.push(`${mod.filePath}: This file was not found in the page context. The AI can only modify existing files that are part of the current page.`);
@@ -512,6 +550,15 @@ async function applyChangesWithBackup(
512
550
  const backupDir = path.join(projectRoot, BACKUP_ROOT, sessionId);
513
551
  const backupPaths: string[] = [];
514
552
 
553
+ // Track which files exist before we modify them
554
+ const existingFiles = new Set<string>();
555
+ for (const mod of modifications) {
556
+ const fullPath = path.join(projectRoot, mod.filePath);
557
+ if (fs.existsSync(fullPath)) {
558
+ existingFiles.add(mod.filePath);
559
+ }
560
+ }
561
+
515
562
  try {
516
563
  // Step 1: Create backup directory
517
564
  fs.mkdirSync(backupDir, { recursive: true });
@@ -520,7 +567,7 @@ async function applyChangesWithBackup(
520
567
  for (const mod of modifications) {
521
568
  const fullPath = path.join(projectRoot, mod.filePath);
522
569
 
523
- if (fs.existsSync(fullPath)) {
570
+ if (existingFiles.has(mod.filePath)) {
524
571
  const backupPath = path.join(backupDir, mod.filePath);
525
572
  const backupDirForFile = path.dirname(backupPath);
526
573
 
@@ -530,13 +577,14 @@ async function applyChangesWithBackup(
530
577
  }
531
578
  }
532
579
 
533
- // Step 3: Write manifest
580
+ // Step 3: Write manifest (track which files are new)
534
581
  const manifest: BackupManifest = {
535
582
  sessionId,
536
583
  timestamp: Date.now(),
537
584
  files: modifications.map(m => ({
538
585
  original: m.filePath,
539
586
  backup: path.join(backupDir, m.filePath),
587
+ isNewFile: !existingFiles.has(m.filePath), // Track if file was newly created
540
588
  })),
541
589
  };
542
590
 
@@ -589,9 +637,11 @@ async function revertFromBackups(
589
637
  sessionId: string,
590
638
  projectRoot: string
591
639
  ): Promise<{ success: boolean; message: string; filesReverted: number }> {
640
+ debugLog("revertFromBackups called", { sessionId, projectRoot });
592
641
  const backupDir = path.join(projectRoot, BACKUP_ROOT, sessionId);
593
642
 
594
643
  if (!fs.existsSync(backupDir)) {
644
+ debugLog("REVERT ERROR: Backup directory not found", { backupDir });
595
645
  return {
596
646
  success: false,
597
647
  message: "Backup session not found",
@@ -603,7 +653,7 @@ async function revertFromBackups(
603
653
  const manifestPath = path.join(backupDir, "manifest.json");
604
654
 
605
655
  if (!fs.existsSync(manifestPath)) {
606
- // If no manifest, try to restore any backup files we find
656
+ debugLog("REVERT ERROR: Manifest not found", { manifestPath });
607
657
  return {
608
658
  success: false,
609
659
  message: "Backup manifest not found",
@@ -614,26 +664,66 @@ async function revertFromBackups(
614
664
  const manifest: BackupManifest = JSON.parse(
615
665
  fs.readFileSync(manifestPath, "utf-8")
616
666
  );
667
+
668
+ debugLog("REVERT: Loaded manifest", {
669
+ sessionId: manifest.sessionId,
670
+ fileCount: manifest.files.length,
671
+ files: manifest.files.map(f => ({ path: f.original, isNewFile: f.isNewFile }))
672
+ });
617
673
 
618
674
  let filesReverted = 0;
675
+ let filesDeleted = 0;
619
676
 
620
677
  for (const file of manifest.files) {
621
- const backupPath = path.join(backupDir, file.original);
622
678
  const originalPath = path.join(projectRoot, file.original);
679
+ debugLog(`REVERT: Processing file`, {
680
+ filePath: file.original,
681
+ isNewFile: file.isNewFile,
682
+ fileExists: fs.existsSync(originalPath)
683
+ });
623
684
 
624
- if (fs.existsSync(backupPath)) {
625
- fs.copyFileSync(backupPath, originalPath);
626
- filesReverted++;
685
+ // Handle new files - delete them
686
+ if (file.isNewFile) {
687
+ debugLog(`REVERT: Attempting to delete new file`, { originalPath });
688
+ if (fs.existsSync(originalPath)) {
689
+ fs.unlinkSync(originalPath);
690
+ filesDeleted++;
691
+ debugLog(`REVERT: Successfully deleted new file`, { originalPath });
692
+ console.log(`[Revert] Deleted new file: ${file.original}`);
693
+
694
+ // Clean up empty parent directories
695
+ try {
696
+ const parentDir = path.dirname(originalPath);
697
+ const entries = fs.readdirSync(parentDir);
698
+ if (entries.length === 0) {
699
+ fs.rmdirSync(parentDir);
700
+ console.log(`[Revert] Removed empty directory: ${path.dirname(file.original)}`);
701
+ }
702
+ } catch {
703
+ // Ignore errors when cleaning up directories
704
+ }
705
+ }
706
+ } else {
707
+ // Handle existing files - restore from backup
708
+ const backupPath = path.join(backupDir, file.original);
709
+ if (fs.existsSync(backupPath)) {
710
+ fs.copyFileSync(backupPath, originalPath);
711
+ filesReverted++;
712
+ }
627
713
  }
628
714
  }
629
715
 
630
716
  // Delete backup directory after successful revert
631
717
  fs.rmSync(backupDir, { recursive: true });
632
718
 
719
+ const message = filesDeleted > 0
720
+ ? `Reverted ${filesReverted} file(s), deleted ${filesDeleted} new file(s)`
721
+ : `Reverted ${filesReverted} file(s)`;
722
+
633
723
  return {
634
724
  success: true,
635
- message: `Reverted ${filesReverted} file(s)`,
636
- filesReverted,
725
+ message,
726
+ filesReverted: filesReverted + filesDeleted,
637
727
  };
638
728
  } catch (error) {
639
729
  console.error("Error reverting from backups:", error);
@@ -812,8 +902,11 @@ function gatherPageContext(
812
902
  }
813
903
 
814
904
  function discoverPageFile(route: string, projectRoot: string): string | null {
905
+ debugLog("discoverPageFile called", { route, projectRoot });
906
+
815
907
  // Handle root route
816
908
  if (route === "/" || route === "") {
909
+ debugLog("Handling root route, checking patterns...");
817
910
  const rootPatterns = [
818
911
  // App Router patterns
819
912
  "src/app/page.tsx",
@@ -860,18 +953,26 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
860
953
  ];
861
954
 
862
955
  for (const pattern of patterns) {
863
- if (fs.existsSync(path.join(projectRoot, pattern))) {
956
+ const fullPath = path.join(projectRoot, pattern);
957
+ const exists = fs.existsSync(fullPath);
958
+ debugLog(`Checking exact pattern: ${pattern}`, { exists });
959
+ if (exists) {
960
+ debugLog("Found exact match!", { pattern });
864
961
  return pattern;
865
962
  }
866
963
  }
867
964
 
965
+ debugLog("No exact match found, trying dynamic route matching...");
966
+
868
967
  // If exact match not found, try dynamic route matching
869
968
  // e.g., /processes/123 -> src/pages/processes/[id].tsx
870
969
  const dynamicResult = findDynamicRoute(cleanRoute, projectRoot);
871
970
  if (dynamicResult) {
971
+ debugLog("Found dynamic route match!", { dynamicResult });
872
972
  return dynamicResult;
873
973
  }
874
974
 
975
+ debugLog("discoverPageFile: NO FILE FOUND for route", { route });
875
976
  return null;
876
977
  }
877
978
 
@@ -881,6 +982,7 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
881
982
  */
882
983
  function findDynamicRoute(cleanRoute: string, projectRoot: string): string | null {
883
984
  const segments = cleanRoute.split("/");
985
+ debugLog("findDynamicRoute called", { cleanRoute, segments });
884
986
 
885
987
  // Try replacing the last segment with dynamic patterns
886
988
  const baseDirs = [
@@ -895,7 +997,10 @@ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | nul
895
997
 
896
998
  for (const baseDir of baseDirs) {
897
999
  const basePath = path.join(projectRoot, baseDir);
898
- if (!fs.existsSync(basePath)) continue;
1000
+ if (!fs.existsSync(basePath)) {
1001
+ debugLog(`Base dir does not exist: ${baseDir}`);
1002
+ continue;
1003
+ }
899
1004
 
900
1005
  // Build path with all segments except the last one
901
1006
  const parentSegments = segments.slice(0, -1);
@@ -903,6 +1008,8 @@ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | nul
903
1008
  ? path.join(basePath, ...parentSegments)
904
1009
  : basePath;
905
1010
 
1011
+ debugLog(`Checking parent path for dynamic routes`, { baseDir, parentPath, exists: fs.existsSync(parentPath) });
1012
+
906
1013
  if (!fs.existsSync(parentPath)) continue;
907
1014
 
908
1015
  // Check for dynamic route files
@@ -933,31 +1040,43 @@ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | nul
933
1040
  // Also scan directory for any dynamic segment pattern [...]
934
1041
  try {
935
1042
  const entries = fs.readdirSync(parentPath, { withFileTypes: true });
1043
+ const dynamicEntries = entries.filter(e => e.name.startsWith("[") && e.name.includes("]"));
1044
+ debugLog(`Scanning directory for dynamic segments`, { parentPath, dynamicEntries: dynamicEntries.map(e => e.name) });
1045
+
936
1046
  for (const entry of entries) {
937
1047
  if (entry.name.startsWith("[") && entry.name.includes("]")) {
1048
+ debugLog(`Found dynamic segment: ${entry.name}`, { isDirectory: entry.isDirectory() });
938
1049
  for (const ext of extensions) {
939
1050
  if (entry.isDirectory()) {
940
1051
  // App Router or Pages Router with folder
941
1052
  const pagePath = path.join(parentPath, entry.name, `page${ext}`);
942
1053
  const indexPath = path.join(parentPath, entry.name, `index${ext}`);
1054
+ debugLog(`Checking dynamic folder paths`, { pagePath, indexPath, pageExists: fs.existsSync(pagePath), indexExists: fs.existsSync(indexPath) });
943
1055
  if (fs.existsSync(pagePath)) {
944
- return path.relative(projectRoot, pagePath);
1056
+ const result = path.relative(projectRoot, pagePath);
1057
+ debugLog(`Found dynamic route (folder/page)!`, { result });
1058
+ return result;
945
1059
  }
946
1060
  if (fs.existsSync(indexPath)) {
947
- return path.relative(projectRoot, indexPath);
1061
+ const result = path.relative(projectRoot, indexPath);
1062
+ debugLog(`Found dynamic route (folder/index)!`, { result });
1063
+ return result;
948
1064
  }
949
1065
  } else if (entry.isFile() && entry.name.endsWith(ext)) {
950
1066
  // Pages Router file-based
951
- return path.relative(projectRoot, path.join(parentPath, entry.name));
1067
+ const result = path.relative(projectRoot, path.join(parentPath, entry.name));
1068
+ debugLog(`Found dynamic route (file)!`, { result });
1069
+ return result;
952
1070
  }
953
1071
  }
954
1072
  }
955
1073
  }
956
- } catch {
957
- // Skip if directory can't be read
1074
+ } catch (e) {
1075
+ debugLog(`Error scanning directory: ${parentPath}`, { error: String(e) });
958
1076
  }
959
1077
  }
960
1078
 
1079
+ debugLog("findDynamicRoute: NO DYNAMIC ROUTE FOUND");
961
1080
  return null;
962
1081
  }
963
1082
 
@@ -56,6 +56,27 @@ interface VisionEditResponse {
56
56
  error?: string;
57
57
  }
58
58
 
59
+ const DEBUG_LOG_FILE = "sonance-debug.log";
60
+
61
+ /**
62
+ * Debug logging utility - writes to sonance-debug.log in project root
63
+ * This helps diagnose issues with file discovery, validation, and revert
64
+ */
65
+ function debugLog(message: string, data?: unknown) {
66
+ const timestamp = new Date().toISOString();
67
+ const dataStr = data !== undefined ? `\n${JSON.stringify(data, null, 2)}` : "";
68
+ const logEntry = `[${timestamp}] [edit] ${message}${dataStr}\n`;
69
+
70
+ try {
71
+ const logPath = path.join(process.cwd(), DEBUG_LOG_FILE);
72
+ fs.appendFileSync(logPath, logEntry);
73
+ // Also log to console for terminal visibility
74
+ console.log(`[Sonance] ${message}`, data !== undefined ? data : "");
75
+ } catch (e) {
76
+ console.error("[Sonance] Failed to write debug log:", e);
77
+ }
78
+ }
79
+
59
80
  const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
60
81
 
61
82
  ═══════════════════════════════════════════════════════════════════════════════
@@ -381,12 +402,19 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
381
402
  validPaths.add(comp.path);
382
403
  }
383
404
 
405
+ debugLog("VALIDATION: Valid file paths from page context", {
406
+ pageFile: pageContext.pageFile,
407
+ validPaths: Array.from(validPaths),
408
+ aiRequestedFiles: (aiResponse.modifications || []).map(m => m.filePath)
409
+ });
410
+
384
411
  // Validate AI response - reject any file paths not in our valid list
385
412
  const invalidMods = (aiResponse.modifications || []).filter(
386
413
  (mod) => !validPaths.has(mod.filePath)
387
414
  );
388
415
 
389
416
  if (invalidMods.length > 0) {
417
+ debugLog("REJECTED: AI attempted to create new files", { invalidMods: invalidMods.map(m => m.filePath) });
390
418
  console.error(
391
419
  "AI attempted to create new files:",
392
420
  invalidMods.map((m) => m.filePath)
@@ -761,8 +789,11 @@ function gatherPageContext(
761
789
  * Supports both App Router (src/app/) and Pages Router (src/pages/, pages/)
762
790
  */
763
791
  function discoverPageFile(route: string, projectRoot: string): string | null {
792
+ debugLog("discoverPageFile called", { route, projectRoot });
793
+
764
794
  // Handle root route
765
795
  if (route === "/" || route === "") {
796
+ debugLog("Handling root route, checking patterns...");
766
797
  const rootPatterns = [
767
798
  // App Router patterns
768
799
  "src/app/page.tsx",
@@ -810,18 +841,26 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
810
841
  ];
811
842
 
812
843
  for (const pattern of patterns) {
813
- if (fs.existsSync(path.join(projectRoot, pattern))) {
844
+ const fullPath = path.join(projectRoot, pattern);
845
+ const exists = fs.existsSync(fullPath);
846
+ debugLog(`Checking exact pattern: ${pattern}`, { exists });
847
+ if (exists) {
848
+ debugLog("Found exact match!", { pattern });
814
849
  return pattern;
815
850
  }
816
851
  }
817
852
 
853
+ debugLog("No exact match found, trying dynamic route matching...");
854
+
818
855
  // If exact match not found, try dynamic route matching
819
856
  // e.g., /processes/123 -> src/pages/processes/[id].tsx
820
857
  const dynamicResult = findDynamicRoute(cleanRoute, projectRoot);
821
858
  if (dynamicResult) {
859
+ debugLog("Found dynamic route match!", { dynamicResult });
822
860
  return dynamicResult;
823
861
  }
824
862
 
863
+ debugLog("discoverPageFile: NO FILE FOUND for route", { route });
825
864
  return null;
826
865
  }
827
866
 
@@ -831,6 +870,7 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
831
870
  */
832
871
  function findDynamicRoute(cleanRoute: string, projectRoot: string): string | null {
833
872
  const segments = cleanRoute.split("/");
873
+ debugLog("findDynamicRoute called", { cleanRoute, segments });
834
874
 
835
875
  // Try replacing the last segment with dynamic patterns
836
876
  const baseDirs = [
@@ -845,7 +885,10 @@ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | nul
845
885
 
846
886
  for (const baseDir of baseDirs) {
847
887
  const basePath = path.join(projectRoot, baseDir);
848
- if (!fs.existsSync(basePath)) continue;
888
+ if (!fs.existsSync(basePath)) {
889
+ debugLog(`Base dir does not exist: ${baseDir}`);
890
+ continue;
891
+ }
849
892
 
850
893
  // Build path with all segments except the last one
851
894
  const parentSegments = segments.slice(0, -1);
@@ -853,6 +896,8 @@ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | nul
853
896
  ? path.join(basePath, ...parentSegments)
854
897
  : basePath;
855
898
 
899
+ debugLog(`Checking parent path for dynamic routes`, { baseDir, parentPath, exists: fs.existsSync(parentPath) });
900
+
856
901
  if (!fs.existsSync(parentPath)) continue;
857
902
 
858
903
  // Check for dynamic route files
@@ -883,31 +928,43 @@ function findDynamicRoute(cleanRoute: string, projectRoot: string): string | nul
883
928
  // Also scan directory for any dynamic segment pattern [...]
884
929
  try {
885
930
  const entries = fs.readdirSync(parentPath, { withFileTypes: true });
931
+ const dynamicEntries = entries.filter(e => e.name.startsWith("[") && e.name.includes("]"));
932
+ debugLog(`Scanning directory for dynamic segments`, { parentPath, dynamicEntries: dynamicEntries.map(e => e.name) });
933
+
886
934
  for (const entry of entries) {
887
935
  if (entry.name.startsWith("[") && entry.name.includes("]")) {
936
+ debugLog(`Found dynamic segment: ${entry.name}`, { isDirectory: entry.isDirectory() });
888
937
  for (const ext of extensions) {
889
938
  if (entry.isDirectory()) {
890
939
  // App Router or Pages Router with folder
891
940
  const pagePath = path.join(parentPath, entry.name, `page${ext}`);
892
941
  const indexPath = path.join(parentPath, entry.name, `index${ext}`);
942
+ debugLog(`Checking dynamic folder paths`, { pagePath, indexPath, pageExists: fs.existsSync(pagePath), indexExists: fs.existsSync(indexPath) });
893
943
  if (fs.existsSync(pagePath)) {
894
- return path.relative(projectRoot, pagePath);
944
+ const result = path.relative(projectRoot, pagePath);
945
+ debugLog(`Found dynamic route (folder/page)!`, { result });
946
+ return result;
895
947
  }
896
948
  if (fs.existsSync(indexPath)) {
897
- return path.relative(projectRoot, indexPath);
949
+ const result = path.relative(projectRoot, indexPath);
950
+ debugLog(`Found dynamic route (folder/index)!`, { result });
951
+ return result;
898
952
  }
899
953
  } else if (entry.isFile() && entry.name.endsWith(ext)) {
900
954
  // Pages Router file-based
901
- return path.relative(projectRoot, path.join(parentPath, entry.name));
955
+ const result = path.relative(projectRoot, path.join(parentPath, entry.name));
956
+ debugLog(`Found dynamic route (file)!`, { result });
957
+ return result;
902
958
  }
903
959
  }
904
960
  }
905
961
  }
906
- } catch {
907
- // Skip if directory can't be read
962
+ } catch (e) {
963
+ debugLog(`Error scanning directory: ${parentPath}`, { error: String(e) });
908
964
  }
909
965
  }
910
966
 
967
+ debugLog("findDynamicRoute: NO DYNAMIC ROUTE FOUND");
911
968
  return null;
912
969
  }
913
970
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.26",
3
+ "version": "1.3.28",
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",