skema-core 0.1.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js ADDED
@@ -0,0 +1,413 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ require('dotenv/config');
5
+ var index_js = require('@modelcontextprotocol/sdk/server/index.js');
6
+ var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
7
+ var types_js = require('@modelcontextprotocol/sdk/types.js');
8
+ var WebSocket = require('ws');
9
+
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ var WebSocket__default = /*#__PURE__*/_interopDefault(WebSocket);
13
+
14
+ var DAEMON_PORT = parseInt(process.env.SKEMA_PORT || "9999", 10);
15
+ var DAEMON_URL = `ws://localhost:${DAEMON_PORT}`;
16
+ var daemonWs = null;
17
+ var daemonConnected = false;
18
+ var reconnectTimer = null;
19
+ var RECONNECT_INTERVAL = 3e3;
20
+ var messageIdCounter = 0;
21
+ var pendingRequests = /* @__PURE__ */ new Map();
22
+ function scheduleReconnect() {
23
+ if (reconnectTimer) return;
24
+ reconnectTimer = setTimeout(async () => {
25
+ reconnectTimer = null;
26
+ console.error("[Skema MCP] Attempting to reconnect to daemon...");
27
+ try {
28
+ await connectToDaemon();
29
+ } catch {
30
+ }
31
+ }, RECONNECT_INTERVAL);
32
+ }
33
+ function connectToDaemon() {
34
+ return new Promise((resolve, reject) => {
35
+ if (daemonWs && daemonConnected) {
36
+ resolve();
37
+ return;
38
+ }
39
+ if (daemonWs) {
40
+ daemonWs.removeAllListeners();
41
+ daemonWs = null;
42
+ }
43
+ daemonWs = new WebSocket__default.default(DAEMON_URL);
44
+ daemonWs.on("open", () => {
45
+ daemonConnected = true;
46
+ console.error("[Skema MCP] Connected to daemon at", DAEMON_URL);
47
+ daemonWs.send(JSON.stringify({
48
+ id: `mcp-identify-${Date.now()}`,
49
+ type: "identify",
50
+ client: "mcp-server"
51
+ }));
52
+ resolve();
53
+ });
54
+ daemonWs.on("message", (data) => {
55
+ try {
56
+ const msg = JSON.parse(data.toString());
57
+ if (msg.id && pendingRequests.has(msg.id)) {
58
+ const pending = pendingRequests.get(msg.id);
59
+ pendingRequests.delete(msg.id);
60
+ clearTimeout(pending.timeout);
61
+ if (msg.type === "error") {
62
+ pending.reject(new Error(msg.error));
63
+ } else {
64
+ pending.resolve(msg);
65
+ }
66
+ }
67
+ } catch (e) {
68
+ console.error("[Skema MCP] Failed to parse daemon message:", e);
69
+ }
70
+ });
71
+ daemonWs.on("close", () => {
72
+ daemonConnected = false;
73
+ daemonWs = null;
74
+ console.error("[Skema MCP] Disconnected from daemon");
75
+ scheduleReconnect();
76
+ });
77
+ daemonWs.on("error", (err) => {
78
+ daemonConnected = false;
79
+ console.error("[Skema MCP] Daemon connection error:", err.message);
80
+ reject(err);
81
+ });
82
+ });
83
+ }
84
+ function sendToDaemon(type, payload = {}) {
85
+ return new Promise(async (resolve, reject) => {
86
+ try {
87
+ if (!daemonConnected) {
88
+ await connectToDaemon();
89
+ }
90
+ if (!daemonWs || !daemonConnected) {
91
+ reject(new Error("Not connected to Skema daemon. Is it running?"));
92
+ return;
93
+ }
94
+ const id = `mcp-${++messageIdCounter}`;
95
+ const timeout = setTimeout(() => {
96
+ pendingRequests.delete(id);
97
+ reject(new Error(`Daemon request timed out: ${type}`));
98
+ }, 3e4);
99
+ pendingRequests.set(id, { resolve, reject, timeout });
100
+ daemonWs.send(JSON.stringify({ id, type, ...payload }));
101
+ } catch (err) {
102
+ reject(err);
103
+ }
104
+ });
105
+ }
106
+ var TOOLS = [
107
+ {
108
+ name: "skema_get_pending",
109
+ description: "Get all pending annotations from the Skema browser overlay. These are visual annotations (DOM selections, drawings, multi-selects) that the user has made on their web page and wants you to implement as code changes. Each annotation includes the user's comment describing what they want, plus element selectors, CSS paths, bounding boxes, and other context to help you find the right code to modify.",
110
+ inputSchema: {
111
+ type: "object",
112
+ properties: {},
113
+ required: []
114
+ }
115
+ },
116
+ {
117
+ name: "skema_get_all_annotations",
118
+ description: "Get all annotations (pending, acknowledged, resolved, dismissed). Useful for reviewing the full history of annotation requests and their current status.",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {},
122
+ required: []
123
+ }
124
+ },
125
+ {
126
+ name: "skema_get_annotation",
127
+ description: "Get details of a specific annotation by ID.",
128
+ inputSchema: {
129
+ type: "object",
130
+ properties: {
131
+ annotationId: {
132
+ type: "string",
133
+ description: "The annotation ID to look up"
134
+ }
135
+ },
136
+ required: ["annotationId"]
137
+ }
138
+ },
139
+ {
140
+ name: "skema_acknowledge",
141
+ description: "Mark an annotation as acknowledged. Use this to tell the user you've seen their annotation and are working on it. The browser overlay will update to show the status change.",
142
+ inputSchema: {
143
+ type: "object",
144
+ properties: {
145
+ annotationId: {
146
+ type: "string",
147
+ description: "The annotation ID to acknowledge"
148
+ }
149
+ },
150
+ required: ["annotationId"]
151
+ }
152
+ },
153
+ {
154
+ name: "skema_resolve",
155
+ description: "Mark an annotation as resolved after you've implemented the requested change. Include a summary of what you did so the user can see it in the browser overlay.",
156
+ inputSchema: {
157
+ type: "object",
158
+ properties: {
159
+ annotationId: {
160
+ type: "string",
161
+ description: "The annotation ID to resolve"
162
+ },
163
+ summary: {
164
+ type: "string",
165
+ description: "Summary of what was done to resolve this annotation"
166
+ }
167
+ },
168
+ required: ["annotationId"]
169
+ }
170
+ },
171
+ {
172
+ name: "skema_dismiss",
173
+ description: "Dismiss an annotation if you decide not to implement it. Include a reason so the user understands why.",
174
+ inputSchema: {
175
+ type: "object",
176
+ properties: {
177
+ annotationId: {
178
+ type: "string",
179
+ description: "The annotation ID to dismiss"
180
+ },
181
+ reason: {
182
+ type: "string",
183
+ description: "Reason for dismissing this annotation"
184
+ }
185
+ },
186
+ required: ["annotationId", "reason"]
187
+ }
188
+ },
189
+ {
190
+ name: "skema_watch",
191
+ description: "Wait for new annotations to appear. This blocks until the user creates new annotations in the browser overlay, then returns them as a batch. Use this in a loop for hands-free processing: call skema_watch, process the returned annotations, resolve them, then call skema_watch again.",
192
+ inputSchema: {
193
+ type: "object",
194
+ properties: {
195
+ timeoutSeconds: {
196
+ type: "number",
197
+ description: "Max seconds to wait for annotations (default: 120, max: 300)"
198
+ }
199
+ },
200
+ required: []
201
+ }
202
+ }
203
+ ];
204
+ function success(data) {
205
+ return {
206
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
207
+ };
208
+ }
209
+ function error(message) {
210
+ return {
211
+ content: [{ type: "text", text: message }],
212
+ isError: true
213
+ };
214
+ }
215
+ async function handleTool(name, args) {
216
+ switch (name) {
217
+ case "skema_get_pending": {
218
+ try {
219
+ const response = await sendToDaemon("get-pending-annotations");
220
+ return success({
221
+ count: response.count,
222
+ annotations: response.annotations
223
+ });
224
+ } catch (err) {
225
+ return error(`Failed to get pending annotations: ${err.message}`);
226
+ }
227
+ }
228
+ case "skema_get_all_annotations": {
229
+ try {
230
+ const response = await sendToDaemon("get-all-annotations");
231
+ return success({
232
+ count: response.count,
233
+ annotations: response.annotations
234
+ });
235
+ } catch (err) {
236
+ return error(`Failed to get annotations: ${err.message}`);
237
+ }
238
+ }
239
+ case "skema_get_annotation": {
240
+ try {
241
+ const response = await sendToDaemon("get-annotation", {
242
+ annotationId: args.annotationId
243
+ });
244
+ return success(response.annotation);
245
+ } catch (err) {
246
+ return error(`Failed to get annotation: ${err.message}`);
247
+ }
248
+ }
249
+ case "skema_acknowledge": {
250
+ try {
251
+ await sendToDaemon("acknowledge-annotation", {
252
+ annotationId: args.annotationId
253
+ });
254
+ return success({ acknowledged: true, annotationId: args.annotationId });
255
+ } catch (err) {
256
+ return error(`Failed to acknowledge: ${err.message}`);
257
+ }
258
+ }
259
+ case "skema_resolve": {
260
+ try {
261
+ await sendToDaemon("resolve-annotation", {
262
+ annotationId: args.annotationId,
263
+ summary: args.summary
264
+ });
265
+ return success({
266
+ resolved: true,
267
+ annotationId: args.annotationId,
268
+ summary: args.summary
269
+ });
270
+ } catch (err) {
271
+ return error(`Failed to resolve: ${err.message}`);
272
+ }
273
+ }
274
+ case "skema_dismiss": {
275
+ try {
276
+ await sendToDaemon("dismiss-annotation", {
277
+ annotationId: args.annotationId,
278
+ reason: args.reason
279
+ });
280
+ return success({
281
+ dismissed: true,
282
+ annotationId: args.annotationId,
283
+ reason: args.reason
284
+ });
285
+ } catch (err) {
286
+ return error(`Failed to dismiss: ${err.message}`);
287
+ }
288
+ }
289
+ case "skema_watch": {
290
+ const timeoutSeconds = Math.min(300, Math.max(1, args?.timeoutSeconds ?? 120));
291
+ const pollInterval = 2e3;
292
+ const maxPolls = Math.ceil(timeoutSeconds * 1e3 / pollInterval);
293
+ for (let i = 0; i < maxPolls; i++) {
294
+ try {
295
+ const response = await sendToDaemon("get-pending-annotations");
296
+ if (response.count > 0) {
297
+ return success({
298
+ timeout: false,
299
+ count: response.count,
300
+ annotations: response.annotations
301
+ });
302
+ }
303
+ } catch (err) {
304
+ return error(`Lost connection to daemon: ${err.message}`);
305
+ }
306
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
307
+ }
308
+ return success({
309
+ timeout: true,
310
+ message: `No new annotations within ${timeoutSeconds} seconds`
311
+ });
312
+ }
313
+ default:
314
+ return error(`Unknown tool: ${name}`);
315
+ }
316
+ }
317
+ async function startMcpServer() {
318
+ try {
319
+ await connectToDaemon();
320
+ } catch {
321
+ console.error("[Skema MCP] Warning: Could not connect to daemon. Will auto-retry...");
322
+ scheduleReconnect();
323
+ }
324
+ const server = new index_js.Server(
325
+ {
326
+ name: "skema",
327
+ version: "0.3.0"
328
+ },
329
+ {
330
+ capabilities: {
331
+ tools: {},
332
+ resources: {}
333
+ }
334
+ }
335
+ );
336
+ server.setRequestHandler(types_js.ListToolsRequestSchema, async () => {
337
+ return { tools: TOOLS };
338
+ });
339
+ server.setRequestHandler(types_js.CallToolRequestSchema, async (request) => {
340
+ const { name, arguments: args } = request.params;
341
+ try {
342
+ return await handleTool(name, args);
343
+ } catch (err) {
344
+ const message = err instanceof Error ? err.message : String(err);
345
+ return error(message);
346
+ }
347
+ });
348
+ server.setRequestHandler(types_js.ListResourcesRequestSchema, async () => {
349
+ return {
350
+ resources: [
351
+ {
352
+ uri: "skema://status",
353
+ name: "Skema Status",
354
+ description: "Current Skema daemon connection status and pending annotation count",
355
+ mimeType: "application/json"
356
+ }
357
+ ]
358
+ };
359
+ });
360
+ server.setRequestHandler(types_js.ReadResourceRequestSchema, async (request) => {
361
+ const { uri } = request.params;
362
+ switch (uri) {
363
+ case "skema://status": {
364
+ let pendingCount = 0;
365
+ let connected = false;
366
+ try {
367
+ const response = await sendToDaemon("get-pending-annotations");
368
+ pendingCount = response.count;
369
+ connected = true;
370
+ } catch {
371
+ connected = false;
372
+ }
373
+ return {
374
+ contents: [{
375
+ uri,
376
+ mimeType: "application/json",
377
+ text: JSON.stringify({
378
+ daemonConnected: connected,
379
+ daemonUrl: DAEMON_URL,
380
+ pendingAnnotations: pendingCount
381
+ }, null, 2)
382
+ }]
383
+ };
384
+ }
385
+ default:
386
+ throw new Error(`Unknown resource: ${uri}`);
387
+ }
388
+ });
389
+ server.oninitialized = () => {
390
+ const clientInfo = server.getClientVersion();
391
+ const clientName = clientInfo?.name || "unknown";
392
+ console.error("[Skema MCP] Connected client:", clientName, clientInfo?.version || "");
393
+ if (daemonWs && daemonConnected) {
394
+ daemonWs.send(JSON.stringify({
395
+ id: `mcp-client-info-${Date.now()}`,
396
+ type: "mcp-client-info",
397
+ clientName,
398
+ clientVersion: clientInfo?.version
399
+ }));
400
+ }
401
+ };
402
+ const transport = new stdio_js.StdioServerTransport();
403
+ await server.connect(transport);
404
+ console.error("[Skema MCP] Server started");
405
+ console.error("[Skema MCP] Daemon URL:", DAEMON_URL);
406
+ console.error("[Skema MCP] Tools: skema_get_pending, skema_acknowledge, skema_resolve, skema_dismiss, skema_watch");
407
+ }
408
+
409
+ // src/mcp/index.ts
410
+ startMcpServer().catch((error2) => {
411
+ console.error("[Skema MCP] Fatal error:", error2);
412
+ process.exit(1);
413
+ });
package/dist/server.d.mts CHANGED
@@ -369,7 +369,47 @@ declare function buildDrawingToCodePrompt(input: DrawingInput): string;
369
369
  */
