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