@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.
- package/dist/components/catalog.d.ts.map +1 -1
- package/dist/components/catalog.js +10 -4
- package/dist/components/catalog.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +11 -6
- package/dist/config/index.js.map +1 -1
- package/dist/context/builder.d.ts.map +1 -1
- package/dist/context/builder.js.map +1 -1
- package/dist/context/filter.d.ts.map +1 -1
- package/dist/context/filter.js +5 -1
- package/dist/context/filter.js.map +1 -1
- package/dist/context/grounding.d.ts.map +1 -1
- package/dist/context/grounding.js +7 -3
- package/dist/context/grounding.js.map +1 -1
- package/dist/context/guards.d.ts.map +1 -1
- package/dist/context/guards.js +53 -0
- package/dist/context/guards.js.map +1 -1
- package/dist/context/repo-hints.js.map +1 -1
- package/dist/context/styling-detector.d.ts +24 -0
- package/dist/context/styling-detector.d.ts.map +1 -0
- package/dist/context/styling-detector.js +337 -0
- package/dist/context/styling-detector.js.map +1 -0
- package/dist/design/principles.js.map +1 -1
- package/dist/generation/gemini-client.d.ts.map +1 -1
- package/dist/generation/gemini-client.js.map +1 -1
- package/dist/generation/litellm-client.d.ts.map +1 -1
- package/dist/generation/litellm-client.js +14 -7
- package/dist/generation/litellm-client.js.map +1 -1
- package/dist/generation/remote-client.d.ts +10 -5
- package/dist/generation/remote-client.d.ts.map +1 -1
- package/dist/generation/remote-client.js +13 -2
- package/dist/generation/remote-client.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/output/file-writer.d.ts.map +1 -1
- package/dist/output/file-writer.js +4 -4
- package/dist/output/file-writer.js.map +1 -1
- package/dist/output/formatter.d.ts.map +1 -1
- package/dist/output/formatter.js +5 -2
- package/dist/output/formatter.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/stack/detect.d.ts.map +1 -1
- package/dist/stack/detect.js +42 -9
- package/dist/stack/detect.js.map +1 -1
- package/dist/tokens/sync.d.ts.map +1 -1
- package/dist/tokens/sync.js +22 -5
- package/dist/tokens/sync.js.map +1 -1
- package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -1
- package/dist/tools/analyze-screenshot-ui.js +5 -5
- package/dist/tools/analyze-screenshot-ui.js.map +1 -1
- package/dist/tools/analyze-tokens.d.ts.map +1 -1
- package/dist/tools/analyze-tokens.js +3 -1
- package/dist/tools/analyze-tokens.js.map +1 -1
- package/dist/tools/catalog-components.d.ts.map +1 -1
- package/dist/tools/catalog-components.js +1 -4
- package/dist/tools/catalog-components.js.map +1 -1
- package/dist/tools/create-ui.d.ts +3 -0
- package/dist/tools/create-ui.d.ts.map +1 -1
- package/dist/tools/create-ui.js +203 -75
- package/dist/tools/create-ui.js.map +1 -1
- package/dist/tools/detect-ui-stack.js.map +1 -1
- package/dist/tools/generate-component-variants.d.ts.map +1 -1
- package/dist/tools/generate-component-variants.js +15 -4
- package/dist/tools/generate-component-variants.js.map +1 -1
- package/dist/tools/generate-vibes.d.ts.map +1 -1
- package/dist/tools/generate-vibes.js +7 -3
- package/dist/tools/generate-vibes.js.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/modify-ui.d.ts.map +1 -1
- package/dist/tools/modify-ui.js +7 -2
- package/dist/tools/modify-ui.js.map +1 -1
- package/dist/tools/scaffold-project.d.ts.map +1 -1
- package/dist/tools/scaffold-project.js +3 -1
- package/dist/tools/scaffold-project.js.map +1 -1
- package/dist/tools/snippet-ui.d.ts +3 -1
- package/dist/tools/snippet-ui.d.ts.map +1 -1
- package/dist/tools/snippet-ui.js +219 -88
- package/dist/tools/snippet-ui.js.map +1 -1
- package/dist/tools/sync-design-tokens.d.ts.map +1 -1
- package/dist/tools/sync-design-tokens.js +26 -11
- package/dist/tools/sync-design-tokens.js.map +1 -1
- package/dist/utils/walk.d.ts.map +1 -1
- package/dist/utils/walk.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +55 -55
- package/src/__tests__/builder.test.ts +19 -19
- package/src/__tests__/config.test.ts +63 -31
- package/src/__tests__/filter.test.ts +98 -92
- package/src/__tests__/remote-client.test.ts +179 -0
- package/src/components/catalog.ts +170 -166
- package/src/config/index.ts +185 -177
- package/src/context/builder.ts +157 -157
- package/src/context/filter.ts +110 -104
- package/src/context/grounding.ts +143 -129
- package/src/context/guards.ts +97 -38
- package/src/context/repo-hints.ts +24 -24
- package/src/context/styling-detector.ts +460 -0
- package/src/design/principles.ts +14 -14
- package/src/generation/gemini-client.ts +53 -56
- package/src/generation/litellm-client.ts +102 -86
- package/src/generation/remote-client.ts +100 -77
- package/src/index.ts +16 -16
- package/src/output/file-writer.ts +123 -123
- package/src/output/formatter.ts +139 -132
- package/src/server.ts +12 -11
- package/src/stack/detect.ts +226 -175
- package/src/tokens/sync.ts +189 -155
- package/src/tools/analyze-screenshot-ui.ts +89 -88
- package/src/tools/analyze-tokens.ts +80 -78
- package/src/tools/catalog-components.ts +68 -68
- package/src/tools/create-ui.ts +295 -142
- package/src/tools/detect-ui-stack.ts +36 -36
- package/src/tools/generate-component-variants.ts +155 -135
- package/src/tools/generate-vibes.ts +121 -117
- package/src/tools/index.ts +14 -14
- package/src/tools/modify-ui.ts +170 -165
- package/src/tools/scaffold-project.ts +68 -66
- package/src/tools/snippet-ui.ts +323 -172
- package/src/tools/sync-design-tokens.ts +217 -195
- package/src/utils/walk.ts +47 -45
- package/src/version.ts +6 -0
- package/tsconfig.json +23 -33
- package/vitest.config.ts +10 -10
- package/.prettierrc +0 -9
- package/eslint.config.js +0 -37
package/src/context/builder.ts
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
47
|
-
|
|
46
|
+
paths: string[],
|
|
47
|
+
config: Config
|
|
48
48
|
): Promise<ContextResult> {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
86
|
+
const stat = fs.statSync(absPath);
|
|
87
|
+
if (!stat.isFile()) {
|
|
88
|
+
filesSkipped.push(`${filePath} (not a file)`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
94
91
|
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
try {
|
|
93
|
+
let content = fs.readFileSync(absPath, 'utf-8');
|
|
97
94
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const maxFileChars = MAX_FILE_TOKENS * CHARS_PER_TOKEN;
|
|
95
|
+
// Sanitize content to remove any secrets
|
|
96
|
+
content = sanitizeContent(content);
|
|
101
97
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
+
if (content.length <= maxChars) return content;
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
const keepStart = Math.floor(maxChars * 0.7);
|
|
177
|
+
const keepEnd = Math.floor(maxChars * 0.2);
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
const start = content.slice(0, keepStart);
|
|
180
|
+
const end = content.slice(-keepEnd);
|
|
181
181
|
|
|
182
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
+
scan(directory);
|
|
230
230
|
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
// Sort by relevance
|
|
232
|
+
return sortByRelevance(files);
|
|
233
233
|
}
|
package/src/context/filter.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
92
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
94
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
95
|
+
if (pattern.test(normalized)) {
|
|
96
|
+
return true;
|
|
98
97
|
}
|
|
98
|
+
}
|
|
99
99
|
|
|
100
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
124
130
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
131
|
+
for (const pattern of UI_INCLUDE_PATTERNS) {
|
|
132
|
+
if (pattern.test(normalized)) {
|
|
133
|
+
return true;
|
|
129
134
|
}
|
|
135
|
+
}
|
|
130
136
|
|
|
131
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
150
|
+
// Bearer tokens
|
|
151
|
+
/Bearer\s+[A-Za-z0-9\-_=]+\.[A-Za-z0-9\-_=]+\.?[A-Za-z0-9\-_.+/=]*/gi,
|
|
146
152
|
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
// AWS keys
|
|
154
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
149
155
|
|
|
150
|
-
|
|
151
|
-
|
|
156
|
+
// Private keys
|
|
157
|
+
/-----BEGIN[\s\w]+PRIVATE KEY-----[\s\S]+?-----END[\s\w]+PRIVATE KEY-----/g,
|
|
152
158
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
159
|
+
// Connection strings
|
|
160
|
+
/(mongodb|postgresql|mysql|redis):\/\/[^\s'"]+/gi,
|
|
161
|
+
];
|
|
156
162
|
|
|
157
|
-
|
|
163
|
+
let result = content;
|
|
158
164
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
165
|
+
for (const pattern of patterns) {
|
|
166
|
+
result = result.replace(pattern, '[REDACTED]');
|
|
167
|
+
}
|
|
162
168
|
|
|
163
|
-
|
|
169
|
+
return result;
|
|
164
170
|
}
|