@gemini-designer/mcp-server 0.1.2 → 0.1.29

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.
Files changed (129) hide show
  1. package/dist/components/catalog.d.ts.map +1 -1
  2. package/dist/components/catalog.js +10 -4
  3. package/dist/components/catalog.js.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +11 -6
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/context/builder.d.ts.map +1 -1
  8. package/dist/context/builder.js.map +1 -1
  9. package/dist/context/filter.d.ts.map +1 -1
  10. package/dist/context/filter.js +5 -1
  11. package/dist/context/filter.js.map +1 -1
  12. package/dist/context/grounding.d.ts.map +1 -1
  13. package/dist/context/grounding.js +7 -3
  14. package/dist/context/grounding.js.map +1 -1
  15. package/dist/context/guards.d.ts.map +1 -1
  16. package/dist/context/guards.js +53 -0
  17. package/dist/context/guards.js.map +1 -1
  18. package/dist/context/repo-hints.js.map +1 -1
  19. package/dist/context/styling-detector.d.ts +24 -0
  20. package/dist/context/styling-detector.d.ts.map +1 -0
  21. package/dist/context/styling-detector.js +337 -0
  22. package/dist/context/styling-detector.js.map +1 -0
  23. package/dist/design/principles.js.map +1 -1
  24. package/dist/generation/gemini-client.d.ts.map +1 -1
  25. package/dist/generation/gemini-client.js.map +1 -1
  26. package/dist/generation/litellm-client.d.ts.map +1 -1
  27. package/dist/generation/litellm-client.js +14 -7
  28. package/dist/generation/litellm-client.js.map +1 -1
  29. package/dist/generation/remote-client.d.ts +10 -5
  30. package/dist/generation/remote-client.d.ts.map +1 -1
  31. package/dist/generation/remote-client.js +13 -2
  32. package/dist/generation/remote-client.js.map +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/output/file-writer.d.ts.map +1 -1
  35. package/dist/output/file-writer.js +4 -4
  36. package/dist/output/file-writer.js.map +1 -1
  37. package/dist/output/formatter.d.ts.map +1 -1
  38. package/dist/output/formatter.js +5 -2
  39. package/dist/output/formatter.js.map +1 -1
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +2 -1
  42. package/dist/server.js.map +1 -1
  43. package/dist/stack/detect.d.ts.map +1 -1
  44. package/dist/stack/detect.js +42 -9
  45. package/dist/stack/detect.js.map +1 -1
  46. package/dist/tokens/sync.d.ts.map +1 -1
  47. package/dist/tokens/sync.js +22 -5
  48. package/dist/tokens/sync.js.map +1 -1
  49. package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -1
  50. package/dist/tools/analyze-screenshot-ui.js +5 -5
  51. package/dist/tools/analyze-screenshot-ui.js.map +1 -1
  52. package/dist/tools/analyze-tokens.d.ts.map +1 -1
  53. package/dist/tools/analyze-tokens.js +3 -1
  54. package/dist/tools/analyze-tokens.js.map +1 -1
  55. package/dist/tools/catalog-components.d.ts.map +1 -1
  56. package/dist/tools/catalog-components.js +1 -4
  57. package/dist/tools/catalog-components.js.map +1 -1
  58. package/dist/tools/create-ui.d.ts +3 -0
  59. package/dist/tools/create-ui.d.ts.map +1 -1
  60. package/dist/tools/create-ui.js +203 -75
  61. package/dist/tools/create-ui.js.map +1 -1
  62. package/dist/tools/detect-ui-stack.js.map +1 -1
  63. package/dist/tools/generate-component-variants.d.ts.map +1 -1
  64. package/dist/tools/generate-component-variants.js +15 -4
  65. package/dist/tools/generate-component-variants.js.map +1 -1
  66. package/dist/tools/generate-vibes.d.ts.map +1 -1
  67. package/dist/tools/generate-vibes.js +7 -3
  68. package/dist/tools/generate-vibes.js.map +1 -1
  69. package/dist/tools/index.js.map +1 -1
  70. package/dist/tools/modify-ui.d.ts.map +1 -1
  71. package/dist/tools/modify-ui.js +7 -2
  72. package/dist/tools/modify-ui.js.map +1 -1
  73. package/dist/tools/scaffold-project.d.ts.map +1 -1
  74. package/dist/tools/scaffold-project.js +3 -1
  75. package/dist/tools/scaffold-project.js.map +1 -1
  76. package/dist/tools/snippet-ui.d.ts +3 -1
  77. package/dist/tools/snippet-ui.d.ts.map +1 -1
  78. package/dist/tools/snippet-ui.js +219 -88
  79. package/dist/tools/snippet-ui.js.map +1 -1
  80. package/dist/tools/sync-design-tokens.d.ts.map +1 -1
  81. package/dist/tools/sync-design-tokens.js +26 -11
  82. package/dist/tools/sync-design-tokens.js.map +1 -1
  83. package/dist/utils/walk.d.ts.map +1 -1
  84. package/dist/utils/walk.js.map +1 -1
  85. package/dist/version.d.ts +2 -0
  86. package/dist/version.d.ts.map +1 -0
  87. package/dist/version.js +5 -0
  88. package/dist/version.js.map +1 -0
  89. package/package.json +55 -55
  90. package/src/__tests__/builder.test.ts +19 -19
  91. package/src/__tests__/config.test.ts +63 -31
  92. package/src/__tests__/filter.test.ts +98 -92
  93. package/src/__tests__/remote-client.test.ts +179 -0
  94. package/src/components/catalog.ts +170 -166
  95. package/src/config/index.ts +185 -177
  96. package/src/context/builder.ts +157 -157
  97. package/src/context/filter.ts +110 -104
  98. package/src/context/grounding.ts +143 -129
  99. package/src/context/guards.ts +97 -38
  100. package/src/context/repo-hints.ts +24 -24
  101. package/src/context/styling-detector.ts +460 -0
  102. package/src/design/principles.ts +14 -14
  103. package/src/generation/gemini-client.ts +53 -56
  104. package/src/generation/litellm-client.ts +102 -86
  105. package/src/generation/remote-client.ts +100 -77
  106. package/src/index.ts +16 -16
  107. package/src/output/file-writer.ts +123 -123
  108. package/src/output/formatter.ts +139 -132
  109. package/src/server.ts +12 -11
  110. package/src/stack/detect.ts +226 -175
  111. package/src/tokens/sync.ts +189 -155
  112. package/src/tools/analyze-screenshot-ui.ts +89 -88
  113. package/src/tools/analyze-tokens.ts +80 -78
  114. package/src/tools/catalog-components.ts +68 -68
  115. package/src/tools/create-ui.ts +295 -142
  116. package/src/tools/detect-ui-stack.ts +36 -36
  117. package/src/tools/generate-component-variants.ts +155 -135
  118. package/src/tools/generate-vibes.ts +121 -117
  119. package/src/tools/index.ts +14 -14
  120. package/src/tools/modify-ui.ts +170 -165
  121. package/src/tools/scaffold-project.ts +68 -66
  122. package/src/tools/snippet-ui.ts +323 -172
  123. package/src/tools/sync-design-tokens.ts +217 -195
  124. package/src/utils/walk.ts +47 -45
  125. package/src/version.ts +6 -0
  126. package/tsconfig.json +23 -33
  127. package/vitest.config.ts +10 -10
  128. package/.prettierrc +0 -9
  129. package/eslint.config.js +0 -37
