@gemini-designer/mcp-server 0.1.0
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/.prettierrc +9 -0
- package/dist/components/catalog.d.ts +24 -0
- package/dist/components/catalog.d.ts.map +1 -0
- package/dist/components/catalog.js +186 -0
- package/dist/components/catalog.js.map +1 -0
- package/dist/config/index.d.ts +60 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +199 -0
- package/dist/config/index.js.map +1 -0
- package/dist/context/builder.d.ts +32 -0
- package/dist/context/builder.d.ts.map +1 -0
- package/dist/context/builder.js +194 -0
- package/dist/context/builder.js.map +1 -0
- package/dist/context/filter.d.ts +28 -0
- package/dist/context/filter.d.ts.map +1 -0
- package/dist/context/filter.js +136 -0
- package/dist/context/filter.js.map +1 -0
- package/dist/context/grounding.d.ts +27 -0
- package/dist/context/grounding.d.ts.map +1 -0
- package/dist/context/grounding.js +162 -0
- package/dist/context/grounding.js.map +1 -0
- package/dist/context/guards.d.ts +31 -0
- package/dist/context/guards.d.ts.map +1 -0
- package/dist/context/guards.js +76 -0
- package/dist/context/guards.js.map +1 -0
- package/dist/context/repo-hints.d.ts +12 -0
- package/dist/context/repo-hints.d.ts.map +1 -0
- package/dist/context/repo-hints.js +40 -0
- package/dist/context/repo-hints.js.map +1 -0
- package/dist/generation/gemini-client.d.ts +27 -0
- package/dist/generation/gemini-client.d.ts.map +1 -0
- package/dist/generation/gemini-client.js +64 -0
- package/dist/generation/gemini-client.js.map +1 -0
- package/dist/generation/litellm-client.d.ts +16 -0
- package/dist/generation/litellm-client.d.ts.map +1 -0
- package/dist/generation/litellm-client.js +98 -0
- package/dist/generation/litellm-client.js.map +1 -0
- package/dist/generation/remote-client.d.ts +20 -0
- package/dist/generation/remote-client.d.ts.map +1 -0
- package/dist/generation/remote-client.js +69 -0
- package/dist/generation/remote-client.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/output/file-writer.d.ts +39 -0
- package/dist/output/file-writer.d.ts.map +1 -0
- package/dist/output/file-writer.js +153 -0
- package/dist/output/file-writer.js.map +1 -0
- package/dist/output/formatter.d.ts +26 -0
- package/dist/output/formatter.d.ts.map +1 -0
- package/dist/output/formatter.js +156 -0
- package/dist/output/formatter.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +22 -0
- package/dist/server.js.map +1 -0
- package/dist/stack/detect.d.ts +49 -0
- package/dist/stack/detect.d.ts.map +1 -0
- package/dist/stack/detect.js +157 -0
- package/dist/stack/detect.js.map +1 -0
- package/dist/tokens/sync.d.ts +32 -0
- package/dist/tokens/sync.d.ts.map +1 -0
- package/dist/tokens/sync.js +188 -0
- package/dist/tokens/sync.js.map +1 -0
- package/dist/tools/analyze-screenshot-ui.d.ts +18 -0
- package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -0
- package/dist/tools/analyze-screenshot-ui.js +133 -0
- package/dist/tools/analyze-screenshot-ui.js.map +1 -0
- package/dist/tools/analyze-tokens.d.ts +10 -0
- package/dist/tools/analyze-tokens.d.ts.map +1 -0
- package/dist/tools/analyze-tokens.js +107 -0
- package/dist/tools/analyze-tokens.js.map +1 -0
- package/dist/tools/catalog-components.d.ts +14 -0
- package/dist/tools/catalog-components.d.ts.map +1 -0
- package/dist/tools/catalog-components.js +85 -0
- package/dist/tools/catalog-components.js.map +1 -0
- package/dist/tools/create-ui.d.ts +10 -0
- package/dist/tools/create-ui.d.ts.map +1 -0
- package/dist/tools/create-ui.js +167 -0
- package/dist/tools/create-ui.js.map +1 -0
- package/dist/tools/detect-ui-stack.d.ts +15 -0
- package/dist/tools/detect-ui-stack.d.ts.map +1 -0
- package/dist/tools/detect-ui-stack.js +52 -0
- package/dist/tools/detect-ui-stack.js.map +1 -0
- package/dist/tools/generate-component-variants.d.ts +15 -0
- package/dist/tools/generate-component-variants.d.ts.map +1 -0
- package/dist/tools/generate-component-variants.js +199 -0
- package/dist/tools/generate-component-variants.js.map +1 -0
- package/dist/tools/generate-vibes.d.ts +10 -0
- package/dist/tools/generate-vibes.d.ts.map +1 -0
- package/dist/tools/generate-vibes.js +145 -0
- package/dist/tools/generate-vibes.js.map +1 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +36 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/modify-ui.d.ts +11 -0
- package/dist/tools/modify-ui.d.ts.map +1 -0
- package/dist/tools/modify-ui.js +207 -0
- package/dist/tools/modify-ui.js.map +1 -0
- package/dist/tools/scaffold-project.d.ts +10 -0
- package/dist/tools/scaffold-project.d.ts.map +1 -0
- package/dist/tools/scaffold-project.js +122 -0
- package/dist/tools/scaffold-project.js.map +1 -0
- package/dist/tools/snippet-ui.d.ts +11 -0
- package/dist/tools/snippet-ui.d.ts.map +1 -0
- package/dist/tools/snippet-ui.js +194 -0
- package/dist/tools/snippet-ui.js.map +1 -0
- package/dist/tools/sync-design-tokens.d.ts +14 -0
- package/dist/tools/sync-design-tokens.d.ts.map +1 -0
- package/dist/tools/sync-design-tokens.js +233 -0
- package/dist/tools/sync-design-tokens.js.map +1 -0
- package/dist/utils/walk.d.ts +15 -0
- package/dist/utils/walk.d.ts.map +1 -0
- package/dist/utils/walk.js +63 -0
- package/dist/utils/walk.js.map +1 -0
- package/eslint.config.js +37 -0
- package/package.json +56 -0
- package/src/__tests__/builder.test.ts +31 -0
- package/src/__tests__/config.test.ts +52 -0
- package/src/__tests__/filter.test.ts +109 -0
- package/src/components/catalog.ts +214 -0
- package/src/config/index.ts +237 -0
- package/src/context/builder.ts +233 -0
- package/src/context/filter.ts +164 -0
- package/src/context/grounding.ts +191 -0
- package/src/context/guards.ts +94 -0
- package/src/context/repo-hints.ts +43 -0
- package/src/generation/gemini-client.ts +94 -0
- package/src/generation/litellm-client.ts +121 -0
- package/src/generation/remote-client.ts +103 -0
- package/src/index.ts +36 -0
- package/src/output/file-writer.ts +181 -0
- package/src/output/formatter.ts +186 -0
- package/src/server.ts +28 -0
- package/src/stack/detect.ts +204 -0
- package/src/tokens/sync.ts +212 -0
- package/src/tools/analyze-screenshot-ui.ts +150 -0
- package/src/tools/analyze-tokens.ts +123 -0
- package/src/tools/catalog-components.ts +99 -0
- package/src/tools/create-ui.ts +194 -0
- package/src/tools/detect-ui-stack.ts +64 -0
- package/src/tools/generate-component-variants.ts +218 -0
- package/src/tools/generate-vibes.ts +177 -0
- package/src/tools/index.ts +42 -0
- package/src/tools/modify-ui.ts +230 -0
- package/src/tools/scaffold-project.ts +138 -0
- package/src/tools/snippet-ui.ts +222 -0
- package/src/tools/sync-design-tokens.ts +256 -0
- package/src/utils/walk.ts +75 -0
- package/tsconfig.json +34 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Catalog
|
|
3
|
+
*
|
|
4
|
+
* Scans TSX/JSX files and extracts exported component-like symbols.
|
|
5
|
+
*
|
|
6
|
+
* Notes:
|
|
7
|
+
* - Uses TypeScript compiler API when available.
|
|
8
|
+
* - Falls back to a lightweight regex scan if TypeScript is not installed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { toPosixPath } from '../utils/walk.js';
|
|
14
|
+
|
|
15
|
+
export interface ComponentExport {
|
|
16
|
+
name: string;
|
|
17
|
+
exportType: 'named' | 'default';
|
|
18
|
+
file: string; // relative to root
|
|
19
|
+
propsType?: string;
|
|
20
|
+
jsDoc?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CatalogResult {
|
|
24
|
+
root: string;
|
|
25
|
+
filesScanned: number;
|
|
26
|
+
components: ComponentExport[];
|
|
27
|
+
warnings: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readFileSafe(filePath: string): string | null {
|
|
31
|
+
try {
|
|
32
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rel(root: string, abs: string): string {
|
|
39
|
+
return toPosixPath(path.relative(root, abs));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extractJSDocFromLeadingComment(text: string): string | undefined {
|
|
43
|
+
// Simple: capture /** ... */ immediately preceding export
|
|
44
|
+
const m = text.match(/\/\*\*([\s\S]*?)\*\//);
|
|
45
|
+
if (!m) return undefined;
|
|
46
|
+
const cleaned = m[1]
|
|
47
|
+
.split('\n')
|
|
48
|
+
.map((l) => l.replace(/^\s*\*\s?/, '').trim())
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join(' ');
|
|
51
|
+
return cleaned || undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function scanWithTypeScript(root: string, files: string[]): Promise<CatalogResult> {
|
|
55
|
+
const warnings: string[] = [];
|
|
56
|
+
const components: ComponentExport[] = [];
|
|
57
|
+
|
|
58
|
+
const ts = await import('typescript');
|
|
59
|
+
|
|
60
|
+
const getScriptKind = (filePath: string): any => {
|
|
61
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
62
|
+
if (ext === '.tsx') return ts.ScriptKind.TSX;
|
|
63
|
+
if (ext === '.jsx') return ts.ScriptKind.JSX;
|
|
64
|
+
if (ext === '.ts') return ts.ScriptKind.TS;
|
|
65
|
+
if (ext === '.js') return ts.ScriptKind.JS;
|
|
66
|
+
return ts.ScriptKind.Unknown;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
for (const f of files) {
|
|
70
|
+
const content = readFileSafe(f);
|
|
71
|
+
if (content == null) continue;
|
|
72
|
+
|
|
73
|
+
const sf = ts.createSourceFile(f, content, ts.ScriptTarget.ES2022, true, getScriptKind(f));
|
|
74
|
+
|
|
75
|
+
const isExported = (node: any): boolean => {
|
|
76
|
+
const mods = node.modifiers;
|
|
77
|
+
if (!mods) return false;
|
|
78
|
+
return mods.some((m: any) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const isDefaultExport = (node: any): boolean => {
|
|
82
|
+
const mods = node.modifiers;
|
|
83
|
+
if (!mods) return false;
|
|
84
|
+
return mods.some((m: any) => m.kind === ts.SyntaxKind.DefaultKeyword);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const textOfType = (typeNode: any): string | undefined => {
|
|
88
|
+
if (!typeNode) return undefined;
|
|
89
|
+
return content.slice(typeNode.pos, typeNode.end).trim();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Collect import modules (optional future use)
|
|
93
|
+
// const imports = sf.statements.filter(ts.isImportDeclaration).map((i: any) => i.moduleSpecifier.text);
|
|
94
|
+
|
|
95
|
+
for (const stmt of sf.statements) {
|
|
96
|
+
if (ts.isFunctionDeclaration(stmt) && isExported(stmt)) {
|
|
97
|
+
const name = stmt.name?.text || (isDefaultExport(stmt) ? 'default' : 'anonymous');
|
|
98
|
+
const firstParam = stmt.parameters?.[0];
|
|
99
|
+
const propsType = textOfType(firstParam?.type);
|
|
100
|
+
components.push({
|
|
101
|
+
name: name === 'default' ? path.basename(f, path.extname(f)) : name,
|
|
102
|
+
exportType: isDefaultExport(stmt) ? 'default' : 'named',
|
|
103
|
+
file: rel(root, f),
|
|
104
|
+
propsType,
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (ts.isVariableStatement(stmt) && isExported(stmt)) {
|
|
110
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
111
|
+
const name = decl.name && ts.isIdentifier(decl.name) ? decl.name.text : undefined;
|
|
112
|
+
if (!name) continue;
|
|
113
|
+
|
|
114
|
+
// Component-like initializers
|
|
115
|
+
const init = decl.initializer;
|
|
116
|
+
let propsType: string | undefined;
|
|
117
|
+
if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
|
|
118
|
+
const firstParam = init.parameters?.[0];
|
|
119
|
+
propsType = textOfType(firstParam?.type);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// React.forwardRef(...) pattern: export const X = React.forwardRef<...>((props, ref) => ...)
|
|
123
|
+
if (init && ts.isCallExpression(init)) {
|
|
124
|
+
const args = init.arguments;
|
|
125
|
+
const firstArg = args?.[0];
|
|
126
|
+
if (firstArg && (ts.isArrowFunction(firstArg) || ts.isFunctionExpression(firstArg))) {
|
|
127
|
+
const firstParam = firstArg.parameters?.[0];
|
|
128
|
+
propsType = textOfType(firstParam?.type);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
components.push({
|
|
133
|
+
name,
|
|
134
|
+
exportType: 'named',
|
|
135
|
+
file: rel(root, f),
|
|
136
|
+
propsType,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (ts.isExportAssignment(stmt)) {
|
|
143
|
+
const expr = stmt.expression;
|
|
144
|
+
const name = ts.isIdentifier(expr) ? expr.text : path.basename(f, path.extname(f));
|
|
145
|
+
components.push({ name, exportType: 'default', file: rel(root, f) });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
root,
|
|
153
|
+
filesScanned: files.length,
|
|
154
|
+
components,
|
|
155
|
+
warnings,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function scanWithRegexFallback(root: string, files: string[]): CatalogResult {
|
|
160
|
+
const components: ComponentExport[] = [];
|
|
161
|
+
const warnings: string[] = ['TypeScript compiler API unavailable; using regex fallback (less accurate).'];
|
|
162
|
+
|
|
163
|
+
for (const f of files) {
|
|
164
|
+
const content = readFileSafe(f);
|
|
165
|
+
if (content == null) continue;
|
|
166
|
+
|
|
167
|
+
// export function Foo(...)
|
|
168
|
+
const fnRe = /export\s+(default\s+)?function\s+([A-Za-z0-9_]+)/g;
|
|
169
|
+
let m: RegExpExecArray | null;
|
|
170
|
+
while ((m = fnRe.exec(content))) {
|
|
171
|
+
components.push({
|
|
172
|
+
name: m[2],
|
|
173
|
+
exportType: m[1] ? 'default' : 'named',
|
|
174
|
+
file: rel(root, f),
|
|
175
|
+
jsDoc: extractJSDocFromLeadingComment(content.slice(0, m.index)),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// export const Foo = (...)
|
|
180
|
+
const constRe = /export\s+const\s+([A-Za-z0-9_]+)/g;
|
|
181
|
+
while ((m = constRe.exec(content))) {
|
|
182
|
+
components.push({
|
|
183
|
+
name: m[1],
|
|
184
|
+
exportType: 'named',
|
|
185
|
+
file: rel(root, f),
|
|
186
|
+
jsDoc: extractJSDocFromLeadingComment(content.slice(0, m.index)),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// export default Identifier
|
|
191
|
+
const defRe = /export\s+default\s+([A-Za-z0-9_]+)/g;
|
|
192
|
+
while ((m = defRe.exec(content))) {
|
|
193
|
+
components.push({
|
|
194
|
+
name: m[1],
|
|
195
|
+
exportType: 'default',
|
|
196
|
+
file: rel(root, f),
|
|
197
|
+
jsDoc: extractJSDocFromLeadingComment(content.slice(0, m.index)),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { root, filesScanned: files.length, components, warnings };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function buildComponentCatalog(root: string, files: string[]): Promise<CatalogResult> {
|
|
206
|
+
try {
|
|
207
|
+
return await scanWithTypeScript(root, files);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const msg = error instanceof Error ? error.message : 'unknown error';
|
|
210
|
+
const fallback = scanWithRegexFallback(root, files);
|
|
211
|
+
fallback.warnings.push(`TypeScript scan failed: ${msg}`);
|
|
212
|
+
return fallback;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration System
|
|
3
|
+
*
|
|
4
|
+
* Loads config from environment variables, config file, and CLI args.
|
|
5
|
+
* Priority: CLI > ENV > config file > defaults
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const ConfigSchema = z.object({
|
|
13
|
+
// Mode: 'local' uses direct Gemini API, 'remote' uses gateway
|
|
14
|
+
mode: z.enum(['local', 'remote']).default('local'),
|
|
15
|
+
|
|
16
|
+
// Local provider: direct Gemini SDK, or OpenAI-compatible proxy (e.g. LiteLLM)
|
|
17
|
+
// Default to LiteLLM because it unlocks routing, retries, caching, and load balancing.
|
|
18
|
+
// If LiteLLM is not configured, we will automatically fall back to direct Gemini.
|
|
19
|
+
localProvider: z.enum(['gemini', 'litellm']).default('litellm'),
|
|
20
|
+
|
|
21
|
+
// API key for local mode (required if mode is 'local')
|
|
22
|
+
apiKey: z.string().optional(),
|
|
23
|
+
|
|
24
|
+
// Remote gateway endpoint (required if mode is 'remote')
|
|
25
|
+
remoteEndpoint: z.string().url().optional(),
|
|
26
|
+
|
|
27
|
+
// Remote API key for gateway authentication
|
|
28
|
+
remoteApiKey: z.string().optional(),
|
|
29
|
+
|
|
30
|
+
// Workspace paths that can be accessed
|
|
31
|
+
allowedPaths: z.array(z.string()).default([process.cwd()]),
|
|
32
|
+
|
|
33
|
+
// Default framework for generated code
|
|
34
|
+
defaultFramework: z
|
|
35
|
+
.enum(['vanilla', 'react', 'vue', 'svelte', 'nextjs'])
|
|
36
|
+
.default('react'),
|
|
37
|
+
|
|
38
|
+
// Gemini model to use (for local mode)
|
|
39
|
+
model: z.string().default('gemini-2.5-flash-lite'),
|
|
40
|
+
|
|
41
|
+
// LiteLLM (or other OpenAI-compatible proxy) configuration for local mode
|
|
42
|
+
litellmEndpoint: z.string().url().optional(),
|
|
43
|
+
litellmApiKey: z.string().optional(),
|
|
44
|
+
litellmModel: z.string().optional(),
|
|
45
|
+
|
|
46
|
+
// Default accessibility level
|
|
47
|
+
accessibility: z.enum(['none', 'wcag-a', 'wcag-aa', 'wcag-aaa']).default('wcag-aa'),
|
|
48
|
+
|
|
49
|
+
// Responsive breakpoints
|
|
50
|
+
breakpoints: z
|
|
51
|
+
.record(z.number())
|
|
52
|
+
.default({
|
|
53
|
+
sm: 640,
|
|
54
|
+
md: 768,
|
|
55
|
+
lg: 1024,
|
|
56
|
+
xl: 1280,
|
|
57
|
+
'2xl': 1536,
|
|
58
|
+
}),
|
|
59
|
+
|
|
60
|
+
// Debug mode
|
|
61
|
+
debug: z.boolean().default(false),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Load configuration from all sources
|
|
68
|
+
*/
|
|
69
|
+
export function loadConfig(): Config {
|
|
70
|
+
const configFromFile = loadConfigFile();
|
|
71
|
+
const configFromEnv = loadConfigFromEnv();
|
|
72
|
+
const configFromArgs = loadConfigFromArgs();
|
|
73
|
+
|
|
74
|
+
// Merge configs with priority: args > env > file > defaults
|
|
75
|
+
const merged = {
|
|
76
|
+
...configFromFile,
|
|
77
|
+
...configFromEnv,
|
|
78
|
+
...configFromArgs,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const result = ConfigSchema.safeParse(merged);
|
|
82
|
+
|
|
83
|
+
if (!result.success) {
|
|
84
|
+
console.error('[config] Invalid configuration:', result.error.format());
|
|
85
|
+
throw new Error('Invalid configuration');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Apply smart defaults / fallbacks
|
|
89
|
+
// - We default to localProvider=litellm, but automatically fall back to direct Gemini
|
|
90
|
+
// when LiteLLM isn't configured.
|
|
91
|
+
let cfg: Config = result.data;
|
|
92
|
+
|
|
93
|
+
if (cfg.mode === 'local') {
|
|
94
|
+
const hasLite = Boolean(cfg.litellmEndpoint);
|
|
95
|
+
const hasGemini = Boolean(cfg.apiKey);
|
|
96
|
+
|
|
97
|
+
if (cfg.localProvider === 'litellm' && !hasLite) {
|
|
98
|
+
if (hasGemini) {
|
|
99
|
+
if (cfg.debug) {
|
|
100
|
+
console.error('[config] LITELLM_ENDPOINT missing; falling back to localProvider=gemini');
|
|
101
|
+
}
|
|
102
|
+
cfg = { ...cfg, localProvider: 'gemini' };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (cfg.localProvider === 'gemini' && !hasGemini) {
|
|
107
|
+
if (hasLite) {
|
|
108
|
+
if (cfg.debug) {
|
|
109
|
+
console.error('[config] GEMINI_API_KEY missing; falling back to localProvider=litellm');
|
|
110
|
+
}
|
|
111
|
+
cfg = { ...cfg, localProvider: 'litellm' };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Validate mode-specific requirements after applying fallbacks
|
|
116
|
+
if (cfg.localProvider === 'gemini' && !cfg.apiKey) {
|
|
117
|
+
console.error('[config] Local mode (gemini) requires GEMINI_API_KEY environment variable');
|
|
118
|
+
throw new Error('Missing Gemini API key for local mode');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (cfg.localProvider === 'litellm' && !cfg.litellmEndpoint) {
|
|
122
|
+
console.error('[config] Local mode (litellm) requires LITELLM_ENDPOINT');
|
|
123
|
+
throw new Error('Missing LiteLLM endpoint for local mode');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (cfg.mode === 'remote' && !cfg.remoteEndpoint) {
|
|
128
|
+
console.error('[config] Remote mode requires GEMINI_DESIGNER_REMOTE_ENDPOINT');
|
|
129
|
+
throw new Error('Missing remote endpoint for remote mode');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return cfg;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Load from .gemini-designer.json config file
|
|
137
|
+
*/
|
|
138
|
+
function loadConfigFile(): Partial<Config> {
|
|
139
|
+
const configPath = path.join(process.cwd(), '.gemini-designer.json');
|
|
140
|
+
|
|
141
|
+
if (!fs.existsSync(configPath)) {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
147
|
+
return JSON.parse(content);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('[config] Error reading config file:', error);
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Load from environment variables
|
|
156
|
+
*/
|
|
157
|
+
function loadConfigFromEnv(): Partial<Config> {
|
|
158
|
+
const config: Partial<Config> = {};
|
|
159
|
+
|
|
160
|
+
if (process.env.GEMINI_API_KEY) {
|
|
161
|
+
config.apiKey = process.env.GEMINI_API_KEY;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (process.env.GEMINI_DESIGNER_MODE) {
|
|
165
|
+
config.mode = process.env.GEMINI_DESIGNER_MODE as 'local' | 'remote';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (process.env.GEMINI_DESIGNER_LOCAL_PROVIDER) {
|
|
169
|
+
config.localProvider = process.env.GEMINI_DESIGNER_LOCAL_PROVIDER as Config['localProvider'];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (process.env.GEMINI_DESIGNER_REMOTE_ENDPOINT) {
|
|
173
|
+
config.remoteEndpoint = process.env.GEMINI_DESIGNER_REMOTE_ENDPOINT;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (process.env.GEMINI_DESIGNER_REMOTE_API_KEY) {
|
|
177
|
+
config.remoteApiKey = process.env.GEMINI_DESIGNER_REMOTE_API_KEY;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (process.env.GEMINI_DESIGNER_DEBUG === 'true') {
|
|
181
|
+
config.debug = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (process.env.GEMINI_DESIGNER_FRAMEWORK) {
|
|
185
|
+
config.defaultFramework = process.env.GEMINI_DESIGNER_FRAMEWORK as Config['defaultFramework'];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (process.env.GEMINI_MODEL) {
|
|
189
|
+
config.model = process.env.GEMINI_MODEL;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (process.env.LITELLM_ENDPOINT) {
|
|
193
|
+
config.litellmEndpoint = process.env.LITELLM_ENDPOINT;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (process.env.LITELLM_API_KEY) {
|
|
197
|
+
config.litellmApiKey = process.env.LITELLM_API_KEY;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (process.env.LITELLM_MODEL) {
|
|
201
|
+
config.litellmModel = process.env.LITELLM_MODEL;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return config;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Load from CLI arguments
|
|
209
|
+
*/
|
|
210
|
+
function loadConfigFromArgs(): Partial<Config> {
|
|
211
|
+
const args = process.argv.slice(2);
|
|
212
|
+
const config: Partial<Config> = {};
|
|
213
|
+
|
|
214
|
+
for (let i = 0; i < args.length; i++) {
|
|
215
|
+
const arg = args[i];
|
|
216
|
+
|
|
217
|
+
if (arg === '--local') {
|
|
218
|
+
config.mode = 'local';
|
|
219
|
+
} else if (arg === '--remote') {
|
|
220
|
+
config.mode = 'remote';
|
|
221
|
+
} else if (arg === '--provider' && args[i + 1]) {
|
|
222
|
+
config.localProvider = args[++i] as Config['localProvider'];
|
|
223
|
+
} else if (arg === '--debug') {
|
|
224
|
+
config.debug = true;
|
|
225
|
+
} else if (arg === '--api-key' && args[i + 1]) {
|
|
226
|
+
config.apiKey = args[++i];
|
|
227
|
+
} else if (arg === '--litellm-endpoint' && args[i + 1]) {
|
|
228
|
+
config.litellmEndpoint = args[++i];
|
|
229
|
+
} else if (arg === '--litellm-model' && args[i + 1]) {
|
|
230
|
+
config.litellmModel = args[++i];
|
|
231
|
+
} else if (arg === '--endpoint' && args[i + 1]) {
|
|
232
|
+
config.remoteEndpoint = args[++i];
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return config;
|
|
237
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Builder
|
|
3
|
+
*
|
|
4
|
+
* Builds token-optimized context from specified files.
|
|
5
|
+
* Filters out sensitive content and optimizes for relevance.
|
|
6
|
+
* Includes token counting for quota management.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import { Config } from '../config/index.js';
|
|
12
|
+
import { isPathAllowed, isSensitiveFile, sanitizeContent } from './filter.js';
|
|
13
|
+
|
|
14
|
+
// Token estimation: ~4 characters per token for English text/code
|
|
15
|
+
const CHARS_PER_TOKEN = 4;
|
|
16
|
+
const MAX_CONTEXT_TOKENS = 12500; // ~50k chars
|
|
17
|
+
const MAX_FILE_TOKENS = 2500; // ~10k chars per file
|
|
18
|
+
|
|
19
|
+
export interface ContextResult {
|
|
20
|
+
content: string;
|
|
21
|
+
estimatedTokens: number;
|
|
22
|
+
filesIncluded: string[];
|
|
23
|
+
filesSkipped: string[];
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Estimate token count from text
|
|
29
|
+
*/
|
|
30
|
+
export function estimateTokens(text: string): number {
|
|
31
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build context from specified file paths with token optimization
|
|
36
|
+
*/
|
|
37
|
+
export async function buildContext(paths: string[], config: Config): Promise<string> {
|
|
38
|
+
const result = await buildContextWithMetadata(paths, config);
|
|
39
|
+
return result.content;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build context with full metadata (tokens, files included, etc.)
|
|
44
|
+
*/
|
|
45
|
+
export async function buildContextWithMetadata(
|
|
46
|
+
paths: string[],
|
|
47
|
+
config: Config
|
|
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
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check if file exists and is a file
|
|
81
|
+
if (!fs.existsSync(absPath)) {
|
|
82
|
+
filesSkipped.push(`${filePath} (not found)`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const stat = fs.statSync(absPath);
|
|
87
|
+
if (!stat.isFile()) {
|
|
88
|
+
filesSkipped.push(`${filePath} (not a file)`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
let content = fs.readFileSync(absPath, 'utf-8');
|
|
94
|
+
|
|
95
|
+
// Sanitize content to remove any secrets
|
|
96
|
+
content = sanitizeContent(content);
|
|
97
|
+
|
|
98
|
+
// Calculate tokens for this file
|
|
99
|
+
let fileTokens = estimateTokens(content);
|
|
100
|
+
const maxFileChars = MAX_FILE_TOKENS * CHARS_PER_TOKEN;
|
|
101
|
+
|
|
102
|
+
// Truncate large files
|
|
103
|
+
if (fileTokens > MAX_FILE_TOKENS) {
|
|
104
|
+
content = smartTruncate(content, maxFileChars);
|
|
105
|
+
fileTokens = MAX_FILE_TOKENS;
|
|
106
|
+
}
|
|
107
|
+
|
|
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
|
+
}
|
|
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)`);
|
|
127
|
+
}
|
|
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
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Sort paths by relevance (design tokens and variables first)
|
|
141
|
+
*/
|
|
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
|
+
}
|
|
165
|
+
|
|
166
|
+
return aPriority - bPriority;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Smart truncate: keep beginning and end, with clear indicator
|
|
172
|
+
*/
|
|
173
|
+
function smartTruncate(content: string, maxChars: number): string {
|
|
174
|
+
if (content.length <= maxChars) return content;
|
|
175
|
+
|
|
176
|
+
const keepStart = Math.floor(maxChars * 0.7);
|
|
177
|
+
const keepEnd = Math.floor(maxChars * 0.2);
|
|
178
|
+
|
|
179
|
+
const start = content.slice(0, keepStart);
|
|
180
|
+
const end = content.slice(-keepEnd);
|
|
181
|
+
|
|
182
|
+
return `${start}\n\n/* ... [${Math.round((content.length - maxChars) / 1000)}k chars truncated] ... */\n\n${end}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Automatically discover relevant UI files in a directory
|
|
187
|
+
*/
|
|
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
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// Skip directories we can't read
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
scan(directory);
|
|
230
|
+
|
|
231
|
+
// Sort by relevance
|
|
232
|
+
return sortByRelevance(files);
|
|
233
|
+
}
|