370
370
  declare const IMAGE_ANALYSIS_PROMPT: string;
371
371
 
372
+ /**
373
+ * Execution modes:
374
+ * - 'direct-cli': Annotations processed instantly via CLI agents (gemini/claude CLI tools)
375
+ * - 'mcp': Annotations routed through an AI agent (Cursor, Claude Desktop, etc.)
376
+ */
377
+ type ExecutionMode = 'direct-cli' | 'mcp';
378
+ type ProviderName = 'gemini' | 'claude';
379
+
380
+ interface DaemonConfig {
381
+ /** Port for WebSocket server (default: 9999) */
382
+ port?: number;
383
+ /** Working directory for file operations and AI commands */
384
+ cwd?: string;
385
+ /** Default AI provider */
386
+ defaultProvider?: ProviderName;
387
+ /** Default execution mode */
388
+ defaultMode?: ExecutionMode;
389
+ }
390
+ interface IncomingMessage {
391
+ id: string;
392
+ type: string;
393
+ [key: string]: unknown;
394
+ }
395
+ interface OutgoingMessage {
396
+ id?: string;
397
+ type: string;
398
+ success?: boolean;
399
+ error?: string;
400
+ [key: string]: unknown;
401
+ }
402
+ interface DaemonInstance {
403
+ port: number;
404
+ close: () => void;
405
+ }
406
+ /**
407
+ * Start the Skema daemon (WebSocket server)
408
+ */
409
+ declare function startDaemon(config?: DaemonConfig): DaemonInstance;
410
+
372
411
  type AIProvider = 'gemini' | 'claude';