@@ -17,217 +17,217 @@ const MAX_CONTEXT_TOKENS = 12500; // ~50k chars
17
17
  const MAX_FILE_TOKENS = 2500; // ~10k chars per file
18
18
 
19
19
  export interface ContextResult {
20
- content: string;
21
- estimatedTokens: number;
22
- filesIncluded: string[];
23
- filesSkipped: string[];
24
- truncated: boolean;
20
+ content: string;
21
+ estimatedTokens: number;
22
+ filesIncluded: string[];
23
+ filesSkipped: string[];
24
+ truncated: boolean;
25
25
  }
26
26
 
27
27
  /**
28
28
  * Estimate token count from text
29
29
  */
30
30
  export function estimateTokens(text: string): number {
31
- return Math.ceil(text.length / CHARS_PER_TOKEN);
31
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
32
32
  }
33
33
 
34
34
  /**
35
35
  * Build context from specified file paths with token optimization
36
36
  */
37
37
  export async function buildContext(paths: string[], config: Config): Promise<string> {
38
- const result = await buildContextWithMetadata(paths, config);
39
- return result.content;
38
+ const result = await buildContextWithMetadata(paths, config);
39
+ return result.content;
40
40
  }
41
41
 
42
42
  /**
43
43
  * Build context with full metadata (tokens, files included, etc.)
44
44
  */
