@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.
Files changed (153) hide show
  1. package/.prettierrc +9 -0
  2. package/dist/components/catalog.d.ts +24 -0
  3. package/dist/components/catalog.d.ts.map +1 -0
  4. package/dist/components/catalog.js +186 -0
  5. package/dist/components/catalog.js.map +1 -0
  6. package/dist/config/index.d.ts +60 -0
  7. package/dist/config/index.d.ts.map +1 -0
  8. package/dist/config/index.js +199 -0
  9. package/dist/config/index.js.map +1 -0
  10. package/dist/context/builder.d.ts +32 -0
  11. package/dist/context/builder.d.ts.map +1 -0
  12. package/dist/context/builder.js +194 -0
  13. package/dist/context/builder.js.map +1 -0
  14. package/dist/context/filter.d.ts +28 -0
  15. package/dist/context/filter.d.ts.map +1 -0
  16. package/dist/context/filter.js +136 -0
  17. package/dist/context/filter.js.map +1 -0
  18. package/dist/context/grounding.d.ts +27 -0
  19. package/dist/context/grounding.d.ts.map +1 -0
  20. package/dist/context/grounding.js +162 -0
  21. package/dist/context/grounding.js.map +1 -0
  22. package/dist/context/guards.d.ts +31 -0
  23. package/dist/context/guards.d.ts.map +1 -0
  24. package/dist/context/guards.js +76 -0
  25. package/dist/context/guards.js.map +1 -0
  26. package/dist/context/repo-hints.d.ts +12 -0
  27. package/dist/context/repo-hints.d.ts.map +1 -0
  28. package/dist/context/repo-hints.js +40 -0
  29. package/dist/context/repo-hints.js.map +1 -0
  30. package/dist/generation/gemini-client.d.ts +27 -0
  31. package/dist/generation/gemini-client.d.ts.map +1 -0
  32. package/dist/generation/gemini-client.js +64 -0
  33. package/dist/generation/gemini-client.js.map +1 -0
  34. package/dist/generation/litellm-client.d.ts +16 -0
  35. package/dist/generation/litellm-client.d.ts.map +1 -0
  36. package/dist/generation/litellm-client.js +98 -0
  37. package/dist/generation/litellm-client.js.map +1 -0
  38. package/dist/generation/remote-client.d.ts +20 -0
  39. package/dist/generation/remote-client.d.ts.map +1 -0
  40. package/dist/generation/remote-client.js +69 -0
  41. package/dist/generation/remote-client.js.map +1 -0
  42. package/dist/index.d.ts +9 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +30 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/output/file-writer.d.ts +39 -0
  47. package/dist/output/file-writer.d.ts.map +1 -0
  48. package/dist/output/file-writer.js +153 -0
  49. package/dist/output/file-writer.js.map +1 -0
  50. package/dist/output/formatter.d.ts +26 -0
  51. package/dist/output/formatter.d.ts.map +1 -0
  52. package/dist/output/formatter.js +156 -0
  53. package/dist/output/formatter.js.map +1 -0
  54. package/dist/server.d.ts +9 -0
  55. package/dist/server.d.ts.map +1 -0
  56. package/dist/server.js +22 -0
  57. package/dist/server.js.map +1 -0
  58. package/dist/stack/detect.d.ts +49 -0
  59. package/dist/stack/detect.d.ts.map +1 -0
  60. package/dist/stack/detect.js +157 -0
  61. package/dist/stack/detect.js.map +1 -0
  62. package/dist/tokens/sync.d.ts +32 -0
  63. package/dist/tokens/sync.d.ts.map +1 -0
  64. package/dist/tokens/sync.js +188 -0
  65. package/dist/tokens/sync.js.map +1 -0
  66. package/dist/tools/analyze-screenshot-ui.d.ts +18 -0
  67. package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -0
  68. package/dist/tools/analyze-screenshot-ui.js +133 -0
  69. package/dist/tools/analyze-screenshot-ui.js.map +1 -0
  70. package/dist/tools/analyze-tokens.d.ts +10 -0
  71. package/dist/tools/analyze-tokens.d.ts.map +1 -0
  72. package/dist/tools/analyze-tokens.js +107 -0
  73. package/dist/tools/analyze-tokens.js.map +1 -0
  74. package/dist/tools/catalog-components.d.ts +14 -0
  75. package/dist/tools/catalog-components.d.ts.map +1 -0
  76. package/dist/tools/catalog-components.js +85 -0
  77. package/dist/tools/catalog-components.js.map +1 -0
  78. package/dist/tools/create-ui.d.ts +10 -0
  79. package/dist/tools/create-ui.d.ts.map +1 -0
  80. package/dist/tools/create-ui.js +167 -0
  81. package/dist/tools/create-ui.js.map +1 -0
  82. package/dist/tools/detect-ui-stack.d.ts +15 -0
  83. package/dist/tools/detect-ui-stack.d.ts.map +1 -0
  84. package/dist/tools/detect-ui-stack.js +52 -0
  85. package/dist/tools/detect-ui-stack.js.map +1 -0
  86. package/dist/tools/generate-component-variants.d.ts +15 -0
  87. package/dist/tools/generate-component-variants.d.ts.map +1 -0
  88. package/dist/tools/generate-component-variants.js +199 -0
  89. package/dist/tools/generate-component-variants.js.map +1 -0
  90. package/dist/tools/generate-vibes.d.ts +10 -0
  91. package/dist/tools/generate-vibes.d.ts.map +1 -0
  92. package/dist/tools/generate-vibes.js +145 -0
  93. package/dist/tools/generate-vibes.js.map +1 -0
  94. package/dist/tools/index.d.ts +12 -0
  95. package/dist/tools/index.d.ts.map +1 -0
  96. package/dist/tools/index.js +36 -0
  97. package/dist/tools/index.js.map +1 -0
  98. package/dist/tools/modify-ui.d.ts +11 -0
  99. package/dist/tools/modify-ui.d.ts.map +1 -0
  100. package/dist/tools/modify-ui.js +207 -0
  101. package/dist/tools/modify-ui.js.map +1 -0
  102. package/dist/tools/scaffold-project.d.ts +10 -0
  103. package/dist/tools/scaffold-project.d.ts.map +1 -0
  104. package/dist/tools/scaffold-project.js +122 -0
  105. package/dist/tools/scaffold-project.js.map +1 -0
  106. package/dist/tools/snippet-ui.d.ts +11 -0
  107. package/dist/tools/snippet-ui.d.ts.map +1 -0
  108. package/dist/tools/snippet-ui.js +194 -0
  109. package/dist/tools/snippet-ui.js.map +1 -0
  110. package/dist/tools/sync-design-tokens.d.ts +14 -0
  111. package/dist/tools/sync-design-tokens.d.ts.map +1 -0
  112. package/dist/tools/sync-design-tokens.js +233 -0
  113. package/dist/tools/sync-design-tokens.js.map +1 -0
  114. package/dist/utils/walk.d.ts +15 -0
  115. package/dist/utils/walk.d.ts.map +1 -0
  116. package/dist/utils/walk.js +63 -0
  117. package/dist/utils/walk.js.map +1 -0
  118. package/eslint.config.js +37 -0
  119. package/package.json +56 -0
  120. package/src/__tests__/builder.test.ts +31 -0
  121. package/src/__tests__/config.test.ts +52 -0
  122. package/src/__tests__/filter.test.ts +109 -0
  123. package/src/components/catalog.ts +214 -0
  124. package/src/config/index.ts +237 -0
  125. package/src/context/builder.ts +233 -0
  126. package/src/context/filter.ts +164 -0
  127. package/src/context/grounding.ts +191 -0
  128. package/src/context/guards.ts +94 -0
  129. package/src/context/repo-hints.ts +43 -0
  130. package/src/generation/gemini-client.ts +94 -0
  131. package/src/generation/litellm-client.ts +121 -0
  132. package/src/generation/remote-client.ts +103 -0
  133. package/src/index.ts +36 -0
  134. package/src/output/file-writer.ts +181 -0
  135. package/src/output/formatter.ts +186 -0
  136. package/src/server.ts +28 -0
  137. package/src/stack/detect.ts +204 -0
  138. package/src/tokens/sync.ts +212 -0
  139. package/src/tools/analyze-screenshot-ui.ts +150 -0
  140. package/src/tools/analyze-tokens.ts +123 -0
  141. package/src/tools/catalog-components.ts +99 -0
  142. package/src/tools/create-ui.ts +194 -0
  143. package/src/tools/detect-ui-stack.ts +64 -0
  144. package/src/tools/generate-component-variants.ts +218 -0
  145. package/src/tools/generate-vibes.ts +177 -0
  146. package/src/tools/index.ts +42 -0
  147. package/src/tools/modify-ui.ts +230 -0
  148. package/src/tools/scaffold-project.ts +138 -0
  149. package/src/tools/snippet-ui.ts +222 -0
  150. package/src/tools/sync-design-tokens.ts +256 -0
  151. package/src/utils/walk.ts +75 -0
  152. package/tsconfig.json +34 -0
  153. 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
+ }