gemini-design-mcp 3.5.1 → 3.6.1
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/build/index.js +9 -3
- package/build/lib/filesystem.d.ts +53 -0
- package/build/lib/filesystem.js +145 -0
- package/build/tools/create-frontend.d.ts +2 -0
- package/build/tools/create-frontend.js +44 -2
- package/build/tools/modify-frontend.d.ts +2 -0
- package/build/tools/modify-frontend.js +51 -1
- package/build/tools/snippet-frontend.d.ts +6 -0
- package/build/tools/snippet-frontend.js +73 -1
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -58,7 +58,9 @@ Present options to user, they select one, then pass it here via designSystem.vib
|
|
|
58
58
|
📤 OUTPUT
|
|
59
59
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
60
60
|
|
|
61
|
-
Returns a COMPLETE file ready to save
|
|
61
|
+
Returns a COMPLETE file ready to save.
|
|
62
|
+
|
|
63
|
+
💡 TIP: Use writeFile: true to write directly to disk (faster, lighter context).`, createFrontendSchema, createFrontend);
|
|
62
64
|
// =============================================================================
|
|
63
65
|
// TOOL 2: MODIFY_FRONTEND
|
|
64
66
|
// =============================================================================
|
|
@@ -100,7 +102,9 @@ import { X } from "y";
|
|
|
100
102
|
<exact existing code>
|
|
101
103
|
|
|
102
104
|
// REPLACE WITH:
|
|
103
|
-
<new redesigned code
|
|
105
|
+
<new redesigned code>
|
|
106
|
+
|
|
107
|
+
💡 TIP: Use writeFile: true to apply the modification directly to disk.`, modifyFrontendSchema, modifyFrontend);
|
|
104
108
|
// =============================================================================
|
|
105
109
|
// TOOL 3: SNIPPET_FRONTEND
|
|
106
110
|
// =============================================================================
|
|
@@ -210,7 +214,9 @@ import { Search } from "lucide-react";
|
|
|
210
214
|
You (Claude) are responsible for:
|
|
211
215
|
- Adding the logic (useState, handlers) BEFORE calling this tool
|
|
212
216
|
- Merging new imports
|
|
213
|
-
- Inserting the snippet at the correct location
|
|
217
|
+
- Inserting the snippet at the correct location
|
|
218
|
+
|
|
219
|
+
💡 TIP: Use writeFile: true + insertAtLine or insertAfterPattern to insert directly.`, snippetFrontendSchema, snippetFrontend);
|
|
214
220
|
// =============================================================================
|
|
215
221
|
// TOOL 4: GENERATE_VIBES
|
|
216
222
|
// =============================================================================
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write a file, creating parent directories if they don't exist
|
|
3
|
+
*/
|
|
4
|
+
export declare function writeFileWithDirs(filePath: string, content: string): void;
|
|
5
|
+
/**
|
|
6
|
+
* Read a file if it exists, return null otherwise
|
|
7
|
+
*/
|
|
8
|
+
export declare function readFileIfExists(filePath: string): string | null;
|
|
9
|
+
/**
|
|
10
|
+
* Get file size in human readable format
|
|
11
|
+
*/
|
|
12
|
+
export declare function getFileSize(content: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Parse FIND/REPLACE format from Gemini output
|
|
15
|
+
* Returns null if parsing fails
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseFindReplace(geminiOutput: string): {
|
|
18
|
+
imports?: string;
|
|
19
|
+
find: string;
|
|
20
|
+
replace: string;
|
|
21
|
+
} | null;
|
|
22
|
+
/**
|
|
23
|
+
* Parse SNIPPET format from Gemini output
|
|
24
|
+
* Returns null if parsing fails
|
|
25
|
+
*/
|
|
26
|
+
export declare function parseSnippet(geminiOutput: string): {
|
|
27
|
+
imports?: string;
|
|
28
|
+
snippet: string;
|
|
29
|
+
} | null;
|
|
30
|
+
/**
|
|
31
|
+
* Apply find/replace to file content
|
|
32
|
+
*/
|
|
33
|
+
export declare function applyFindReplace(fileContent: string, find: string, replace: string): {
|
|
34
|
+
success: boolean;
|
|
35
|
+
newContent: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Merge new imports into existing file content
|
|
40
|
+
*/
|
|
41
|
+
export declare function mergeImports(fileContent: string, newImports: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Insert snippet after a specific line number (1-indexed)
|
|
44
|
+
*/
|
|
45
|
+
export declare function insertAfterLine(fileContent: string, snippet: string, lineNumber: number): string;
|
|
46
|
+
/**
|
|
47
|
+
* Insert snippet after a pattern match
|
|
48
|
+
*/
|
|
49
|
+
export declare function insertAfterPattern(fileContent: string, snippet: string, pattern: string): {
|
|
50
|
+
success: boolean;
|
|
51
|
+
newContent: string;
|
|
52
|
+
error?: string;
|
|
53
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Write a file, creating parent directories if they don't exist
|
|
5
|
+
*/
|
|
6
|
+
export function writeFileWithDirs(filePath, content) {
|
|
7
|
+
const dir = path.dirname(filePath);
|
|
8
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
9
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read a file if it exists, return null otherwise
|
|
13
|
+
*/
|
|
14
|
+
export function readFileIfExists(filePath) {
|
|
15
|
+
if (fs.existsSync(filePath)) {
|
|
16
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get file size in human readable format
|
|
22
|
+
*/
|
|
23
|
+
export function getFileSize(content) {
|
|
24
|
+
const bytes = Buffer.byteLength(content, "utf-8");
|
|
25
|
+
if (bytes < 1024)
|
|
26
|
+
return `${bytes} B`;
|
|
27
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse FIND/REPLACE format from Gemini output
|
|
31
|
+
* Returns null if parsing fails
|
|
32
|
+
*/
|
|
33
|
+
export function parseFindReplace(geminiOutput) {
|
|
34
|
+
try {
|
|
35
|
+
let imports;
|
|
36
|
+
// Extract imports if present
|
|
37
|
+
const importsMatch = geminiOutput.match(/\/\/ NEW IMPORTS NEEDED:\s*([\s\S]*?)(?=\/\/ FIND THIS CODE:)/);
|
|
38
|
+
if (importsMatch) {
|
|
39
|
+
imports = importsMatch[1].trim();
|
|
40
|
+
}
|
|
41
|
+
// Extract find section
|
|
42
|
+
const findMatch = geminiOutput.match(/\/\/ FIND THIS CODE:\s*([\s\S]*?)(?=\/\/ REPLACE WITH:)/);
|
|
43
|
+
if (!findMatch)
|
|
44
|
+
return null;
|
|
45
|
+
// Extract replace section
|
|
46
|
+
const replaceMatch = geminiOutput.match(/\/\/ REPLACE WITH:\s*([\s\S]*?)$/);
|
|
47
|
+
if (!replaceMatch)
|
|
48
|
+
return null;
|
|
49
|
+
return {
|
|
50
|
+
imports: imports || undefined,
|
|
51
|
+
find: findMatch[1].trim(),
|
|
52
|
+
replace: replaceMatch[1].trim(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse SNIPPET format from Gemini output
|
|
61
|
+
* Returns null if parsing fails
|
|
62
|
+
*/
|
|
63
|
+
export function parseSnippet(geminiOutput) {
|
|
64
|
+
try {
|
|
65
|
+
let imports;
|
|
66
|
+
// Extract imports if present
|
|
67
|
+
const importsMatch = geminiOutput.match(/\/\/ NEW IMPORTS NEEDED:\s*([\s\S]*?)(?=\/\/ SNIPPET:)/);
|
|
68
|
+
if (importsMatch) {
|
|
69
|
+
imports = importsMatch[1].trim();
|
|
70
|
+
}
|
|
71
|
+
// Extract snippet
|
|
72
|
+
const snippetMatch = geminiOutput.match(/\/\/ SNIPPET:\s*([\s\S]*?)$/);
|
|
73
|
+
if (!snippetMatch) {
|
|
74
|
+
// If no SNIPPET marker, assume the whole thing (minus imports) is the snippet
|
|
75
|
+
return { imports, snippet: geminiOutput.trim() };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
imports: imports || undefined,
|
|
79
|
+
snippet: snippetMatch[1].trim(),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Apply find/replace to file content
|
|
88
|
+
*/
|
|
89
|
+
export function applyFindReplace(fileContent, find, replace) {
|
|
90
|
+
if (!fileContent.includes(find)) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
newContent: fileContent,
|
|
94
|
+
error: "Could not find the target code in file",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const newContent = fileContent.replace(find, replace);
|
|
98
|
+
return { success: true, newContent };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Merge new imports into existing file content
|
|
102
|
+
*/
|
|
103
|
+
export function mergeImports(fileContent, newImports) {
|
|
104
|
+
const lines = fileContent.split("\n");
|
|
105
|
+
// Find the last import line
|
|
106
|
+
let lastImportIndex = -1;
|
|
107
|
+
for (let i = 0; i < lines.length; i++) {
|
|
108
|
+
if (lines[i].startsWith("import ") || lines[i].startsWith("import{")) {
|
|
109
|
+
lastImportIndex = i;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (lastImportIndex === -1) {
|
|
113
|
+
// No imports found, add at the beginning
|
|
114
|
+
return newImports + "\n" + fileContent;
|
|
115
|
+
}
|
|
116
|
+
// Insert after last import
|
|
117
|
+
lines.splice(lastImportIndex + 1, 0, newImports);
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Insert snippet after a specific line number (1-indexed)
|
|
122
|
+
*/
|
|
123
|
+
export function insertAfterLine(fileContent, snippet, lineNumber) {
|
|
124
|
+
const lines = fileContent.split("\n");
|
|
125
|
+
lines.splice(lineNumber, 0, snippet);
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Insert snippet after a pattern match
|
|
130
|
+
*/
|
|
131
|
+
export function insertAfterPattern(fileContent, snippet, pattern) {
|
|
132
|
+
const index = fileContent.indexOf(pattern);
|
|
133
|
+
if (index === -1) {
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
newContent: fileContent,
|
|
137
|
+
error: `Pattern not found: ${pattern}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const endOfPattern = index + pattern.length;
|
|
141
|
+
const newContent = fileContent.slice(0, endOfPattern) +
|
|
142
|
+
"\n" + snippet +
|
|
143
|
+
fileContent.slice(endOfPattern);
|
|
144
|
+
return { success: true, newContent };
|
|
145
|
+
}
|
|
@@ -36,6 +36,7 @@ export declare const createFrontendSchema: {
|
|
|
36
36
|
keywords: string[];
|
|
37
37
|
};
|
|
38
38
|
}>>;
|
|
39
|
+
writeFile: z.ZodOptional<z.ZodBoolean>;
|
|
39
40
|
};
|
|
40
41
|
export declare function createFrontend(params: {
|
|
41
42
|
request: string;
|
|
@@ -50,6 +51,7 @@ export declare function createFrontend(params: {
|
|
|
50
51
|
keywords: string[];
|
|
51
52
|
};
|
|
52
53
|
};
|
|
54
|
+
writeFile?: boolean;
|
|
53
55
|
}): Promise<{
|
|
54
56
|
content: {
|
|
55
57
|
type: "text";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { generateWithGemini } from "../lib/gemini.js";
|
|
3
3
|
import { CREATE_FRONTEND_PROMPT } from "../prompts/system.js";
|
|
4
|
+
import { writeFileWithDirs, getFileSize } from "../lib/filesystem.js";
|
|
4
5
|
export const createFrontendSchema = {
|
|
5
6
|
request: z.string().describe("What to create: describe the page, component, or section. " +
|
|
6
7
|
"Be specific about functionality and content. " +
|
|
@@ -21,24 +22,53 @@ export const createFrontendSchema = {
|
|
|
21
22
|
}).describe("The selected design vibe"),
|
|
22
23
|
}).optional().describe("Design system with selected vibe. REQUIRED for new projects without existing design. " +
|
|
23
24
|
"Call generate_vibes first, user selects, then pass the selection here."),
|
|
25
|
+
writeFile: z.boolean().optional().describe("If true, write the file directly to disk instead of returning the code. " +
|
|
26
|
+
"Returns a confirmation message instead of the full code, keeping context lightweight."),
|
|
24
27
|
};
|
|
25
28
|
export async function createFrontend(params) {
|
|
26
|
-
const { request, filePath, techStack, context, designSystem } = params;
|
|
29
|
+
const { request, filePath, techStack, context, designSystem, writeFile } = params;
|
|
27
30
|
// Build design system instructions if provided
|
|
28
31
|
let designSystemInstructions = '';
|
|
29
32
|
if (designSystem?.vibe) {
|
|
30
33
|
const { vibe } = designSystem;
|
|
31
34
|
const scale = vibe.scale || "balanced";
|
|
35
|
+
// Conceptual scale descriptions - let Gemini decide exact values
|
|
36
|
+
const scaleDescriptions = {
|
|
37
|
+
refined: `**SCALE: REFINED**
|
|
38
|
+
This is about ELEMENT SIZE, not density. Refined means:
|
|
39
|
+
- Smaller, more elegant typography — avoid oversized headlines
|
|
40
|
+
- Compact, contained buttons — not chunky or oversized
|
|
41
|
+
- Cards and containers with constrained widths — not full-width sprawling layouts
|
|
42
|
+
- Tighter, purposeful spacing between elements
|
|
43
|
+
- Smaller icons that complement rather than dominate
|
|
44
|
+
|
|
45
|
+
Reference: Linear.app, Raycast, Notion — these apps feel sophisticated because their elements are proportionally small and refined, never billboard-sized or bloated.
|
|
46
|
+
|
|
47
|
+
The interface should feel like a premium Swiss watch: precise, elegant, where every element is intentionally sized smaller than the default.`,
|
|
48
|
+
balanced: `**SCALE: BALANCED**
|
|
49
|
+
Standard, conventional sizing. Use typical defaults — nothing particularly large or small. A middle-ground that works for most general-purpose applications.`,
|
|
50
|
+
zoomed: `**SCALE: ZOOMED**
|
|
51
|
+
This is about ELEMENT SIZE, not density. Zoomed means:
|
|
52
|
+
- Large, prominent typography — bold headlines that command attention
|
|
53
|
+
- Big, chunky buttons — easy to see and click/tap
|
|
54
|
+
- Wide cards and containers — full-width or near full-width layouts
|
|
55
|
+
- Generous spacing and padding throughout
|
|
56
|
+
- Large icons that stand out
|
|
57
|
+
|
|
58
|
+
Reference: Kids apps, accessibility-focused interfaces, bold marketing sites — elements are intentionally oversized for impact and ease of use.`
|
|
59
|
+
};
|
|
32
60
|
designSystemInstructions = `
|
|
33
61
|
MANDATORY DESIGN SYSTEM (User Selected Vibe):
|
|
34
62
|
|
|
35
63
|
**Design Vibe: "${vibe.name}"**
|
|
36
64
|
${vibe.description}
|
|
37
65
|
|
|
38
|
-
|
|
66
|
+
${scaleDescriptions[scale] || scaleDescriptions.balanced}
|
|
67
|
+
|
|
39
68
|
Keywords: ${vibe.keywords.join(', ')}
|
|
40
69
|
|
|
41
70
|
Interpret this vibe creatively — choose colors, typography, and styling that embody this atmosphere.
|
|
71
|
+
The scale above is MANDATORY and defines how large or small UI elements should be.
|
|
42
72
|
`;
|
|
43
73
|
}
|
|
44
74
|
// Build context instructions (ignore empty strings and "null" strings)
|
|
@@ -64,6 +94,18 @@ FILE PATH: ${filePath}
|
|
|
64
94
|
|
|
65
95
|
Remember: Return a COMPLETE file ready to save at ${filePath}`.trim();
|
|
66
96
|
const result = await generateWithGemini(systemPrompt, request, undefined, "high");
|
|
97
|
+
// Write file directly if requested
|
|
98
|
+
if (writeFile) {
|
|
99
|
+
writeFileWithDirs(filePath, result);
|
|
100
|
+
const size = getFileSize(result);
|
|
101
|
+
return {
|
|
102
|
+
content: [{
|
|
103
|
+
type: "text",
|
|
104
|
+
text: `✅ Created: ${filePath} (${size})`,
|
|
105
|
+
}],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Default: return the code
|
|
67
109
|
return {
|
|
68
110
|
content: [{ type: "text", text: result }],
|
|
69
111
|
};
|
|
@@ -4,12 +4,14 @@ export declare const modifyFrontendSchema: {
|
|
|
4
4
|
targetCode: z.ZodString;
|
|
5
5
|
filePath: z.ZodString;
|
|
6
6
|
context: z.ZodOptional<z.ZodString>;
|
|
7
|
+
writeFile: z.ZodOptional<z.ZodBoolean>;
|
|
7
8
|
};
|
|
8
9
|
export declare function modifyFrontend(params: {
|
|
9
10
|
modification: string;
|
|
10
11
|
targetCode: string;
|
|
11
12
|
filePath: string;
|
|
12
13
|
context?: string;
|
|
14
|
+
writeFile?: boolean;
|
|
13
15
|
}): Promise<{
|
|
14
16
|
content: {
|
|
15
17
|
type: "text";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { generateWithGemini } from "../lib/gemini.js";
|
|
3
3
|
import { MODIFY_FRONTEND_PROMPT } from "../prompts/system.js";
|
|
4
|
+
import { readFileIfExists, writeFileWithDirs, parseFindReplace, applyFindReplace, mergeImports, } from "../lib/filesystem.js";
|
|
4
5
|
export const modifyFrontendSchema = {
|
|
5
6
|
modification: z.string().describe("The SINGLE design modification to make. Be specific. " +
|
|
6
7
|
"Examples: " +
|
|
@@ -14,9 +15,12 @@ export const modifyFrontendSchema = {
|
|
|
14
15
|
"Example: 'src/components/Sidebar.tsx'"),
|
|
15
16
|
context: z.string().optional().describe("Additional context about the file's design system. " +
|
|
16
17
|
"Example: 'Uses Tailwind, dark theme with zinc colors, rounded-xl borders'"),
|
|
18
|
+
writeFile: z.boolean().optional().describe("If true, apply the modification directly to the file on disk. " +
|
|
19
|
+
"Reads the file, applies find/replace, and writes back. " +
|
|
20
|
+
"Returns a confirmation instead of the find/replace instructions."),
|
|
17
21
|
};
|
|
18
22
|
export async function modifyFrontend(params) {
|
|
19
|
-
const { modification, targetCode, filePath, context } = params;
|
|
23
|
+
const { modification, targetCode, filePath, context, writeFile } = params;
|
|
20
24
|
// Build context instructions
|
|
21
25
|
let contextInstructions = '';
|
|
22
26
|
if (context) {
|
|
@@ -38,6 +42,52 @@ MODIFICATION REQUESTED: ${modification}
|
|
|
38
42
|
|
|
39
43
|
Remember: Return ONLY the find/replace block. ONE modification, surgical precision.`.trim();
|
|
40
44
|
const result = await generateWithGemini(systemPrompt, modification, undefined, "minimal");
|
|
45
|
+
// Apply modification directly if requested
|
|
46
|
+
if (writeFile) {
|
|
47
|
+
const fileContent = readFileIfExists(filePath);
|
|
48
|
+
if (!fileContent) {
|
|
49
|
+
return {
|
|
50
|
+
content: [{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: `❌ Error: File not found: ${filePath}`,
|
|
53
|
+
}],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const parsed = parseFindReplace(result);
|
|
57
|
+
if (!parsed) {
|
|
58
|
+
// Fallback: return raw result if parsing fails
|
|
59
|
+
return {
|
|
60
|
+
content: [{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: `⚠️ Could not parse Gemini output. Raw result:\n\n${result}`,
|
|
63
|
+
}],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Apply the find/replace
|
|
67
|
+
const { success, newContent, error } = applyFindReplace(fileContent, parsed.find, parsed.replace);
|
|
68
|
+
if (!success) {
|
|
69
|
+
return {
|
|
70
|
+
content: [{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: `❌ Error: ${error}\n\nExpected to find:\n${parsed.find.slice(0, 200)}...`,
|
|
73
|
+
}],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Merge imports if any
|
|
77
|
+
let finalContent = newContent;
|
|
78
|
+
if (parsed.imports) {
|
|
79
|
+
finalContent = mergeImports(newContent, parsed.imports);
|
|
80
|
+
}
|
|
81
|
+
// Write the file
|
|
82
|
+
writeFileWithDirs(filePath, finalContent);
|
|
83
|
+
return {
|
|
84
|
+
content: [{
|
|
85
|
+
type: "text",
|
|
86
|
+
text: `✅ Modified: ${filePath}${parsed.imports ? `\n Added imports` : ""}`,
|
|
87
|
+
}],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// Default: return the find/replace instructions
|
|
41
91
|
return {
|
|
42
92
|
content: [{ type: "text", text: result }],
|
|
43
93
|
};
|
|
@@ -5,6 +5,9 @@ export declare const snippetFrontendSchema: {
|
|
|
5
5
|
techStack: z.ZodString;
|
|
6
6
|
insertionContext: z.ZodString;
|
|
7
7
|
context: z.ZodOptional<z.ZodString>;
|
|
8
|
+
writeFile: z.ZodOptional<z.ZodBoolean>;
|
|
9
|
+
insertAtLine: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
insertAfterPattern: z.ZodOptional<z.ZodString>;
|
|
8
11
|
};
|
|
9
12
|
export declare function snippetFrontend(params: {
|
|
10
13
|
request: string;
|
|
@@ -12,6 +15,9 @@ export declare function snippetFrontend(params: {
|
|
|
12
15
|
techStack: string;
|
|
13
16
|
insertionContext: string;
|
|
14
17
|
context?: string;
|
|
18
|
+
writeFile?: boolean;
|
|
19
|
+
insertAtLine?: number;
|
|
20
|
+
insertAfterPattern?: string;
|
|
15
21
|
}): Promise<{
|
|
16
22
|
content: {
|
|
17
23
|
type: "text";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { generateWithGemini } from "../lib/gemini.js";
|
|
3
3
|
import { SNIPPET_FRONTEND_PROMPT } from "../prompts/system.js";
|
|
4
|
+
import { readFileIfExists, writeFileWithDirs, parseSnippet, mergeImports, insertAfterLine, insertAfterPattern, } from "../lib/filesystem.js";
|
|
4
5
|
export const snippetFrontendSchema = {
|
|
5
6
|
request: z.string().describe("What code snippet to generate. Be specific about what you need. " +
|
|
6
7
|
"Examples: " +
|
|
@@ -19,9 +20,17 @@ export const snippetFrontendSchema = {
|
|
|
19
20
|
context: z.string().optional().describe("Additional project files for design/pattern reference. " +
|
|
20
21
|
"Pass components, styles, or utilities that the snippet should match. " +
|
|
21
22
|
"Optional but recommended for consistency."),
|
|
23
|
+
writeFile: z.boolean().optional().describe("If true, insert the snippet directly into the file on disk. " +
|
|
24
|
+
"Requires insertAtLine OR insertAfterPattern to know where to insert."),
|
|
25
|
+
insertAtLine: z.number().optional().describe("Line number (1-indexed) where to insert the snippet. " +
|
|
26
|
+
"The snippet will be inserted AFTER this line. " +
|
|
27
|
+
"Required if writeFile is true and insertAfterPattern is not provided."),
|
|
28
|
+
insertAfterPattern: z.string().optional().describe("A string pattern to find in the file. The snippet will be inserted after this pattern. " +
|
|
29
|
+
"Example: 'return (' or '<main>' or '</header>'. " +
|
|
30
|
+
"Required if writeFile is true and insertAtLine is not provided."),
|
|
22
31
|
};
|
|
23
32
|
export async function snippetFrontend(params) {
|
|
24
|
-
const { request, targetFile, techStack, insertionContext, context } = params;
|
|
33
|
+
const { request, targetFile, techStack, insertionContext, context, writeFile, insertAtLine: lineNumber, insertAfterPattern: pattern, } = params;
|
|
25
34
|
// Build context instructions
|
|
26
35
|
let contextInstructions = '';
|
|
27
36
|
if (context) {
|
|
@@ -40,6 +49,69 @@ ${insertionContext}
|
|
|
40
49
|
|
|
41
50
|
Generate a snippet that will integrate smoothly at this location.`.trim();
|
|
42
51
|
const result = await generateWithGemini(systemPrompt, request, undefined, "minimal");
|
|
52
|
+
// Insert snippet directly if requested
|
|
53
|
+
if (writeFile) {
|
|
54
|
+
if (!lineNumber && !pattern) {
|
|
55
|
+
return {
|
|
56
|
+
content: [{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: `❌ Error: writeFile requires either insertAtLine or insertAfterPattern`,
|
|
59
|
+
}],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const fileContent = readFileIfExists(targetFile);
|
|
63
|
+
if (!fileContent) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: `❌ Error: File not found: ${targetFile}`,
|
|
68
|
+
}],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const parsed = parseSnippet(result);
|
|
72
|
+
if (!parsed) {
|
|
73
|
+
return {
|
|
74
|
+
content: [{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: `⚠️ Could not parse snippet. Raw result:\n\n${result}`,
|
|
77
|
+
}],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
let newContent;
|
|
81
|
+
// Insert using line number or pattern
|
|
82
|
+
if (lineNumber) {
|
|
83
|
+
newContent = insertAfterLine(fileContent, parsed.snippet, lineNumber);
|
|
84
|
+
}
|
|
85
|
+
else if (pattern) {
|
|
86
|
+
const insertResult = insertAfterPattern(fileContent, parsed.snippet, pattern);
|
|
87
|
+
if (!insertResult.success) {
|
|
88
|
+
return {
|
|
89
|
+
content: [{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: `❌ Error: ${insertResult.error}`,
|
|
92
|
+
}],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
newContent = insertResult.newContent;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Should never reach here due to earlier check
|
|
99
|
+
newContent = fileContent;
|
|
100
|
+
}
|
|
101
|
+
// Merge imports if any
|
|
102
|
+
if (parsed.imports) {
|
|
103
|
+
newContent = mergeImports(newContent, parsed.imports);
|
|
104
|
+
}
|
|
105
|
+
// Write the file
|
|
106
|
+
writeFileWithDirs(targetFile, newContent);
|
|
107
|
+
return {
|
|
108
|
+
content: [{
|
|
109
|
+
type: "text",
|
|
110
|
+
text: `✅ Inserted snippet in: ${targetFile}${parsed.imports ? `\n Added imports` : ""}${lineNumber ? `\n After line ${lineNumber}` : ""}${pattern ? `\n After pattern: ${pattern}` : ""}`,
|
|
111
|
+
}],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// Default: return the snippet
|
|
43
115
|
return {
|
|
44
116
|
content: [{ type: "text", text: result }],
|
|
45
117
|
};
|