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/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
- // src/server/gemini-cli.ts
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, projectContext, options) {
296
+ function buildPromptFromAnnotation(annotation, _projectContext, options) {
70
297
  const fastMode = options?.fastMode ?? true;
71
298
  if (annotation.type === "drawing") {
72
- return buildDrawingPrompt(annotation, projectContext, options?.visionDescription);
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 (fastMode) {
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
- prompt += `gesture: ${gestureAnnotation.gesture || "unknown"} at (${gestureAnnotation.boundingBox?.x}, ${gestureAnnotation.boundingBox?.y})`;
103
- } else {
104
- prompt += `annotation at (${annotation.boundingBox?.x}, ${annotation.boundingBox?.y})`;
105
- }
106
- prompt += `
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
- let imageNote = hasImage ? "\n**[Drawing image provided as base64 PNG with labeled grid overlay]**" : "";
155
- if (visionDescription) {
156
- imageNote += `
157
-
158
- ## Visual Analysis of Drawing
159
- ${visionDescription}`;
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
- const prompt = `Your task is to interpret a user's sketch/wireframe and turn it into code that is integrated in the codebase.
162
-
163
- ## User's Request
164
- "${comment}"
165
-
166
- ## Drawing Context
167
- ${positionContext}${textContext}${nearbyContext}${imageNote}
168
-
169
- ## Your Process
170
- 1. **Analyze the Sketch:** Understand the visual intent\u2014what UI component does the user want?
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
- imageAnalysisPrompt,
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: "\u{1F3A8} Analyzing drawing image with Gemini Vision...",
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: `\u{1F441}\uFE0F Visual Analysis:
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