412
+ type AnyProvider = 'gemini' | 'claude';
373
413
  interface AIProviderConfig {
374
414
  provider: AIProvider;
375
415
  /** Working directory for CLI commands */
@@ -381,7 +421,7 @@ interface AIStreamEvent {
381
421
  type: 'init' | 'text' | 'tool_use' | 'tool_result' | 'error' | 'done' | 'debug';
382
422
  content?: string;
383
423
  timestamp: string;
384
- provider: AIProvider;
424
+ provider: AnyProvider;
385
425
  /** Raw event from the CLI (provider-specific) */
386
426
  raw?: unknown;
387
427
  }
@@ -411,35 +451,6 @@ declare function isProviderAvailable(provider: AIProvider): boolean;
411
451
  */
412
452
  declare function getAvailableProviders(): AIProvider[];
413
453
 
414
- interface DaemonConfig {
415
- /** Port for WebSocket server (default: 9999) */
416
- port?: number;
417
- /** Working directory for file operations and AI commands */
418
- cwd?: string;
419
- /** Default AI provider */
420
- defaultProvider?: AIProvider;
421
- }
422
- interface IncomingMessage {
423
- id: string;
424
- type: string;
425
- [key: string]: unknown;
426
- }
427
- interface OutgoingMessage {
428
- id?: string;
429
- type: string;
430
- success?: boolean;
431
- error?: string;
432
- [key: string]: unknown;
433
- }
434
- interface DaemonInstance {
435
- port: number;
436
- close: () => void;
437
- }
438
- /**
439
- * Start the Skema daemon (WebSocket server)
440
- */
441
- declare function startDaemon(config?: DaemonConfig): DaemonInstance;
442
-
443
454
  interface VisionAnalysisResult {
444
455
  success: boolean;
445
456
  description: string;
@@ -462,4 +473,69 @@ declare function analyzeImage(base64Image: string, config: VisionConfig): Promis
462
473
  */
463
474
  declare function isVisionAvailable(provider: AIProvider): boolean;
464
475
 
465
- export { type AIProvider, type AIProviderConfig, type AIRunResult, type AIStreamEvent, DELETE, type DaemonConfig, type DaemonInstance, type DetailedDomSelectionInput, type DomSelectionInput, type DrawingInput, type GeminiCLIEvent, type GeminiCLIOptions, type GestureInput, IMAGE_ANALYSIS_PROMPT, type IncomingMessage, type OutgoingMessage, POST, type ProjectContext, type VisionAnalysisResult, type VisionConfig, analyzeImage, buildDetailedDomSelectionPrompt, buildDrawingToCodePrompt, buildFastDomSelectionPrompt, buildGesturePrompt, buildPromptFromAnnotation, createGeminiCLIStream, createGeminiRouteHandler, createRevertRouteHandler, getAvailableProviders, getTrackedAnnotations, isProviderAvailable, isVisionAvailable, revertAnnotation, runAICLI, runGeminiCLI, spawnAICLI, spawnGeminiCLI, startDaemon };
476
+ type AnnotationStatus = 'pending' | 'acknowledged' | 'resolved' | 'dismissed';
477
+ interface StoredAnnotation {
478
+ /** The original Skema annotation data */
479
+ annotation: Annotation;
480
+ /** User comment describing the desired change */
481
+ comment: string;
482
+ /** Current status in the MCP workflow */
483
+ status: AnnotationStatus;
484
+ /** When it was queued */
485
+ createdAt: string;
486
+ /** When status last changed */
487
+ updatedAt: string;
488
+ /** If resolved/dismissed, who did it */
489
+ resolvedBy?: 'human' | 'agent';
490
+ /** Resolution summary (from agent) */
491
+ resolutionSummary?: string;
492
+ /** Dismissal reason (from agent) */
493
+ dismissalReason?: string;
494
+ }
495
+ type StoreListener = (event: string, annotation: StoredAnnotation) => void;
496
+ /**
497
+ * Queue an annotation (called when user submits in MCP mode)
498
+ */
499
+ declare function queueAnnotation(annotation: Annotation, comment: string): StoredAnnotation;
500
+ /**
501
+ * Get all pending annotations
502
+ */
503
+ declare function getPendingAnnotations(): StoredAnnotation[];
504
+ /**
505
+ * Get all annotations (any status)
506
+ */
507
+ declare function getAllAnnotations(): StoredAnnotation[];
508
+ /**
509
+ * Get a specific annotation by ID
510
+ */
511
+ declare function getAnnotation(id: string): StoredAnnotation | undefined;
512
+ /**
513
+ * Mark an annotation as acknowledged (agent has seen it)
514
+ */
515
+ declare function acknowledgeAnnotation(id: string): StoredAnnotation | undefined;
516
+ /**
517
+ * Mark an annotation as resolved (agent has implemented the change)
518
+ */
519
+ declare function resolveAnnotation(id: string, summary?: string): StoredAnnotation | undefined;
520
+ /**
521
+ * Dismiss an annotation (agent decided not to address it)
522
+ */
523
+ declare function dismissAnnotation(id: string, reason: string): StoredAnnotation | undefined;
524
+ /**
525
+ * Remove an annotation from the store
526
+ */
527
+ declare function removeAnnotation(id: string): StoredAnnotation | undefined;
528
+ /**
529
+ * Clear all annotations
530
+ */
531
+ declare function clearAnnotations(): void;
532
+ /**
533
+ * Subscribe to store events
534
+ */
535
+ declare function onStoreEvent(listener: StoreListener): () => void;
536
+ /**
537
+ * Get count of pending annotations
538
+ */
539
+ declare function getPendingCount(): number;
540
+
541
+ export { type AIProviderConfig, type AIRunResult, type AIStreamEvent, type AnnotationStatus, type AIProvider as CLIProvider, DELETE, type DaemonConfig, type DaemonInstance, type DetailedDomSelectionInput, type DomSelectionInput, type DrawingInput, type ExecutionMode, type GeminiCLIEvent, type GeminiCLIOptions, type GestureInput, IMAGE_ANALYSIS_PROMPT, type IncomingMessage, type OutgoingMessage, POST, type ProjectContext, type ProviderName, type StoredAnnotation, type VisionAnalysisResult, type VisionConfig, acknowledgeAnnotation, analyzeImage, buildDetailedDomSelectionPrompt, buildDrawingToCodePrompt, buildFastDomSelectionPrompt, buildGesturePrompt, buildPromptFromAnnotation, clearAnnotations, createGeminiCLIStream, createGeminiRouteHandler, createRevertRouteHandler, dismissAnnotation, getAllAnnotations, getAnnotation, getAvailableProviders as getCLIProviders, getPendingAnnotations, getPendingCount, getTrackedAnnotations, isProviderAvailable, isVisionAvailable, onStoreEvent, queueAnnotation, removeAnnotation, resolveAnnotation, revertAnnotation, runAICLI, runGeminiCLI, spawnAICLI, spawnGeminiCLI, startDaemon };