skema-core 0.1.0 → 0.1.2
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/README.md +98 -49
- package/dist/cli.js +1186 -23
- package/dist/index.d.mts +81 -3
- package/dist/index.d.ts +81 -3
- package/dist/index.js +2388 -991
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2389 -993
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +208 -3
- package/dist/server.d.ts +208 -3
- package/dist/server.js +1012 -143
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +981 -145
- package/dist/server.mjs.map +1 -1
- package/package.json +7 -2
package/dist/server.js
CHANGED
|
@@ -2,8 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
var child_process = require('child_process');
|
|
4
4
|
var generativeAi = require('@google/generative-ai');
|
|
5
|
+
var ws = require('ws');
|
|
6
|
+
var fs = require('fs');
|
|
7
|
+
var path = require('path');
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
function _interopNamespace(e) {
|
|
10
|
+
if (e && e.__esModule) return e;
|
|
11
|
+
var n = Object.create(null);
|
|
12
|
+
if (e) {
|
|
13
|
+
Object.keys(e).forEach(function (k) {
|
|
14
|
+
if (k !== 'default') {
|
|
15
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
16
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
get: function () { return e[k]; }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
n.default = e;
|
|
24
|
+
return Object.freeze(n);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
28
|
+
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
29
|
+
|
|
30
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
31
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
32
|
+
}) : x)(function(x) {
|
|
33
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
34
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
35
|
+
});
|
|
7
36
|
|
|
8
37
|
// src/lib/utils.ts
|
|
9
38
|
function getGridCellReference(x, y, gridSize = 100) {
|
|
@@ -13,6 +42,204 @@ function getGridCellReference(x, y, gridSize = 100) {
|
|
|
13
42
|
return `${colLabel}${row}`;
|
|
14
43
|
}
|
|
15
44
|
|
|
45
|
+
// src/server/prompts.ts
|
|
46
|
+
var CRITICAL_RULES = `CRITICAL RULES:
|
|
47
|
+
- Do NOT create new files. Only edit existing files.
|
|
48
|
+
- Do NOT modify the import { SkemaWrapper } from "@/components/skema-wrapper" line.
|
|
49
|
+
- Ensure all JSX tags are properly closed - every <tag> needs a matching </tag>.
|
|
50
|
+
- Do NOT run any shell commands. No npm, no git, no lint, no build commands. Just edit files.
|
|
51
|
+
- STOP immediately after making the file changes. Do not verify, do not run tests, do not check status.`;
|
|
52
|
+
var JSX_VALIDATION_RULE = `JSX SYNTAX: Every opening tag (<div>, <a>, <span>, <button>) MUST have a matching closing tag. Self-closing tags (<img />, <input />, <br />) must end with />.`;
|
|
53
|
+
function buildFastDomSelectionPrompt(input) {
|
|
54
|
+
const { comment, selector, text, tagName } = input;
|
|
55
|
+
const parts = [];
|
|
56
|
+
if (tagName) {
|
|
57
|
+
parts.push(`<${tagName.toLowerCase()}>`);
|
|
58
|
+
}
|
|
59
|
+
if (selector) {
|
|
60
|
+
parts.push(`selector: ${selector}`);
|
|
61
|
+
}
|
|
62
|
+
if (text) {
|
|
63
|
+
parts.push(`text: "${text.slice(0, 50)}"`);
|
|
64
|
+
}
|
|
65
|
+
const target = parts.length > 0 ? ` (${parts.join(" | ")})` : "";
|
|
66
|
+
return `${comment}${target}. Make the change directly, no explanation needed. ${CRITICAL_RULES} ${JSX_VALIDATION_RULE}`;
|
|
67
|
+
}
|
|
68
|
+
function buildDetailedDomSelectionPrompt(input) {
|
|
69
|
+
const { comment, tagName, selector, text, elementPath, cssClasses, attributes, elements } = input;
|
|
70
|
+
let prompt = `Make this code change: "${comment || "No specific comment provided"}"
|
|
71
|
+
|
|
72
|
+
## Target Element
|
|
73
|
+
- Tag: <${tagName?.toLowerCase() || "unknown"}>`;
|
|
74
|
+
if (selector) {
|
|
75
|
+
prompt += `
|
|
76
|
+
- Selector: ${selector}`;
|
|
77
|
+
}
|
|
78
|
+
if (elementPath) {
|
|
79
|
+
prompt += `
|
|
80
|
+
- DOM Path: ${elementPath}`;
|
|
81
|
+
}
|
|
82
|
+
if (cssClasses) {
|
|
83
|
+
prompt += `
|
|
84
|
+
- CSS Classes: ${cssClasses}`;
|
|
85
|
+
}
|
|
86
|
+
if (attributes && Object.keys(attributes).length > 0) {
|
|
87
|
+
const attrStr = Object.entries(attributes).map(([k, v]) => `${k}="${v}"`).join(", ");
|
|
88
|
+
prompt += `
|
|
89
|
+
- Attributes: ${attrStr}`;
|
|
90
|
+
}
|
|
91
|
+
if (text) {
|
|
92
|
+
prompt += `
|
|
93
|
+
- Text Content: "${text.slice(0, 150)}"`;
|
|
94
|
+
}
|
|
95
|
+
if (elements && elements.length > 1) {
|
|
96
|
+
prompt += `
|
|
97
|
+
|
|
98
|
+
## Multi-Selection (${elements.length} elements)`;
|
|
99
|
+
elements.slice(0, 5).forEach((el, i) => {
|
|
100
|
+
prompt += `
|
|
101
|
+
${i + 1}. <${el.tagName}> ${el.selector}`;
|
|
102
|
+
if (el.text) prompt += ` - "${el.text.slice(0, 50)}"`;
|
|
103
|
+
});
|
|
104
|
+
if (elements.length > 5) {
|
|
105
|
+
prompt += `
|
|
106
|
+
... and ${elements.length - 5} more`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
prompt += `
|
|
110
|
+
|
|
111
|
+
Make minimal changes. ${CRITICAL_RULES} ${JSX_VALIDATION_RULE}`;
|
|
112
|
+
return prompt;
|
|
113
|
+
}
|
|
114
|
+
function buildGesturePrompt(input) {
|
|
115
|
+
const { comment, gesture, boundingBox } = input;
|
|
116
|
+
return `Make this code change: "${comment || "No specific comment provided"}"
|
|
117
|
+
|
|
118
|
+
Element: gesture: ${gesture || "unknown"} at (${boundingBox?.x || 0}, ${boundingBox?.y || 0})
|
|
119
|
+
|
|
120
|
+
Make minimal changes. ${CRITICAL_RULES} ${JSX_VALIDATION_RULE}`;
|
|
121
|
+
}
|
|
122
|
+
function buildDrawingToCodePrompt(input) {
|
|
123
|
+
const {
|
|
124
|
+
comment = "Create a component based on this drawing",
|
|
125
|
+
boundingBox,
|
|
126
|
+
extractedText,
|
|
127
|
+
gridConfig,
|
|
128
|
+
viewport,
|
|
129
|
+
nearbyElements = [],
|
|
130
|
+
visionDescription
|
|
131
|
+
} = input;
|
|
132
|
+
const gridSize = gridConfig?.size || 100;
|
|
133
|
+
let gridCellRef = "";
|
|
134
|
+
if (boundingBox) {
|
|
135
|
+
gridCellRef = getGridCellReference(boundingBox.x, boundingBox.y, gridSize);
|
|
136
|
+
}
|
|
137
|
+
const positionContext = buildPositionContext(boundingBox, viewport, gridCellRef);
|
|
138
|
+
const nearbyContext = buildNearbyElementsContext(nearbyElements);
|
|
139
|
+
const textContext = extractedText?.trim() ? `
|
|
140
|
+
**Text found in drawing (use as reference if hard to read):**
|
|
141
|
+
${extractedText}` : "";
|
|
142
|
+
const imageNote = buildImageNote(!!input.drawingImage, visionDescription);
|
|
143
|
+
return `Your task is to interpret a user's sketch/wireframe and turn it into code that is integrated in the codebase.
|
|
144
|
+
|
|
145
|
+
## User's Request
|
|
146
|
+
"${comment}"
|
|
147
|
+
|
|
148
|
+
## Drawing Context
|
|
149
|
+
${positionContext}${textContext}${nearbyContext}${imageNote}
|
|
150
|
+
|
|
151
|
+
## Your Process
|
|
152
|
+
1. **Analyze the Sketch:** Understand the visual intent\u2014what UI component does the user want?
|
|
153
|
+
2. **Interpret, Don't Transcribe:** Elevate the low-fidelity drawing into a high-fidelity component. Choose appropriate spacing, colors, and typography that match modern design standards.
|
|
154
|
+
3. **Infer Missing Details:** If something is underspecified, use your expertise to make the best choice. An informed decision is better than an incomplete component.
|
|
155
|
+
|
|
156
|
+
## Implementation Guidelines
|
|
157
|
+
${DRAWING_IMPLEMENTATION_GUIDELINES}
|
|
158
|
+
|
|
159
|
+
Make the changes directly. Insert the UI elements inline at the appropriate location in the page. No explanation needed.
|
|
160
|
+
|
|
161
|
+
## Error Prevention Rules
|
|
162
|
+
${DRAWING_ERROR_PREVENTION_RULES}`;
|
|
163
|
+
}
|
|
164
|
+
var DRAWING_IMPLEMENTATION_GUIDELINES = `- **CRITICAL: DO NOT CREATE ANY NEW FILES. NEVER CREATE NEW FILES. You must ONLY edit existing files.**
|
|
165
|
+
- Add your code directly inline within the existing JSX of the page file - do NOT create separate component files.
|
|
166
|
+
- DO NOT run any shell commands (no npm, git, lint, build, or verification commands). Just edit files and STOP.
|
|
167
|
+
- After making your file edits, you are DONE. Do not run any follow-up commands or checks.
|
|
168
|
+
- Write the UI as inline JSX elements (divs, sections, etc.) directly in the return statement - NOT as a separate component definition
|
|
169
|
+
- Use Tailwind CSS classes for styling (the project uses Tailwind)
|
|
170
|
+
- Do NOT use hardcoded pixel positions or absolute coordinates - integrate naturally with existing page flow
|
|
171
|
+
- Use flexbox, grid, or relative positioning to place the component appropriately
|
|
172
|
+
- If the sketch shows:
|
|
173
|
+
- **Rectangle/box:** Card, container, button, or input field depending on context
|
|
174
|
+
- **Text elements:** Headings, paragraphs, or labels with appropriate hierarchy
|
|
175
|
+
- **Form layout:** Input fields with labels, proper spacing
|
|
176
|
+
- **Icons/shapes:** Use appropriate icons from lucide-react or inline SVGs (but DO NOT add new imports mid-file)
|
|
177
|
+
- **Navigation:** Nav links, menus, or breadcrumbs
|
|
178
|
+
- **Lists:** Ordered/unordered lists or grid layouts
|
|
179
|
+
- Make the UI fit naturally with the existing page design
|
|
180
|
+
- Style it nicely and according to the existing codebase
|
|
181
|
+
- Use semantic HTML and ARIA attributes where appropriate
|
|
182
|
+
- **NEVER add import statements inside JSX or in the middle of a file - all imports must be at the very top of the file**
|
|
183
|
+
- If you need a new import, add it at the TOP of the file with the other imports, then use it in the JSX below`;
|
|
184
|
+
var DRAWING_ERROR_PREVENTION_RULES = `1. **NEVER CREATE NEW FILES** - Do NOT create new component files, utility files, or any other files. Write everything inline in the existing file
|
|
185
|
+
2. You do not need to update package.json or anything, just add / edit the react component.
|
|
186
|
+
3. Do NOT add import statements in the middle of the file or inside JSX - imports go ONLY at the top
|
|
187
|
+
4. Do NOT modify the import { SkemaWrapper } from "@/components/skema-wrapper" line or the SkemaWrapper component itself
|
|
188
|
+
5. If you need something that requires an import and it's not already imported, either use an alternative that doesn't need an import, or add the import at the very TOP of the file with the other imports
|
|
189
|
+
6. DONT MAKE ANY CHANGES THAT WOULD RESULT IN A Build Error
|
|
190
|
+
7. **JSX SYNTAX VALIDATION** - ALWAYS ensure every JSX tag is properly closed. Every opening tag like <div>, <a>, <span>, <button> MUST have a matching closing tag </div>, </a>, </span>, </button>. Self-closing tags like <img />, <input />, <br /> must end with />. Before finishing, mentally verify all tag pairs are balanced.`;
|
|
191
|
+
var IMAGE_ANALYSIS_PROMPT = `
|
|
192
|
+
Analyze this UI wireframe sketch in some detail (not too long) for a front-end developer.
|
|
193
|
+
|
|
194
|
+
Describe every element, layout, spacing, icons, and text you see.
|
|
195
|
+
Focus on whats apparent, don't overthink it.
|
|
196
|
+
Mention relative positions and hierarchy.
|
|
197
|
+
Be distinct about what is drawn vs what might be background.
|
|
198
|
+
|
|
199
|
+
Do NOT focus on exact pixel coordinates or absolute positions - describe layouts
|
|
200
|
+
in terms of relative positioning (left/right/top/bottom, centered, evenly spaced, etc.).
|
|
201
|
+
|
|
202
|
+
It is expected that they will be rough draft's / hand-drawn things. Interpret the drawing and its goals as best as you can.
|
|
203
|
+
DO NOT MENTION THAT THINGS HAVE "Hand-sketched" or "Hand-drawn" vibes. Make assumptions of what they were trying to do.
|
|
204
|
+
Just FYI, this gets passed onto a generator to generate the actual code of modern UI componenents.
|
|
205
|
+
`.trim();
|
|
206
|
+
function buildPositionContext(bbox, viewport, gridCellRef) {
|
|
207
|
+
if (!bbox) return "";
|
|
208
|
+
if (viewport) {
|
|
209
|
+
const relX = (bbox.x / viewport.width * 100).toFixed(1);
|
|
210
|
+
const relY = (bbox.y / viewport.height * 100).toFixed(1);
|
|
211
|
+
let context = `**Drawing Location:** Approximately ${relX}% from left, ${relY}% from top of viewport`;
|
|
212
|
+
if (gridCellRef) {
|
|
213
|
+
context += ` (grid cell ${gridCellRef})`;
|
|
214
|
+
}
|
|
215
|
+
return context;
|
|
216
|
+
}
|
|
217
|
+
return `**Drawing Area:** ${Math.round(bbox.width)}\xD7${Math.round(bbox.height)}px`;
|
|
218
|
+
}
|
|
219
|
+
function buildNearbyElementsContext(nearbyElements) {
|
|
220
|
+
if (nearbyElements.length === 0) return "";
|
|
221
|
+
const elementList = nearbyElements.slice(0, 5).map((el) => {
|
|
222
|
+
let desc = `- <${el.tagName.toLowerCase()}>`;
|
|
223
|
+
if (el.text) desc += `: "${el.text.slice(0, 50)}"`;
|
|
224
|
+
if (el.className) desc += ` (class: ${el.className.slice(0, 50)})`;
|
|
225
|
+
desc += ` (${el.selector})`;
|
|
226
|
+
return desc;
|
|
227
|
+
}).join("\n");
|
|
228
|
+
return `
|
|
229
|
+
**Nearby DOM Elements (for placement reference):**
|
|
230
|
+
${elementList}`;
|
|
231
|
+
}
|
|
232
|
+
function buildImageNote(hasImage, visionDescription) {
|
|
233
|
+
let note = hasImage ? "\n**[Drawing image provided as base64 PNG with labeled grid overlay]**" : "";
|
|
234
|
+
if (visionDescription) {
|
|
235
|
+
note += `
|
|
236
|
+
|
|
237
|
+
## Visual Analysis of Drawing
|
|
238
|
+
${visionDescription}`;
|
|
239
|
+
}
|
|
240
|
+
return note;
|
|
241
|
+
}
|
|
242
|
+
|
|
16
243
|
// src/server/gemini-cli.ts
|
|
17
244
|
var annotationSnapshots = /* @__PURE__ */ new Map();
|
|
18
245
|
function createSnapshot(annotationId, cwd) {
|
|
@@ -66,135 +293,50 @@ function revertAnnotation(annotationId, cwd = process.cwd()) {
|
|
|
66
293
|
function getTrackedAnnotations() {
|
|
67
294
|
return Array.from(annotationSnapshots.keys());
|
|
68
295
|
}
|
|
69
|
-
function buildPromptFromAnnotation(annotation,
|
|
296
|
+
function buildPromptFromAnnotation(annotation, _projectContext, options) {
|
|
70
297
|
const fastMode = options?.fastMode ?? true;
|
|
71
298
|
if (annotation.type === "drawing") {
|
|
72
|
-
|
|
299
|
+
const drawingAnnotation = annotation;
|
|
300
|
+
return buildDrawingToCodePrompt({
|
|
301
|
+
comment: drawingAnnotation.comment || "Create a component based on this drawing",
|
|
302
|
+
boundingBox: drawingAnnotation.boundingBox,
|
|
303
|
+
drawingSvg: drawingAnnotation.drawingSvg,
|
|
304
|
+
drawingImage: drawingAnnotation.drawingImage,
|
|
305
|
+
extractedText: drawingAnnotation.extractedText,
|
|
306
|
+
gridConfig: drawingAnnotation.gridConfig,
|
|
307
|
+
viewport: drawingAnnotation.viewport,
|
|
308
|
+
projectStyles: drawingAnnotation.projectStyles,
|
|
309
|
+
nearbyElements: drawingAnnotation.nearbyElements,
|
|
310
|
+
visionDescription: options?.visionDescription
|
|
311
|
+
});
|
|
73
312
|
}
|
|
74
|
-
if (
|
|
75
|
-
const selector = annotation.selector || "";
|
|
76
|
-
const text = annotation.text || "";
|
|
77
|
-
const tag = annotation.tagName?.toLowerCase() || "";
|
|
78
|
-
let target = "";
|
|
79
|
-
if (text) {
|
|
80
|
-
target = `"${text.slice(0, 50)}"`;
|
|
81
|
-
} else if (selector) {
|
|
82
|
-
target = `\`${selector}\``;
|
|
83
|
-
} else if (tag) {
|
|
84
|
-
target = `<${tag}>`;
|
|
85
|
-
}
|
|
86
|
-
return `${annotation.comment}${target ? ` (target: ${target})` : ""}. Make the change directly, no explanation needed. IMPORTANT: Do NOT modify the import { SkemaWrapper } from "@/components/skema-wrapper" line.`;
|
|
87
|
-
}
|
|
88
|
-
let prompt = `Make this code change: "${annotation.comment || "No specific comment provided"}"
|
|
89
|
-
|
|
90
|
-
Element: `;
|
|
91
|
-
if (annotation.type === "dom_selection") {
|
|
92
|
-
const domAnnotation = annotation;
|
|
93
|
-
prompt += `<${domAnnotation.tagName?.toLowerCase() || "unknown"}>`;
|
|
94
|
-
if (domAnnotation.selector) prompt += ` | selector: ${domAnnotation.selector}`;
|
|
95
|
-
if (domAnnotation.text) prompt += ` | text: "${domAnnotation.text.slice(0, 100)}"`;
|
|
96
|
-
if (domAnnotation.elements && domAnnotation.elements.length > 1) {
|
|
97
|
-
prompt += `
|
|
98
|
-
${domAnnotation.elements.length} elements selected`;
|
|
99
|
-
}
|
|
100
|
-
} else if (annotation.type === "gesture") {
|
|
313
|
+
if (annotation.type === "gesture") {
|
|
101
314
|
const gestureAnnotation = annotation;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Make minimal changes. IMPORTANT: Do NOT modify the import { SkemaWrapper } from "@/components/skema-wrapper" line.`;
|
|
109
|
-
return prompt;
|
|
110
|
-
}
|
|
111
|
-
function buildDrawingPrompt(annotation, projectContext, visionDescription) {
|
|
112
|
-
const drawingAnnotation = annotation;
|
|
113
|
-
const bbox = drawingAnnotation.boundingBox;
|
|
114
|
-
const nearbyElements = drawingAnnotation.nearbyElements || [];
|
|
115
|
-
const extractedText = drawingAnnotation.extractedText;
|
|
116
|
-
const comment = drawingAnnotation.comment || "Create a component based on this drawing";
|
|
117
|
-
const hasImage = !!drawingAnnotation.drawingImage;
|
|
118
|
-
const gridSize = drawingAnnotation.gridConfig?.size || 100;
|
|
119
|
-
let gridCellRef = "";
|
|
120
|
-
if (bbox) {
|
|
121
|
-
gridCellRef = getGridCellReference(bbox.x, bbox.y, gridSize);
|
|
122
|
-
}
|
|
123
|
-
let positionContext = "";
|
|
124
|
-
if (bbox && drawingAnnotation.viewport) {
|
|
125
|
-
const vp = drawingAnnotation.viewport;
|
|
126
|
-
const relX = (bbox.x / vp.width * 100).toFixed(1);
|
|
127
|
-
const relY = (bbox.y / vp.height * 100).toFixed(1);
|
|
128
|
-
positionContext = `**Drawing Location:** Approximately ${relX}% from left, ${relY}% from top of viewport`;
|
|
129
|
-
if (gridCellRef) {
|
|
130
|
-
positionContext += ` (grid cell ${gridCellRef})`;
|
|
131
|
-
}
|
|
132
|
-
} else if (bbox) {
|
|
133
|
-
positionContext = `**Drawing Area:** ${Math.round(bbox.width)}\xD7${Math.round(bbox.height)}px`;
|
|
134
|
-
}
|
|
135
|
-
let nearbyContext = "";
|
|
136
|
-
if (nearbyElements.length > 0) {
|
|
137
|
-
const elementList = nearbyElements.slice(0, 5).map((el) => {
|
|
138
|
-
let desc = `- <${el.tagName.toLowerCase()}>`;
|
|
139
|
-
if (el.text) desc += `: "${el.text.slice(0, 50)}"`;
|
|
140
|
-
if (el.className) desc += ` (class: ${el.className.slice(0, 50)})`;
|
|
141
|
-
desc += ` (${el.selector})`;
|
|
142
|
-
return desc;
|
|
143
|
-
}).join("\n");
|
|
144
|
-
nearbyContext = `
|
|
145
|
-
**Nearby DOM Elements (for placement reference):**
|
|
146
|
-
${elementList}`;
|
|
147
|
-
}
|
|
148
|
-
let textContext = "";
|
|
149
|
-
if (extractedText && extractedText.trim()) {
|
|
150
|
-
textContext = `
|
|
151
|
-
**Text found in drawing (use as reference if hard to read):**
|
|
152
|
-
${extractedText}`;
|
|
315
|
+
return buildGesturePrompt({
|
|
316
|
+
comment: annotation.comment || "No specific comment provided",
|
|
317
|
+
gesture: gestureAnnotation.gesture,
|
|
318
|
+
boundingBox: gestureAnnotation.boundingBox
|
|
319
|
+
});
|
|
153
320
|
}
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
321
|
+
const domAnnotation = annotation;
|
|
322
|
+
if (fastMode) {
|
|
323
|
+
return buildFastDomSelectionPrompt({
|
|
324
|
+
comment: annotation.comment || "No specific comment provided",
|
|
325
|
+
selector: domAnnotation.selector,
|
|
326
|
+
text: domAnnotation.text,
|
|
327
|
+
tagName: domAnnotation.tagName
|
|
328
|
+
});
|
|
160
329
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
2. **Interpret, Don't Transcribe:** Elevate the low-fidelity drawing into a high-fidelity component. Choose appropriate spacing, colors, and typography that match modern design standards.
|
|
172
|
-
3. **Infer Missing Details:** If something is underspecified, use your expertise to make the best choice. An informed decision is better than an incomplete component.
|
|
173
|
-
|
|
174
|
-
## Implementation Guidelines
|
|
175
|
-
- Create a React component with inline styles or Tailwind CSS classes
|
|
176
|
-
- Do NOT use hardcoded pixel positions or absolute coordinates - integrate naturally with existing page flow
|
|
177
|
-
- Use flexbox, grid, or relative positioning to place the component appropriately
|
|
178
|
-
- If the sketch shows:
|
|
179
|
-
- **Rectangle/box:** Card, container, button, or input field depending on context
|
|
180
|
-
- **Text elements:** Headings, paragraphs, or labels with appropriate hierarchy
|
|
181
|
-
- **Form layout:** Input fields with labels, proper spacing
|
|
182
|
-
- **Icons/shapes:** Use appropriate icons from lucide-react or inline SVGs
|
|
183
|
-
- **Navigation:** Nav links, menus, or breadcrumbs
|
|
184
|
-
- **Lists:** Ordered/unordered lists or grid layouts
|
|
185
|
-
- Make the component fit naturally with the existing page design
|
|
186
|
-
- Style it nicely and according to the existing codebase
|
|
187
|
-
- Use semantic HTML and ARIA attributes where appropriate
|
|
188
|
-
- The component should integrate well with the existing codebase structure
|
|
189
|
-
|
|
190
|
-
## Annotations
|
|
191
|
-
- Any **red marks** in the drawing are instructions\u2014follow them but don't render them
|
|
192
|
-
- Text annotations describe intent or constraints
|
|
193
|
-
|
|
194
|
-
Make the changes directly. Insert the component at the appropriate location in the page. No explanation needed.
|
|
195
|
-
|
|
196
|
-
IMPORTANT: Do NOT modify the import { SkemaWrapper } from "@/components/skema-wrapper" line or the SkemaWrapper component itself.`;
|
|
197
|
-
return prompt;
|
|
330
|
+
return buildDetailedDomSelectionPrompt({
|
|
331
|
+
comment: annotation.comment || "No specific comment provided",
|
|
332
|
+
tagName: domAnnotation.tagName,
|
|
333
|
+
selector: domAnnotation.selector,
|
|
334
|
+
text: domAnnotation.text,
|
|
335
|
+
elementPath: domAnnotation.elementPath,
|
|
336
|
+
cssClasses: domAnnotation.cssClasses,
|
|
337
|
+
attributes: domAnnotation.attributes,
|
|
338
|
+
elements: domAnnotation.elements
|
|
339
|
+
});
|
|
198
340
|
}
|
|
199
341
|
function spawnGeminiCLI(prompt, options = {}) {
|
|
200
342
|
const {
|
|
@@ -300,22 +442,8 @@ async function analyzeImageWithGemini(apiKey, base64Image, modelName = "gemini-2
|
|
|
300
442
|
}
|
|
301
443
|
}
|
|
302
444
|
];
|
|
303
|
-
const imageAnalysisPrompt = `
|
|
304
|
-
Analyze this UI wireframe sketch in extreme detail for a front-end developer.
|
|
305
|
-
|
|
306
|
-
Describe every element, layout, spacing, icons, and text you see.
|
|
307
|
-
Mention relative positions and hierarchy.
|
|
308
|
-
Be distinct about what is drawn vs what might be background.
|
|
309
|
-
|
|
310
|
-
Do NOT focus on exact pixel coordinates or absolute positions - describe layouts
|
|
311
|
-
in terms of relative positioning (left/right/top/bottom, centered, evenly spaced, etc.).
|
|
312
|
-
|
|
313
|
-
It is expected that they will be rough draft's / hand-drawn things. Interpret the drawing and its goals as best as you can.
|
|
314
|
-
DO NOT MENTION THAT THINGS HAVE "Hand-sketched" or "Hand-drawn" vibes. Make assumptions of what they were trying to do.
|
|
315
|
-
Just FYI, this gets passed onto a generator to generate the actual code of modern UI componenents.
|
|
316
|
-
`.trim();
|
|
317
445
|
const result = await model.generateContent([
|
|
318
|
-
|
|
446
|
+
IMAGE_ANALYSIS_PROMPT,
|
|
319
447
|
...imageParts
|
|
320
448
|
]);
|
|
321
449
|
const response = await result.response;
|
|
@@ -328,15 +456,32 @@ Just FYI, this gets passed onto a generator to generate the actual code of moder
|
|
|
328
456
|
}
|
|
329
457
|
function createGeminiCLIStream(annotation, projectContext, options) {
|
|
330
458
|
const encoder = new TextEncoder();
|
|
459
|
+
let geminiProcess = null;
|
|
460
|
+
let isCancelled = false;
|
|
331
461
|
return new ReadableStream({
|
|
332
462
|
async start(controller) {
|
|
333
463
|
const apiKey = options?.apiKey || process.env.GEMINI_API_KEY;
|
|
334
464
|
let visionDescription = "";
|
|
465
|
+
if (options?.signal) {
|
|
466
|
+
options.signal.addEventListener("abort", () => {
|
|
467
|
+
isCancelled = true;
|
|
468
|
+
if (geminiProcess && !geminiProcess.killed) {
|
|
469
|
+
console.log("[Skema Server] Killing Gemini CLI process due to client disconnect");
|
|
470
|
+
geminiProcess.kill("SIGTERM");
|
|
471
|
+
}
|
|
472
|
+
options.onCancel?.();
|
|
473
|
+
try {
|
|
474
|
+
controller.close();
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
335
479
|
if (annotation.type === "drawing" && annotation.drawingImage && apiKey) {
|
|
480
|
+
if (isCancelled) return;
|
|
336
481
|
const progressEvent = {
|
|
337
482
|
type: "message",
|
|
338
483
|
role: "assistant",
|
|
339
|
-
content: "
|
|
484
|
+
content: "[Analyzing] Drawing image with Gemini Vision...",
|
|
340
485
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
341
486
|
};
|
|
342
487
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(progressEvent)}
|
|
@@ -347,10 +492,11 @@ function createGeminiCLIStream(annotation, projectContext, options) {
|
|
|
347
492
|
annotation.drawingImage,
|
|
348
493
|
options?.model || "gemini-2.5-flash"
|
|
349
494
|
);
|
|
495
|
+
if (isCancelled) return;
|
|
350
496
|
const analysisEvent = {
|
|
351
497
|
type: "message",
|
|
352
498
|
role: "assistant",
|
|
353
|
-
content:
|
|
499
|
+
content: `[Vision] Visual Analysis:
|
|
354
500
|
${visionDescription}`,
|
|
355
501
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
356
502
|
};
|
|
@@ -358,6 +504,7 @@ ${visionDescription}`,
|
|
|
358
504
|
|
|
359
505
|
`));
|
|
360
506
|
}
|
|
507
|
+
if (isCancelled) return;
|
|
361
508
|
const prompt = buildPromptFromAnnotation(
|
|
362
509
|
annotation,
|
|
363
510
|
projectContext,
|
|
@@ -375,8 +522,10 @@ ${visionDescription}`,
|
|
|
375
522
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(promptEvent)}
|
|
376
523
|
|
|
377
524
|
`));
|
|
378
|
-
const { events } = spawnGeminiCLI(prompt, options);
|
|
525
|
+
const { process: proc, events } = spawnGeminiCLI(prompt, options);
|
|
526
|
+
geminiProcess = proc;
|
|
379
527
|
for await (const event of events) {
|
|
528
|
+
if (isCancelled) break;
|
|
380
529
|
const sseData = `data: ${JSON.stringify(event)}
|
|
381
530
|
|
|
382
531
|
`;
|
|
@@ -386,6 +535,13 @@ ${visionDescription}`,
|
|
|
386
535
|
break;
|
|
387
536
|
}
|
|
388
537
|
}
|
|
538
|
+
},
|
|
539
|
+
cancel() {
|
|
540
|
+
isCancelled = true;
|
|
541
|
+
if (geminiProcess && !geminiProcess.killed) {
|
|
542
|
+
geminiProcess.kill("SIGTERM");
|
|
543
|
+
}
|
|
544
|
+
options?.onCancel?.();
|
|
389
545
|
}
|
|
390
546
|
});
|
|
391
547
|
}
|
|
@@ -430,7 +586,19 @@ function createGeminiRouteHandler(defaultOptions) {
|
|
|
430
586
|
createSnapshot(annotationId, cwd);
|
|
431
587
|
const stream = createGeminiCLIStream(annotation, projectContext, {
|
|
432
588
|
cwd,
|
|
433
|
-
...defaultOptions
|
|
589
|
+
...defaultOptions,
|
|
590
|
+
// Pass abort signal from request for client disconnect detection
|
|
591
|
+
signal: request.signal,
|
|
592
|
+
// Revert snapshot if client cancels/disconnects
|
|
593
|
+
onCancel: () => {
|
|
594
|
+
console.log(`[Skema Server] Client disconnected, reverting snapshot for: ${annotationId}`);
|
|
595
|
+
const result = revertAnnotation(annotationId, cwd);
|
|
596
|
+
if (result.success) {
|
|
597
|
+
console.log(`[Skema Server] Successfully reverted changes for: ${annotationId}`);
|
|
598
|
+
} else {
|
|
599
|
+
console.error(`[Skema Server] Failed to revert changes: ${result.message}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
434
602
|
});
|
|
435
603
|
return new Response(stream, {
|
|
436
604
|
headers: {
|
|
@@ -461,16 +629,717 @@ function createRevertRouteHandler(defaultOptions) {
|
|
|
461
629
|
}
|
|
462
630
|
var POST = createGeminiRouteHandler();
|
|
463
631
|
var DELETE = createRevertRouteHandler();
|
|
632
|
+
var PROVIDERS = {
|
|
633
|
+
gemini: {
|
|
634
|
+
command: "gemini",
|
|
635
|
+
buildArgs: (prompt, options) => {
|
|
636
|
+
const args = ["-p", prompt];
|
|
637
|
+
if (options?.yolo !== false) args.push("--yolo");
|
|
638
|
+
args.push("--output-format", "stream-json");
|
|
639
|
+
if (options?.model) args.push("-m", options.model);
|
|
640
|
+
else args.push("-m", "gemini-2.5-flash");
|
|
641
|
+
return args;
|
|
642
|
+
},
|
|
643
|
+
parseOutput: (line) => {
|
|
644
|
+
try {
|
|
645
|
+
const parsed = JSON.parse(line);
|
|
646
|
+
return {
|
|
647
|
+
type: mapGeminiEventType(parsed.type),
|
|
648
|
+
content: parsed.content || parsed.message,
|
|
649
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
650
|
+
provider: "gemini",
|
|
651
|
+
raw: parsed
|
|
652
|
+
};
|
|
653
|
+
} catch {
|
|
654
|
+
return {
|
|
655
|
+
type: "text",
|
|
656
|
+
content: line,
|
|
657
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
658
|
+
provider: "gemini"
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
claude: {
|
|
664
|
+
command: "claude",
|
|
665
|
+
buildArgs: (prompt, options) => {
|
|
666
|
+
const args = ["-p", prompt, "--output-format", "stream-json"];
|
|
667
|
+
if (options?.yolo !== false) args.push("--dangerously-skip-permissions");
|
|
668
|
+
if (options?.model) args.push("--model", options.model);
|
|
669
|
+
return args;
|
|
670
|
+
},
|
|
671
|
+
parseOutput: (line) => {
|
|
672
|
+
try {
|
|
673
|
+
const parsed = JSON.parse(line);
|
|
674
|
+
return {
|
|
675
|
+
type: mapClaudeEventType(parsed.type),
|
|
676
|
+
content: parsed.content || extractClaudeContent(parsed),
|
|
677
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
678
|
+
provider: "claude",
|
|
679
|
+
raw: parsed
|
|
680
|
+
};
|
|
681
|
+
} catch {
|
|
682
|
+
return {
|
|
683
|
+
type: "text",
|
|
684
|
+
content: line,
|
|
685
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
686
|
+
provider: "claude"
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
function mapGeminiEventType(type) {
|
|
693
|
+
switch (type) {
|
|
694
|
+
case "init":
|
|
695
|
+
return "init";
|
|
696
|
+
case "message":
|
|
697
|
+
return "text";
|
|
698
|
+
case "tool_use":
|
|
699
|
+
return "tool_use";
|
|
700
|
+
case "tool_result":
|
|
701
|
+
return "tool_result";
|
|
702
|
+
case "error":
|
|
703
|
+
return "error";
|
|
704
|
+
case "done":
|
|
705
|
+
case "result":
|
|
706
|
+
return "done";
|
|
707
|
+
default:
|
|
708
|
+
return "text";
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function mapClaudeEventType(type) {
|
|
712
|
+
switch (type) {
|
|
713
|
+
case "system":
|
|
714
|
+
return "init";
|
|
715
|
+
case "assistant":
|
|
716
|
+
case "text":
|
|
717
|
+
return "text";
|
|
718
|
+
case "tool_use":
|
|
719
|
+
return "tool_use";
|
|
720
|
+
case "tool_result":
|
|
721
|
+
return "tool_result";
|
|
722
|
+
case "error":
|
|
723
|
+
return "error";
|
|
724
|
+
case "result":
|
|
725
|
+
return "done";
|
|
726
|
+
default:
|
|
727
|
+
return "text";
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function extractClaudeContent(parsed) {
|
|
731
|
+
if (typeof parsed.content === "string") return parsed.content;
|
|
732
|
+
if (parsed.message && typeof parsed.message === "object") {
|
|
733
|
+
const msg = parsed.message;
|
|
734
|
+
if (typeof msg.content === "string") return msg.content;
|
|
735
|
+
}
|
|
736
|
+
return void 0;
|
|
737
|
+
}
|
|
738
|
+
function spawnAICLI(prompt, config) {
|
|
739
|
+
const spec = PROVIDERS[config.provider];
|
|
740
|
+
const args = spec.buildArgs(prompt, { model: config.model });
|
|
741
|
+
const cwd = config.cwd || process.cwd();
|
|
742
|
+
console.log(`[Skema] Spawning ${config.provider}: ${spec.command} ${args.join(" ")}`);
|
|
743
|
+
const child = child_process.spawn(spec.command, args, {
|
|
744
|
+
cwd,
|
|
745
|
+
env: process.env
|
|
746
|
+
});
|
|
747
|
+
const events = {
|
|
748
|
+
[Symbol.asyncIterator]() {
|
|
749
|
+
let buffer = "";
|
|
750
|
+
let done = false;
|
|
751
|
+
const queue = [];
|
|
752
|
+
let resolveNext = null;
|
|
753
|
+
const pushEvent = (event) => {
|
|
754
|
+
if (resolveNext) {
|
|
755
|
+
resolveNext({ value: event, done: false });
|
|
756
|
+
resolveNext = null;
|
|
757
|
+
} else {
|
|
758
|
+
queue.push(event);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
child.stdout?.on("data", (data) => {
|
|
762
|
+
buffer += data.toString();
|
|
763
|
+
const lines = buffer.split("\n");
|
|
764
|
+
buffer = lines.pop() || "";
|
|
765
|
+
for (const line of lines) {
|
|
766
|
+
if (line.trim()) {
|
|
767
|
+
const event = spec.parseOutput(line.trim());
|
|
768
|
+
if (event) pushEvent(event);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
child.stderr?.on("data", (data) => {
|
|
773
|
+
const content = data.toString().trim();
|
|
774
|
+
if (content) {
|
|
775
|
+
pushEvent({
|
|
776
|
+
type: "error",
|
|
777
|
+
content,
|
|
778
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
779
|
+
provider: config.provider
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
child.on("close", (code) => {
|
|
784
|
+
if (buffer.trim()) {
|
|
785
|
+
const event = spec.parseOutput(buffer.trim());
|
|
786
|
+
if (event) pushEvent(event);
|
|
787
|
+
}
|
|
788
|
+
pushEvent({
|
|
789
|
+
type: "done",
|
|
790
|
+
content: `Process exited with code ${code}`,
|
|
791
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
792
|
+
provider: config.provider
|
|
793
|
+
});
|
|
794
|
+
done = true;
|
|
795
|
+
if (resolveNext) {
|
|
796
|
+
resolveNext({ value: void 0, done: true });
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
child.on("error", (err) => {
|
|
800
|
+
pushEvent({
|
|
801
|
+
type: "error",
|
|
802
|
+
content: `Failed to spawn ${config.provider}: ${err.message}`,
|
|
803
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
804
|
+
provider: config.provider
|
|
805
|
+
});
|
|
806
|
+
done = true;
|
|
807
|
+
if (resolveNext) {
|
|
808
|
+
resolveNext({ value: void 0, done: true });
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
return {
|
|
812
|
+
next() {
|
|
813
|
+
if (queue.length > 0) {
|
|
814
|
+
return Promise.resolve({ value: queue.shift(), done: false });
|
|
815
|
+
}
|
|
816
|
+
if (done) {
|
|
817
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
818
|
+
}
|
|
819
|
+
return new Promise((resolve) => {
|
|
820
|
+
resolveNext = resolve;
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
return { process: child, events };
|
|
827
|
+
}
|
|
828
|
+
async function runAICLI(prompt, config) {
|
|
829
|
+
const { events } = spawnAICLI(prompt, config);
|
|
830
|
+
const collectedEvents = [];
|
|
831
|
+
let output = "";
|
|
832
|
+
let success = true;
|
|
833
|
+
for await (const event of events) {
|
|
834
|
+
collectedEvents.push(event);
|
|
835
|
+
if (event.type === "text" && event.content) {
|
|
836
|
+
output += event.content + "\n";
|
|
837
|
+
}
|
|
838
|
+
if (event.type === "error") {
|
|
839
|
+
success = false;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
success,
|
|
844
|
+
output: output.trim(),
|
|
845
|
+
events: collectedEvents,
|
|
846
|
+
provider: config.provider
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function isProviderAvailable(provider) {
|
|
850
|
+
const spec = PROVIDERS[provider];
|
|
851
|
+
try {
|
|
852
|
+
const { execSync: execSync3 } = __require("child_process");
|
|
853
|
+
execSync3(`which ${spec.command}`, { stdio: "ignore" });
|
|
854
|
+
return true;
|
|
855
|
+
} catch {
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function getAvailableProviders() {
|
|
860
|
+
return ["gemini", "claude"].filter(isProviderAvailable);
|
|
861
|
+
}
|
|
862
|
+
async function analyzeWithGemini(base64Image, apiKey, model = "gemini-2.5-flash") {
|
|
863
|
+
try {
|
|
864
|
+
const genAI = new generativeAi.GoogleGenerativeAI(apiKey);
|
|
865
|
+
const visionModel = genAI.getGenerativeModel({ model });
|
|
866
|
+
const cleanBase64 = base64Image.replace(/^data:image\/\w+;base64,/, "");
|
|
867
|
+
const result = await visionModel.generateContent([
|
|
868
|
+
IMAGE_ANALYSIS_PROMPT,
|
|
869
|
+
{
|
|
870
|
+
inlineData: {
|
|
871
|
+
data: cleanBase64,
|
|
872
|
+
mimeType: "image/png"
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
]);
|
|
876
|
+
const response = await result.response;
|
|
877
|
+
const text = response.text();
|
|
878
|
+
return {
|
|
879
|
+
success: true,
|
|
880
|
+
description: text,
|
|
881
|
+
provider: "gemini"
|
|
882
|
+
};
|
|
883
|
+
} catch (error) {
|
|
884
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
885
|
+
console.error("[Vision] Gemini analysis failed:", message);
|
|
886
|
+
return {
|
|
887
|
+
success: false,
|
|
888
|
+
description: "",
|
|
889
|
+
provider: "gemini",
|
|
890
|
+
error: message
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async function analyzeWithClaude(base64Image, apiKey, model = "claude-sonnet-4-20250514") {
|
|
895
|
+
try {
|
|
896
|
+
const Anthropic = (await import('@anthropic-ai/sdk')).default;
|
|
897
|
+
const client = new Anthropic({ apiKey });
|
|
898
|
+
const cleanBase64 = base64Image.replace(/^data:image\/\w+;base64,/, "");
|
|
899
|
+
const response = await client.messages.create({
|
|
900
|
+
model,
|
|
901
|
+
max_tokens: 1024,
|
|
902
|
+
messages: [
|
|
903
|
+
{
|
|
904
|
+
role: "user",
|
|
905
|
+
content: [
|
|
906
|
+
{
|
|
907
|
+
type: "image",
|
|
908
|
+
source: {
|
|
909
|
+
type: "base64",
|
|
910
|
+
media_type: "image/png",
|
|
911
|
+
data: cleanBase64
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
type: "text",
|
|
916
|
+
text: IMAGE_ANALYSIS_PROMPT
|
|
917
|
+
}
|
|
918
|
+
]
|
|
919
|
+
}
|
|
920
|
+
]
|
|
921
|
+
});
|
|
922
|
+
const textContent = response.content.find((c) => c.type === "text");
|
|
923
|
+
const description = textContent && "text" in textContent ? textContent.text : "";
|
|
924
|
+
return {
|
|
925
|
+
success: true,
|
|
926
|
+
description,
|
|
927
|
+
provider: "claude"
|
|
928
|
+
};
|
|
929
|
+
} catch (error) {
|
|
930
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
931
|
+
console.error("[Vision] Claude analysis failed:", message);
|
|
932
|
+
return {
|
|
933
|
+
success: false,
|
|
934
|
+
description: "",
|
|
935
|
+
provider: "claude",
|
|
936
|
+
error: message
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async function analyzeImage(base64Image, config) {
|
|
941
|
+
const { provider } = config;
|
|
942
|
+
let apiKey = config.apiKey;
|
|
943
|
+
if (!apiKey) {
|
|
944
|
+
apiKey = provider === "gemini" ? process.env.GEMINI_API_KEY : process.env.ANTHROPIC_API_KEY;
|
|
945
|
+
}
|
|
946
|
+
if (!apiKey) {
|
|
947
|
+
return {
|
|
948
|
+
success: false,
|
|
949
|
+
description: "",
|
|
950
|
+
provider,
|
|
951
|
+
error: `No API key found for ${provider} vision. Set ${provider === "gemini" ? "GEMINI_API_KEY" : "ANTHROPIC_API_KEY"} environment variable.`
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
console.log(`[Vision] Analyzing image with ${provider}...`);
|
|
955
|
+
if (provider === "gemini") {
|
|
956
|
+
return analyzeWithGemini(base64Image, apiKey, config.model);
|
|
957
|
+
} else {
|
|
958
|
+
return analyzeWithClaude(base64Image, apiKey, config.model);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function isVisionAvailable(provider) {
|
|
962
|
+
if (provider === "gemini") {
|
|
963
|
+
return !!process.env.GEMINI_API_KEY;
|
|
964
|
+
} else {
|
|
965
|
+
return !!process.env.ANTHROPIC_API_KEY;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/server/daemon.ts
|
|
970
|
+
var currentProvider = "gemini";
|
|
971
|
+
var workingDirectory = process.cwd();
|
|
972
|
+
var annotationSnapshots2 = /* @__PURE__ */ new Map();
|
|
973
|
+
function createSnapshot2(annotationId) {
|
|
974
|
+
try {
|
|
975
|
+
const stashRef = child_process.execSync("git stash create", { cwd: workingDirectory, encoding: "utf-8" }).trim();
|
|
976
|
+
if (stashRef) {
|
|
977
|
+
annotationSnapshots2.set(annotationId, stashRef);
|
|
978
|
+
console.log(`[Daemon] Created snapshot ${stashRef.slice(0, 7)} for ${annotationId}`);
|
|
979
|
+
return stashRef;
|
|
980
|
+
}
|
|
981
|
+
const headRef = child_process.execSync("git rev-parse HEAD", { cwd: workingDirectory, encoding: "utf-8" }).trim();
|
|
982
|
+
annotationSnapshots2.set(annotationId, headRef);
|
|
983
|
+
return headRef;
|
|
984
|
+
} catch (error) {
|
|
985
|
+
console.error("[Daemon] Failed to create snapshot:", error);
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function revertSnapshot(annotationId) {
|
|
990
|
+
const snapshotRef = annotationSnapshots2.get(annotationId);
|
|
991
|
+
if (!snapshotRef) {
|
|
992
|
+
return { success: false, message: `No snapshot found for ${annotationId}` };
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
const changedFiles = child_process.execSync(`git diff --name-only ${snapshotRef}`, {
|
|
996
|
+
cwd: workingDirectory,
|
|
997
|
+
encoding: "utf-8"
|
|
998
|
+
}).trim().split("\n").filter(Boolean);
|
|
999
|
+
if (changedFiles.length === 0) {
|
|
1000
|
+
annotationSnapshots2.delete(annotationId);
|
|
1001
|
+
return { success: true, message: "No changes to revert" };
|
|
1002
|
+
}
|
|
1003
|
+
for (const file of changedFiles) {
|
|
1004
|
+
try {
|
|
1005
|
+
child_process.execSync(`git checkout ${snapshotRef} -- "${file}"`, { cwd: workingDirectory });
|
|
1006
|
+
} catch {
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
annotationSnapshots2.delete(annotationId);
|
|
1010
|
+
return { success: true, message: `Reverted ${changedFiles.length} file(s)` };
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
return { success: false, message: String(error) };
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
var handlers = {
|
|
1016
|
+
// -------------------------------------------------------------------------
|
|
1017
|
+
// Provider Management
|
|
1018
|
+
// -------------------------------------------------------------------------
|
|
1019
|
+
"get-provider": async (msg) => {
|
|
1020
|
+
return {
|
|
1021
|
+
id: msg.id,
|
|
1022
|
+
type: "provider",
|
|
1023
|
+
provider: currentProvider,
|
|
1024
|
+
available: getAvailableProviders()
|
|
1025
|
+
};
|
|
1026
|
+
},
|
|
1027
|
+
"set-provider": async (msg) => {
|
|
1028
|
+
const newProvider = msg.provider;
|
|
1029
|
+
if (!["gemini", "claude"].includes(newProvider)) {
|
|
1030
|
+
return { id: msg.id, type: "error", error: `Invalid provider: ${newProvider}` };
|
|
1031
|
+
}
|
|
1032
|
+
if (!isProviderAvailable(newProvider)) {
|
|
1033
|
+
return {
|
|
1034
|
+
id: msg.id,
|
|
1035
|
+
type: "error",
|
|
1036
|
+
error: `Provider "${newProvider}" is not installed. Run: ${newProvider === "gemini" ? "npm install -g @anthropic-ai/gemini-cli" : "npm install -g @anthropic-ai/claude-code"}`
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
currentProvider = newProvider;
|
|
1040
|
+
console.log(`[Daemon] Switched to provider: ${currentProvider}`);
|
|
1041
|
+
return { id: msg.id, type: "provider-changed", provider: currentProvider };
|
|
1042
|
+
},
|
|
1043
|
+
// -------------------------------------------------------------------------
|
|
1044
|
+
// AI Generation (streaming)
|
|
1045
|
+
// -------------------------------------------------------------------------
|
|
1046
|
+
generate: async (msg, ws) => {
|
|
1047
|
+
const annotation = msg.annotation;
|
|
1048
|
+
const projectContext = msg.projectContext;
|
|
1049
|
+
const annotationId = annotation.id || `temp-${Date.now()}`;
|
|
1050
|
+
createSnapshot2(annotationId);
|
|
1051
|
+
let visionDescription = "";
|
|
1052
|
+
const drawingAnnotation = annotation;
|
|
1053
|
+
if (annotation.type === "drawing" && drawingAnnotation.drawingImage) {
|
|
1054
|
+
if (isVisionAvailable("gemini")) {
|
|
1055
|
+
sendMessage(ws, {
|
|
1056
|
+
id: msg.id,
|
|
1057
|
+
type: "ai-event",
|
|
1058
|
+
event: {
|
|
1059
|
+
type: "text",
|
|
1060
|
+
content: `[Analyzing drawing with Gemini vision...]`,
|
|
1061
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1062
|
+
provider: currentProvider
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
const visionResult = await analyzeImage(drawingAnnotation.drawingImage, {
|
|
1066
|
+
provider: "gemini"
|
|
1067
|
+
});
|
|
1068
|
+
if (visionResult.success) {
|
|
1069
|
+
visionDescription = visionResult.description;
|
|
1070
|
+
sendMessage(ws, {
|
|
1071
|
+
id: msg.id,
|
|
1072
|
+
type: "ai-event",
|
|
1073
|
+
event: {
|
|
1074
|
+
type: "text",
|
|
1075
|
+
content: `[Vision analysis complete]
|
|
1076
|
+
${visionDescription}`,
|
|
1077
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1078
|
+
provider: currentProvider
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
} else {
|
|
1082
|
+
sendMessage(ws, {
|
|
1083
|
+
id: msg.id,
|
|
1084
|
+
type: "ai-event",
|
|
1085
|
+
event: {
|
|
1086
|
+
type: "error",
|
|
1087
|
+
content: `Vision analysis failed: ${visionResult.error}`,
|
|
1088
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1089
|
+
provider: currentProvider
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
} else {
|
|
1094
|
+
sendMessage(ws, {
|
|
1095
|
+
id: msg.id,
|
|
1096
|
+
type: "ai-event",
|
|
1097
|
+
event: {
|
|
1098
|
+
type: "text",
|
|
1099
|
+
content: `[Vision not available - set GEMINI_API_KEY for image analysis]`,
|
|
1100
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1101
|
+
provider: currentProvider
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
const prompt = buildPromptFromAnnotation(annotation, projectContext, {
|
|
1107
|
+
fastMode: msg.fastMode === true,
|
|
1108
|
+
// Default to detailed mode (false) unless explicitly set to true
|
|
1109
|
+
visionDescription
|
|
1110
|
+
});
|
|
1111
|
+
sendMessage(ws, {
|
|
1112
|
+
id: msg.id,
|
|
1113
|
+
type: "ai-event",
|
|
1114
|
+
event: {
|
|
1115
|
+
type: "debug",
|
|
1116
|
+
content: prompt,
|
|
1117
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1118
|
+
provider: currentProvider
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
const config = {
|
|
1122
|
+
provider: currentProvider,
|
|
1123
|
+
cwd: workingDirectory,
|
|
1124
|
+
model: msg.model
|
|
1125
|
+
};
|
|
1126
|
+
const { process: aiProcess, events } = spawnAICLI(prompt, config);
|
|
1127
|
+
for await (const event of events) {
|
|
1128
|
+
sendMessage(ws, {
|
|
1129
|
+
id: msg.id,
|
|
1130
|
+
type: "ai-event",
|
|
1131
|
+
event,
|
|
1132
|
+
annotationId
|
|
1133
|
+
});
|
|
1134
|
+
if (event.type === "done") {
|
|
1135
|
+
sendMessage(ws, {
|
|
1136
|
+
id: msg.id,
|
|
1137
|
+
type: "generate-complete",
|
|
1138
|
+
success: true,
|
|
1139
|
+
annotationId,
|
|
1140
|
+
provider: currentProvider
|
|
1141
|
+
});
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1146
|
+
// -------------------------------------------------------------------------
|
|
1147
|
+
// Undo/Revert
|
|
1148
|
+
// -------------------------------------------------------------------------
|
|
1149
|
+
revert: async (msg) => {
|
|
1150
|
+
const annotationId = msg.annotationId;
|
|
1151
|
+
if (!annotationId) {
|
|
1152
|
+
return { id: msg.id, type: "error", error: "Missing annotationId" };
|
|
1153
|
+
}
|
|
1154
|
+
const result = revertSnapshot(annotationId);
|
|
1155
|
+
return { id: msg.id, type: "revert-result", ...result };
|
|
1156
|
+
},
|
|
1157
|
+
// -------------------------------------------------------------------------
|
|
1158
|
+
// File Operations
|
|
1159
|
+
// -------------------------------------------------------------------------
|
|
1160
|
+
"read-file": async (msg) => {
|
|
1161
|
+
const filePath = msg.path;
|
|
1162
|
+
const absolutePath = path__namespace.isAbsolute(filePath) ? filePath : path__namespace.join(workingDirectory, filePath);
|
|
1163
|
+
try {
|
|
1164
|
+
const content = fs__namespace.readFileSync(absolutePath, "utf-8");
|
|
1165
|
+
return { id: msg.id, type: "file-content", path: filePath, content };
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
return { id: msg.id, type: "error", error: `Failed to read file: ${error}` };
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
"write-file": async (msg) => {
|
|
1171
|
+
const filePath = msg.path;
|
|
1172
|
+
const content = msg.content;
|
|
1173
|
+
const absolutePath = path__namespace.isAbsolute(filePath) ? filePath : path__namespace.join(workingDirectory, filePath);
|
|
1174
|
+
try {
|
|
1175
|
+
fs__namespace.mkdirSync(path__namespace.dirname(absolutePath), { recursive: true });
|
|
1176
|
+
fs__namespace.writeFileSync(absolutePath, content, "utf-8");
|
|
1177
|
+
console.log(`[Daemon] Wrote file: ${filePath}`);
|
|
1178
|
+
return { id: msg.id, type: "write-success", path: filePath };
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
return { id: msg.id, type: "error", error: `Failed to write file: ${error}` };
|
|
1181
|
+
}
|
|
1182
|
+
},
|
|
1183
|
+
"list-files": async (msg) => {
|
|
1184
|
+
const dirPath = msg.path || ".";
|
|
1185
|
+
const absolutePath = path__namespace.isAbsolute(dirPath) ? dirPath : path__namespace.join(workingDirectory, dirPath);
|
|
1186
|
+
try {
|
|
1187
|
+
const entries = fs__namespace.readdirSync(absolutePath, { withFileTypes: true });
|
|
1188
|
+
const files = entries.map((entry) => ({
|
|
1189
|
+
name: entry.name,
|
|
1190
|
+
isDirectory: entry.isDirectory()
|
|
1191
|
+
}));
|
|
1192
|
+
return { id: msg.id, type: "file-list", path: dirPath, files };
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
return { id: msg.id, type: "error", error: `Failed to list files: ${error}` };
|
|
1195
|
+
}
|
|
1196
|
+
},
|
|
1197
|
+
// -------------------------------------------------------------------------
|
|
1198
|
+
// Command Execution
|
|
1199
|
+
// -------------------------------------------------------------------------
|
|
1200
|
+
"run-command": async (msg, ws) => {
|
|
1201
|
+
const command = msg.command;
|
|
1202
|
+
if (!command) {
|
|
1203
|
+
return { id: msg.id, type: "error", error: "Missing command" };
|
|
1204
|
+
}
|
|
1205
|
+
console.log(`[Daemon] Running command: ${command}`);
|
|
1206
|
+
return new Promise((resolve) => {
|
|
1207
|
+
child_process.exec(command, { cwd: workingDirectory }, (error, stdout, stderr) => {
|
|
1208
|
+
resolve({
|
|
1209
|
+
id: msg.id,
|
|
1210
|
+
type: "command-result",
|
|
1211
|
+
stdout,
|
|
1212
|
+
stderr,
|
|
1213
|
+
exitCode: error ? error.code : 0
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
});
|
|
1217
|
+
},
|
|
1218
|
+
// -------------------------------------------------------------------------
|
|
1219
|
+
// Status
|
|
1220
|
+
// -------------------------------------------------------------------------
|
|
1221
|
+
ping: async (msg) => {
|
|
1222
|
+
return {
|
|
1223
|
+
id: msg.id,
|
|
1224
|
+
type: "pong",
|
|
1225
|
+
provider: currentProvider,
|
|
1226
|
+
cwd: workingDirectory,
|
|
1227
|
+
availableProviders: getAvailableProviders()
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
function sendMessage(ws$1, message) {
|
|
1232
|
+
if (ws$1.readyState === ws.WebSocket.OPEN) {
|
|
1233
|
+
ws$1.send(JSON.stringify(message));
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function handleConnection(ws) {
|
|
1237
|
+
console.log("[Daemon] Client connected");
|
|
1238
|
+
sendMessage(ws, {
|
|
1239
|
+
type: "connected",
|
|
1240
|
+
provider: currentProvider,
|
|
1241
|
+
cwd: workingDirectory,
|
|
1242
|
+
availableProviders: getAvailableProviders()
|
|
1243
|
+
});
|
|
1244
|
+
ws.on("message", async (data) => {
|
|
1245
|
+
let msg;
|
|
1246
|
+
try {
|
|
1247
|
+
msg = JSON.parse(data.toString());
|
|
1248
|
+
} catch {
|
|
1249
|
+
sendMessage(ws, { type: "error", error: "Invalid JSON" });
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const handler = handlers[msg.type];
|
|
1253
|
+
if (!handler) {
|
|
1254
|
+
sendMessage(ws, { id: msg.id, type: "error", error: `Unknown message type: ${msg.type}` });
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
try {
|
|
1258
|
+
const response = await handler(msg, ws);
|
|
1259
|
+
if (response) {
|
|
1260
|
+
sendMessage(ws, response);
|
|
1261
|
+
}
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
sendMessage(ws, {
|
|
1264
|
+
id: msg.id,
|
|
1265
|
+
type: "error",
|
|
1266
|
+
error: `Handler error: ${error}`
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
ws.on("close", () => {
|
|
1271
|
+
console.log("[Daemon] Client disconnected");
|
|
1272
|
+
});
|
|
1273
|
+
ws.on("error", (error) => {
|
|
1274
|
+
console.error("[Daemon] WebSocket error:", error);
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
function startDaemon(config = {}) {
|
|
1278
|
+
const port = config.port ?? 9999;
|
|
1279
|
+
workingDirectory = config.cwd ?? process.cwd();
|
|
1280
|
+
currentProvider = config.defaultProvider ?? "gemini";
|
|
1281
|
+
if (!isProviderAvailable(currentProvider)) {
|
|
1282
|
+
const available = getAvailableProviders();
|
|
1283
|
+
if (available.length > 0) {
|
|
1284
|
+
console.log(`[Daemon] ${currentProvider} not found, falling back to ${available[0]}`);
|
|
1285
|
+
currentProvider = available[0];
|
|
1286
|
+
} else {
|
|
1287
|
+
console.warn("[Daemon] Warning: No AI providers found. Install gemini or claude CLI.");
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
const wss = new ws.WebSocketServer({ port });
|
|
1291
|
+
wss.on("connection", handleConnection);
|
|
1292
|
+
wss.on("listening", () => {
|
|
1293
|
+
console.log("");
|
|
1294
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
1295
|
+
console.log(" \u2502 \u2502");
|
|
1296
|
+
console.log(` \u2502 \u{1F3A8} Skema Daemon running on ws://localhost:${port} \u2502`);
|
|
1297
|
+
console.log(" \u2502 \u2502");
|
|
1298
|
+
console.log(` \u2502 Provider: ${currentProvider.padEnd(35)}\u2502`);
|
|
1299
|
+
console.log(` \u2502 Directory: ${workingDirectory.slice(-33).padEnd(34)}\u2502`);
|
|
1300
|
+
console.log(" \u2502 \u2502");
|
|
1301
|
+
console.log(" \u2502 Waiting for browser connections... \u2502");
|
|
1302
|
+
console.log(" \u2502 \u2502");
|
|
1303
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
1304
|
+
console.log("");
|
|
1305
|
+
});
|
|
1306
|
+
wss.on("error", (error) => {
|
|
1307
|
+
if (error.code === "EADDRINUSE") {
|
|
1308
|
+
console.error(`[Daemon] Port ${port} is already in use. Is another Skema daemon running?`);
|
|
1309
|
+
} else {
|
|
1310
|
+
console.error("[Daemon] Server error:", error);
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
return {
|
|
1314
|
+
port,
|
|
1315
|
+
close: () => {
|
|
1316
|
+
wss.close();
|
|
1317
|
+
console.log("[Daemon] Server stopped");
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
464
1321
|
|
|
465
1322
|
exports.DELETE = DELETE;
|
|
1323
|
+
exports.IMAGE_ANALYSIS_PROMPT = IMAGE_ANALYSIS_PROMPT;
|
|
466
1324
|
exports.POST = POST;
|
|
1325
|
+
exports.analyzeImage = analyzeImage;
|
|
1326
|
+
exports.buildDetailedDomSelectionPrompt = buildDetailedDomSelectionPrompt;
|
|
1327
|
+
exports.buildDrawingToCodePrompt = buildDrawingToCodePrompt;
|
|
1328
|
+
exports.buildFastDomSelectionPrompt = buildFastDomSelectionPrompt;
|
|
1329
|
+
exports.buildGesturePrompt = buildGesturePrompt;
|
|
467
1330
|
exports.buildPromptFromAnnotation = buildPromptFromAnnotation;
|
|
468
1331
|
exports.createGeminiCLIStream = createGeminiCLIStream;
|
|
469
1332
|
exports.createGeminiRouteHandler = createGeminiRouteHandler;
|
|
470
1333
|
exports.createRevertRouteHandler = createRevertRouteHandler;
|
|
1334
|
+
exports.getAvailableProviders = getAvailableProviders;
|
|
471
1335
|
exports.getTrackedAnnotations = getTrackedAnnotations;
|
|
1336
|
+
exports.isProviderAvailable = isProviderAvailable;
|
|
1337
|
+
exports.isVisionAvailable = isVisionAvailable;
|
|
472
1338
|
exports.revertAnnotation = revertAnnotation;
|
|
1339
|
+
exports.runAICLI = runAICLI;
|
|
473
1340
|
exports.runGeminiCLI = runGeminiCLI;
|
|
1341
|
+
exports.spawnAICLI = spawnAICLI;
|
|
474
1342
|
exports.spawnGeminiCLI = spawnGeminiCLI;
|
|
1343
|
+
exports.startDaemon = startDaemon;
|
|
475
1344
|
//# sourceMappingURL=server.js.map
|
|
476
1345
|
//# sourceMappingURL=server.js.map
|