45
45
  export async function buildContextWithMetadata(
46
- paths: string[],
47
- config: Config
46
+ paths: string[],
47
+ config: Config
48
48
  ): Promise<ContextResult> {
49
- const contents: string[] = [];
50
- const filesIncluded: string[] = [];
51
- const filesSkipped: string[] = [];
52
- let totalTokens = 0;
53
- let truncated = false;
54
-
55
- // Sort paths by likely relevance (design tokens first, then components)
56
- const sortedPaths = sortByRelevance(paths);
57
-
58
- for (const filePath of sortedPaths) {
59
- // Resolve to absolute path
60
- const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
61
-
62
- // Security check: path must be in allowed paths
63
- if (!isPathAllowed(absPath, config.allowedPaths)) {
64
- if (config.debug) {
65
- console.error(`[context] Skipping ${filePath}: outside allowed paths`);
66
- }
67
- filesSkipped.push(`${filePath} (outside allowed paths)`);
68
- continue;
69
- }
70
-
71
- // Security check: not a sensitive file
72
- if (isSensitiveFile(absPath)) {
73
- if (config.debug) {
74
- console.error(`[context] Skipping ${filePath}: sensitive file`);
75
- }
76
- filesSkipped.push(`${filePath} (sensitive)`);
77
- continue;
78
- }
49
+ const contents: string[] = [];
50
+ const filesIncluded: string[] = [];
51
+ const filesSkipped: string[] = [];
52
+ let totalTokens = 0;
53
+ let truncated = false;
54
+
55
+ // Sort paths by likely relevance (design tokens first, then components)
56
+ const sortedPaths = sortByRelevance(paths);
57
+
58
+ for (const filePath of sortedPaths) {
59
+ // Resolve to absolute path
60
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
61
+
62
+ // Security check: path must be in allowed paths
63
+ if (!isPathAllowed(absPath, config.allowedPaths)) {
64
+ if (config.debug) {
65
+ console.error(`[context] Skipping ${filePath}: outside allowed paths`);
66
+ }
67
+ filesSkipped.push(`${filePath} (outside allowed paths)`);
68
+ continue;
69
+ }
79
70
 
80
- // Check if file exists and is a file
81
- if (!fs.existsSync(absPath)) {
82
- filesSkipped.push(`${filePath} (not found)`);
83
- continue;
84
- }
71
+ // Security check: not a sensitive file
72
+ if (isSensitiveFile(absPath)) {
73
+ if (config.debug) {
74
+ console.error(`[context] Skipping ${filePath}: sensitive file`);
75
+ }
76
+ filesSkipped.push(`${filePath} (sensitive)`);
77
+ continue;
78
+ }
85
79
 
86
- const stat = fs.statSync(absPath);
87
- if (!stat.isFile()) {
88
- filesSkipped.push(`${filePath} (not a file)`);
89
- continue;
90
- }
80
+ // Check if file exists and is a file
81
+ if (!fs.existsSync(absPath)) {
82
+ filesSkipped.push(`${filePath} (not found)`);
83
+ continue;
84
+ }
91
85
 
92
- try {
93
- let content = fs.readFileSync(absPath, 'utf-8');
86
+ const stat = fs.statSync(absPath);
87
+ if (!stat.isFile()) {
88
+ filesSkipped.push(`${filePath} (not a file)`);
89
+ continue;
90
+ }
94
91
 
95
- // Sanitize content to remove any secrets
96
- content = sanitizeContent(content);
92
+ try {
93
+ let content = fs.readFileSync(absPath, 'utf-8');
97
94
 
98
- // Calculate tokens for this file
99
- let fileTokens = estimateTokens(content);
100
- const maxFileChars = MAX_FILE_TOKENS * CHARS_PER_TOKEN;
95
+ // Sanitize content to remove any secrets
96
+ content = sanitizeContent(content);
101
97
 
102
- // Truncate large files
103
- if (fileTokens > MAX_FILE_TOKENS) {
104
- content = smartTruncate(content, maxFileChars);
105
- fileTokens = MAX_FILE_TOKENS;
106
- }
98
+ // Calculate tokens for this file
99
+ let fileTokens = estimateTokens(content);
100
+ const maxFileChars = MAX_FILE_TOKENS * CHARS_PER_TOKEN;
107
101
 
108
- // Check if adding this would exceed total limit
109
- if (totalTokens + fileTokens > MAX_CONTEXT_TOKENS) {
110
- if (config.debug) {
111
- console.error(`[context] Stopping: token limit reached`);
112
- }
113
- truncated = true;
114
- break;
115
- }
102
+ // Truncate large files
103
+ if (fileTokens > MAX_FILE_TOKENS) {
104
+ content = smartTruncate(content, maxFileChars);
105
+ fileTokens = MAX_FILE_TOKENS;
106
+ }
116
107
 
117
- const ext = path.extname(absPath);
118
- const header = `/* File: ${path.basename(absPath)} (${ext}) - ~${fileTokens} tokens */`;
119
- contents.push(`${header}\n${content}`);
120
- filesIncluded.push(filePath);
121
- totalTokens += fileTokens;
122
- } catch (error) {
123
- if (config.debug) {
124
- console.error(`[context] Error reading ${filePath}:`, error);
125
- }
126
- filesSkipped.push(`${filePath} (read error)`);
108
+ // Check if adding this would exceed total limit
109
+ if (totalTokens + fileTokens > MAX_CONTEXT_TOKENS) {
110
+ if (config.debug) {
111
+ console.error(`[context] Stopping: token limit reached`);
127
112
  }
113
+ truncated = true;
114
+ break;
115
+ }
116
+
117
+ const ext = path.extname(absPath);
118
+ const header = `/* File: ${path.basename(absPath)} (${ext}) - ~${fileTokens} tokens */`;
119
+ contents.push(`${header}\n${content}`);
120
+ filesIncluded.push(filePath);
121
+ totalTokens += fileTokens;
122
+ } catch (error) {
123
+ if (config.debug) {
124
+ console.error(`[context] Error reading ${filePath}:`, error);
125
+ }
126
+ filesSkipped.push(`${filePath} (read error)`);
128
127
  }
129
-
130
- return {
131
- content: contents.length > 0 ? contents.join('\n\n---\n\n') : '',
132
- estimatedTokens: totalTokens,
133
- filesIncluded,
134
- filesSkipped,
135
- truncated,
136
- };
128
+ }
129
+
130
+ return {
131
+ content: contents.length > 0 ? contents.join('\n\n---\n\n') : '',
132
+ estimatedTokens: totalTokens,
133
+ filesIncluded,
134
+ filesSkipped,
135
+ truncated,
136
+ };
137
137
  }
138
138
 
139
139
  /**
140
140
  * Sort paths by relevance (design tokens and variables first)
141
141
  */
142
142
  function sortByRelevance(paths: string[]): string[] {
143
- const priority: Record<string, number> = {
144
- tokens: 0,
145
- variables: 0,
146
- theme: 1,
147
- design: 1,
148
- colors: 2,
149
- typography: 2,
150
- styles: 3,
151
- css: 4,
152
- };
153
-
154
- return [...paths].sort((a, b) => {
155
- const aName = path.basename(a).toLowerCase();
156
- const bName = path.basename(b).toLowerCase();
157
-
158
- let aPriority = 10;
159
- let bPriority = 10;
160
-
161
- for (const [key, value] of Object.entries(priority)) {
162
- if (aName.includes(key)) aPriority = Math.min(aPriority, value);
163
- if (bName.includes(key)) bPriority = Math.min(bPriority, value);
164
- }
143
+ const priority: Record<string, number> = {
144
+ tokens: 0,
145
+ variables: 0,
146
+ theme: 1,
147
+ design: 1,
148
+ colors: 2,
149
+ typography: 2,
150
+ styles: 3,
151
+ css: 4,
152
+ };
153
+
154
+ return [...paths].sort((a, b) => {
155
+ const aName = path.basename(a).toLowerCase();
156
+ const bName = path.basename(b).toLowerCase();
157
+
158
+ let aPriority = 10;
159
+ let bPriority = 10;
160
+
161
+ for (const [key, value] of Object.entries(priority)) {
162
+ if (aName.includes(key)) aPriority = Math.min(aPriority, value);
163
+ if (bName.includes(key)) bPriority = Math.min(bPriority, value);
164
+ }
165
165
 
166
- return aPriority - bPriority;
167
- });
166
+ return aPriority - bPriority;
167
+ });
168
168
  }
169
169
 
170
170
  /**
171
171
  * Smart truncate: keep beginning and end, with clear indicator
172
172
  */
173
173
  function smartTruncate(content: string, maxChars: number): string {
174
- if (content.length <= maxChars) return content;
174
+ if (content.length <= maxChars) return content;
175
175
 
176
- const keepStart = Math.floor(maxChars * 0.7);
177
- const keepEnd = Math.floor(maxChars * 0.2);
176
+ const keepStart = Math.floor(maxChars * 0.7);
177
+ const keepEnd = Math.floor(maxChars * 0.2);
178
178
 
179
- const start = content.slice(0, keepStart);
180
- const end = content.slice(-keepEnd);
179
+ const start = content.slice(0, keepStart);
180
+ const end = content.slice(-keepEnd);
181
181
 
182
- return `${start}\n\n/* ... [${Math.round((content.length - maxChars) / 1000)}k chars truncated] ... */\n\n${end}`;
182
+ return `${start}\n\n/* ... [${Math.round((content.length - maxChars) / 1000)}k chars truncated] ... */\n\n${end}`;
183
183
  }
184
184
 
185
185
  /**
186
186
  * Automatically discover relevant UI files in a directory
187
187
  */
188
188
  export async function discoverUIFiles(directory: string, config: Config): Promise<string[]> {
189
- const uiPatterns = [
190
- /\.(css|scss|less|sass)$/,
191
- /\.(tsx|jsx)$/,
192
- /\.(vue|svelte)$/,
193
- /theme\./,
194
- /design[-_]?tokens?\./,
195
- /tailwind\.config\./,
196
- ];
197
-
198
- const files: string[] = [];
199
-
200
- function scan(dir: string, depth: number = 0) {
201
- if (depth > 3) return; // Max depth
202
-
203
- try {
204
- const entries = fs.readdirSync(dir, { withFileTypes: true });
205
-
206
- for (const entry of entries) {
207
- const fullPath = path.join(dir, entry.name);
208
-
209
- // Skip node_modules, .git, etc.
210
- if (entry.isDirectory()) {
211
- if (['node_modules', '.git', 'dist', 'build', '.next', '.nuxt'].includes(entry.name)) {
212
- continue;
213
- }
214
- scan(fullPath, depth + 1);
215
- } else if (entry.isFile()) {
216
- // Check if matches UI patterns
217
- if (uiPatterns.some((pattern) => pattern.test(entry.name))) {
218
- if (isPathAllowed(fullPath, config.allowedPaths) && !isSensitiveFile(fullPath)) {
219
- files.push(fullPath);
220
- }
221
- }
222
- }
189
+ const uiPatterns = [
190
+ /\.(css|scss|less|sass)$/,
191
+ /\.(tsx|jsx)$/,
192
+ /\.(vue|svelte)$/,
193
+ /theme\./,
194
+ /design[-_]?tokens?\./,
195
+ /tailwind\.config\./,
196
+ ];
197
+
198
+ const files: string[] = [];
199
+
200
+ function scan(dir: string, depth: number = 0) {
201
+ if (depth > 3) return; // Max depth
202
+
203
+ try {
204
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
205
+
206
+ for (const entry of entries) {
207
+ const fullPath = path.join(dir, entry.name);
208
+
209
+ // Skip node_modules, .git, etc.
210
+ if (entry.isDirectory()) {
211
+ if (['node_modules', '.git', 'dist', 'build', '.next', '.nuxt'].includes(entry.name)) {
212
+ continue;
213
+ }
214
+ scan(fullPath, depth + 1);
215
+ } else if (entry.isFile()) {
216
+ // Check if matches UI patterns
217
+ if (uiPatterns.some((pattern) => pattern.test(entry.name))) {
218
+ if (isPathAllowed(fullPath, config.allowedPaths) && !isSensitiveFile(fullPath)) {
219
+ files.push(fullPath);
223
220
  }
224
- } catch {
225
- // Skip directories we can't read
221
+ }
226
222
  }
223
+ }
224
+ } catch {
225
+ // Skip directories we can't read
227
226
  }
227
+ }
228
228
 
229
- scan(directory);
229
+ scan(directory);
230
230
 
231
- // Sort by relevance
232
- return sortByRelevance(files);
231
+ // Sort by relevance
232
+ return sortByRelevance(files);
233
233
  }
@@ -11,124 +11,130 @@ import * as path from 'node:path';
11
11
  * Files/directories that should NEVER be included in context
12
12
  */
13
13
  const SENSITIVE_PATTERNS = [
14
- // Environment and secrets
15
- /\.env/i,
16
- /secrets?\./i,
17
- /\.pem$/i,
18
- /\.key$/i,
19
- /\.crt$/i,
20
- /credentials/i,
21
- /\.htpasswd/i,
22
-
23
- // Private keys and certificates
24
- /id_rsa/i,
25
- /id_ed25519/i,
26
- /\.p12$/i,
27
- /\.pfx$/i,
28
-
29
- // Config with potential secrets
30
- /\.npmrc$/i,
31
- /\.pypirc$/i,
32
- /kubeconfig/i,
33
- /\.docker\/config\.json$/i,
34
-
35
- // Database
36
- /\.sqlite$/i,
37
- /\.db$/i,
38
- /migrations?\//i,
39
- /seeds?\//i,
40
-
41
- // Backend/server code (when isolating UI)
42
- /\/api\//i,
43
- /\/server\//i,
44
- /\/backend\//i,
45
- /\/functions\//i, // Serverless
46
- /\/lambda\//i,
47
- /\/middleware\//i,
48
-
49
- // Auth-related
50
- /\/auth\//i,
51
- /passport/i,
52
- /jwt/i,
53
-
54
- // System and dependencies
55
- /node_modules\//i,
56
- /\.git\//i,
57
- /vendor\//i,
58
- /\.cache\//i,
59
- /\.next\//i,
60
- /\.nuxt\//i,
61
- /dist\//i,
62
- /build\//i,
14
+ // Environment and secrets
15
+ /\.env/i,
16
+ /secrets?\./i,
17
+ /\.pem$/i,
18
+ /\.key$/i,
19
+ /\.crt$/i,
20
+ /credentials/i,
21
+ /\.htpasswd/i,
22
+
23
+ // Private keys and certificates
24
+ /id_rsa/i,
25
+ /id_ed25519/i,
26
+ /\.p12$/i,
27
+ /\.pfx$/i,
28
+
29
+ // Config with potential secrets
30
+ /\.npmrc$/i,
31
+ /\.pypirc$/i,
32
+ /kubeconfig/i,
33
+ /\.docker\/config\.json$/i,
34
+
35
+ // Database
36
+ /\.sqlite$/i,
37
+ /\.db$/i,
38
+ /migrations?\//i,
39
+ /seeds?\//i,
40
+
41
+ // Backend/server code (when isolating UI)
42
+ /\/api\//i,
43
+ /\/server\//i,
44
+ /\/backend\//i,
45
+ /\/functions\//i, // Serverless
46
+ /\/lambda\//i,
47
+ /\/middleware\//i,
48
+
49
+ // Auth-related
50
+ /\/auth\//i,
51
+ /passport/i,
52
+ /jwt/i,
53
+
54
+ // System and dependencies
55
+ /node_modules\//i,
56
+ /\.git\//i,
57
+ /vendor\//i,
58
+ /\.cache\//i,
59
+ /\.next\//i,
60
+ /\.nuxt\//i,
61
+ /dist\//i,
62
+ /build\//i,
63
63
  ];
64
64
 
65
65
  /**
66
66
  * UI-relevant file patterns (for auto-discovery)
67
67
  */
68
68
  export const UI_INCLUDE_PATTERNS = [
69
- // Stylesheets
70
- /\.(css|scss|less|sass|styl)$/i,
71
-
72
- // Components
73
- /\.(tsx|jsx)$/i,
74
- /\.(vue|svelte)$/i,
75
-
76
- // Design tokens
77
- /theme\./i,
78
- /tokens?\./i,
79
- /variables\./i,
80
- /design[-_]?system/i,
81
-
82
- // Config files for styling
83
- /tailwind\.config/i,
84
- /postcss\.config/i,
85
- /styled-components/i,
69
+ // Stylesheets
70
+ /\.(css|scss|less|sass|styl)$/i,
71
+
72
+ // Components
73
+ /\.(tsx|jsx)$/i,
74
+ /\.(vue|svelte)$/i,
75
+
76
+ // Design tokens
77
+ /theme\./i,
78
+ /tokens?\./i,
79
+ /variables\./i,
80
+ /design[-_]?system/i,
81
+
82
+ // Config files for styling
83
+ /tailwind\.config/i,
84
+ /postcss\.config/i,
85
+ /styled-components/i,
86
86
  ];
87
87
 
88
88
  /**
89
89
  * Check if a file path matches sensitive patterns
90
90
  */
91
91
  export function isSensitiveFile(filePath: string): boolean {
92
- const normalized = filePath.replace(/\\/g, '/');
92
+ const normalized = filePath.replace(/\\/g, '/');
93
93
 
94
- for (const pattern of SENSITIVE_PATTERNS) {
95
- if (pattern.test(normalized)) {
96
- return true;
97
- }
94
+ for (const pattern of SENSITIVE_PATTERNS) {
95
+ if (pattern.test(normalized)) {
96
+ return true;
98
97
  }
98
+ }
99
99
 
100
- return false;
100
+ return false;
101
101
  }
102
102
 
103
103
  /**
104
104
  * Check if a file path is within allowed paths
105
105
  */
106
106
  export function isPathAllowed(filePath: string, allowedPaths: string[]): boolean {
107
- const absPath = path.resolve(filePath);
108
-
109
- for (const allowed of allowedPaths) {
110
- const absAllowed = path.resolve(allowed);
111
- if (absPath.startsWith(absAllowed)) {
112
- return true;
113
- }
107
+ const absPath = path.resolve(filePath);
108
+
109
+ for (const allowed of allowedPaths) {
110
+ const absAllowed = path.resolve(allowed);
111
+ const rel = path.relative(absAllowed, absPath);
112
+ // IMPORTANT: use a path-segment-aware check; `startsWith('..')` is not enough
113
+ // because legitimate paths like `.../file` would be incorrectly blocked.
114
+ if (
115
+ rel === '' ||
116
+ (!rel.startsWith(`..${path.sep}`) && rel !== '..' && !path.isAbsolute(rel))
117
+ ) {
118
+ return true;
114
119
  }
120
+ }
115
121
 
116
- return false;
122
+ return false;
117
123
  }
118
124
 
119
125
  /**
120
126
  * Check if a file is UI-relevant
121
127
  */
122
128
  export function isUIRelevant(filePath: string): boolean {
123
- const normalized = filePath.replace(/\\/g, '/');
129
+ const normalized = filePath.replace(/\\/g, '/');
124
130
 
125
- for (const pattern of UI_INCLUDE_PATTERNS) {
126
- if (pattern.test(normalized)) {
127
- return true;
128
- }
131
+ for (const pattern of UI_INCLUDE_PATTERNS) {
132
+ if (pattern.test(normalized)) {
133
+ return true;
129
134
  }
135
+ }
130
136
 
131
- return false;
137
+ return false;
132
138
  }
133
139
 
134
140
  /**
@@ -136,29 +142,29 @@ export function isUIRelevant(filePath: string): boolean {
136
142
  * This is a best-effort filter for dynamic content
137
143
  */
138
144
  export function sanitizeContent(content: string): string {
139
- // Remove common secret patterns
140
- const patterns = [
141
- // API keys (generic patterns)
142
- /(['"`])?(api[_-]?key|apikey|secret|password|token|auth)(['"`])?[\s]*[:=][\s]*['"`][^'"`]+['"`]/gi,
145
+ // Remove common secret patterns
146
+ const patterns = [
147
+ // API keys (generic patterns)
148
+ /(['"`])?(api[_-]?key|apikey|secret|password|token|auth)(['"`])?[\s]*[:=][\s]*['"`][^'"`]+['"`]/gi,
143
149
 
144
- // Bearer tokens
145
- /Bearer\s+[A-Za-z0-9\-_=]+\.[A-Za-z0-9\-_=]+\.?[A-Za-z0-9\-_.+/=]*/gi,
150
+ // Bearer tokens
151
+ /Bearer\s+[A-Za-z0-9\-_=]+\.[A-Za-z0-9\-_=]+\.?[A-Za-z0-9\-_.+/=]*/gi,
146
152
 
147
- // AWS keys
148
- /AKIA[0-9A-Z]{16}/g,
153
+ // AWS keys
154
+ /AKIA[0-9A-Z]{16}/g,
149
155
 
150
- // Private keys
151
- /-----BEGIN[\s\w]+PRIVATE KEY-----[\s\S]+?-----END[\s\w]+PRIVATE KEY-----/g,
156
+ // Private keys
157
+ /-----BEGIN[\s\w]+PRIVATE KEY-----[\s\S]+?-----END[\s\w]+PRIVATE KEY-----/g,
152
158
 
153
- // Connection strings
154
- /(mongodb|postgresql|mysql|redis):\/\/[^\s'"]+/gi,
155
- ];
159
+ // Connection strings
160
+ /(mongodb|postgresql|mysql|redis):\/\/[^\s'"]+/gi,
161
+ ];
156
162
 
157
- let result = content;
163
+ let result = content;
158
164
 
159
- for (const pattern of patterns) {
160
- result = result.replace(pattern, '[REDACTED]');
161
- }
165
+ for (const pattern of patterns) {
166
+ result = result.replace(pattern, '[REDACTED]');
167
+ }
162
168
 
163
- return result;
169
+ return result;
164
170
  }