zelune 1.5.7 → 1.6.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/mcp/server.ts CHANGED
@@ -1,134 +1,198 @@
1
+ import fs from 'node:fs';
1
2
  import path from 'node:path';
2
- import { fileURLToPath } from 'node:url';
3
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import {
6
- CallToolRequestSchema,
7
- ListToolsRequestSchema,
8
- } from '@modelcontextprotocol/sdk/types.js';
3
+ import readline from 'node:readline';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
9
5
  import {
10
6
  discoverScreens,
11
7
  inspectElement,
12
8
  patchElement,
13
9
  reorderElement,
10
+ insertElement,
11
+ duplicateElement,
12
+ deleteElement,
13
+ type InsertElementPreset,
14
14
  type StyleValue,
15
15
  } from '../src/server/astEngine.js';
16
16
 
17
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
- const WORKSPACE_ROOT = path.resolve(__dirname, '..');
18
+ const DEFAULT_WORKSPACE_ROOT = path.resolve(__dirname, '..');
19
+
20
+ export interface JsonRpcRequest {
21
+ jsonrpc?: string;
22
+ id?: string | number | null;
23
+ method?: string;
24
+ params?: Record<string, unknown>;
25
+ }
19
26
 
20
- const server = new Server(
27
+ export interface JsonRpcResponse {
28
+ jsonrpc: '2.0';
29
+ id: string | number | null;
30
+ result?: unknown;
31
+ error?: {
32
+ code: number;
33
+ message: string;
34
+ };
35
+ }
36
+
37
+ export const MCP_TOOLS = [
21
38
  {
22
- name: 'zelune',
23
- version: '1.5.0',
39
+ name: 'list_canvas_screens',
40
+ description:
41
+ 'Lists all discovered React Native screens on the visual canvas along with their element counts and outgoing navigation flow edges.',
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {},
45
+ },
24
46
  },
25
47
  {
26
- capabilities: {
27
- tools: {},
28
- },
29
- }
30
- );
31
-
32
- server.setRequestHandler(ListToolsRequestSchema, async () => {
33
- return {
34
- tools: [
35
- {
36
- name: 'list_canvas_screens',
37
- description:
38
- 'Lists all discovered React Native screens on the visual canvas along with their element counts and outgoing navigation flow edges.',
39
- inputSchema: {
40
- type: 'object',
41
- properties: {},
48
+ name: 'inspect_screen_element',
49
+ description:
50
+ 'Inspects a specific JSX element inside a React Native screen by its relative file path and AST index path.',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ file: {
55
+ type: 'string',
56
+ description: 'Relative path to the screen file (e.g. src/screens/HomeScreen.tsx)',
57
+ },
58
+ astPath: {
59
+ type: 'string',
60
+ description: 'Preorder AST index of the JSX element (e.g. "0", "4")',
42
61
  },
43
62
  },
44
- {
45
- name: 'inspect_screen_element',
46
- description:
47
- 'Inspects a specific JSX element inside a React Native screen by its relative file path and AST index path.',
48
- inputSchema: {
63
+ required: ['file', 'astPath'],
64
+ },
65
+ },
66
+ {
67
+ name: 'patch_screen_element',
68
+ description:
69
+ 'Surgically patches the text copy or StyleSheet properties of a React Native JSX element.',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ file: {
74
+ type: 'string',
75
+ description: 'Relative path to the screen file (e.g. src/screens/HomeScreen.tsx)',
76
+ },
77
+ astPath: {
78
+ type: 'string',
79
+ description: 'Preorder AST index of the JSX element',
80
+ },
81
+ text: {
82
+ type: 'string',
83
+ description: 'Optional new static text content for <Text> elements',
84
+ },
85
+ styles: {
49
86
  type: 'object',
50
- properties: {
51
- file: {
52
- type: 'string',
53
- description: 'Relative path to the screen file (e.g. src/screens/HomeScreen.tsx)',
54
- },
55
- astPath: {
56
- type: 'string',
57
- description: 'Preorder AST index of the JSX element (e.g. "0", "4")',
58
- },
59
- },
60
- required: ['file', 'astPath'],
87
+ description: 'Optional map of React Native style properties to update',
61
88
  },
62
89
  },
63
- {
64
- name: 'patch_screen_element',
65
- description:
66
- 'Surgically patches the text copy or StyleSheet properties of a React Native JSX element.',
67
- inputSchema: {
68
- type: 'object',
69
- properties: {
70
- file: {
71
- type: 'string',
72
- description: 'Relative path to the screen file (e.g. src/screens/HomeScreen.tsx)',
73
- },
74
- astPath: {
75
- type: 'string',
76
- description: 'Preorder AST index of the JSX element',
77
- },
78
- text: {
79
- type: 'string',
80
- description: 'Optional new static text content for <Text> elements',
81
- },
82
- styles: {
83
- type: 'object',
84
- description: 'Optional map of React Native style properties to update',
85
- },
86
- },
87
- required: ['file', 'astPath'],
90
+ required: ['file', 'astPath'],
91
+ },
92
+ },
93
+ {
94
+ name: 'reorder_screen_element',
95
+ description:
96
+ 'Surgically reorders a React Native JSX element among its Flexbox siblings in the .tsx AST.',
97
+ inputSchema: {
98
+ type: 'object',
99
+ properties: {
100
+ file: {
101
+ type: 'string',
102
+ description: 'Relative path to the screen file (e.g. src/screens/HomeScreen.tsx)',
103
+ },
104
+ astPath: {
105
+ type: 'string',
106
+ description: 'Preorder AST index of the JSX element to move',
107
+ },
108
+ direction: {
109
+ type: 'string',
110
+ enum: ['up', 'down'],
111
+ description: 'Optional relative direction ("up" or "down") among siblings',
112
+ },
113
+ targetAstPath: {
114
+ type: 'string',
115
+ description: 'Optional AST index of the sibling to place before/after',
116
+ },
117
+ placement: {
118
+ type: 'string',
119
+ enum: ['before', 'after'],
120
+ description: 'Whether to insert before or after targetAstPath',
88
121
  },
89
122
  },
90
- {
91
- name: 'reorder_screen_element',
92
- description:
93
- 'Surgically reorders a React Native JSX element among its Flexbox siblings in the .tsx AST.',
94
- inputSchema: {
95
- type: 'object',
96
- properties: {
97
- file: {
98
- type: 'string',
99
- description: 'Relative path to the screen file (e.g. src/screens/HomeScreen.tsx)',
100
- },
101
- astPath: {
102
- type: 'string',
103
- description: 'Preorder AST index of the JSX element to move',
104
- },
105
- direction: {
106
- type: 'string',
107
- enum: ['up', 'down'],
108
- description: 'Optional relative direction ("up" or "down") among siblings',
109
- },
110
- targetAstPath: {
111
- type: 'string',
112
- description: 'Optional AST index of the sibling to place before/after',
113
- },
114
- placement: {
115
- type: 'string',
116
- enum: ['before', 'after'],
117
- description: 'Whether to insert before or after targetAstPath',
118
- },
119
- },
120
- required: ['file', 'astPath'],
123
+ required: ['file', 'astPath'],
124
+ },
125
+ },
126
+ {
127
+ name: 'insert_screen_element',
128
+ description:
129
+ 'Surgically inserts a new React Native component/section preset (button, section, row, heading, text, input, badge, divider) into a screen .tsx AST and auto-injects required react-native imports.',
130
+ inputSchema: {
131
+ type: 'object',
132
+ properties: {
133
+ file: {
134
+ type: 'string',
135
+ description: 'Relative path to the screen file (e.g. src/screens/HomeScreen.tsx)',
136
+ },
137
+ astPath: {
138
+ type: 'string',
139
+ description: 'Optional AST index of the reference JSX element',
140
+ },
141
+ placement: {
142
+ type: 'string',
143
+ enum: ['inside', 'after', 'before'],
144
+ description: 'Placement relative to astPath (defaults to "after" or "inside")',
145
+ },
146
+ preset: {
147
+ type: 'string',
148
+ enum: ['button', 'section', 'row', 'heading', 'text', 'input', 'badge', 'divider'],
149
+ description: 'Component preset to insert',
150
+ },
151
+ label: {
152
+ type: 'string',
153
+ description: 'Optional custom label/text/placeholder for the inserted component',
154
+ },
155
+ navigateTo: {
156
+ type: 'string',
157
+ description: 'Optional target screen ID if inserting a navigation button',
121
158
  },
122
159
  },
123
- ],
124
- };
125
- });
126
-
127
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
128
- const { name, arguments: args } = request.params;
160
+ required: ['file', 'preset'],
161
+ },
162
+ },
163
+ {
164
+ name: 'duplicate_screen_element',
165
+ description: 'Duplicates a JSX element right after itself in the screen .tsx AST.',
166
+ inputSchema: {
167
+ type: 'object',
168
+ properties: {
169
+ file: { type: 'string' },
170
+ astPath: { type: 'string' },
171
+ },
172
+ required: ['file', 'astPath'],
173
+ },
174
+ },
175
+ {
176
+ name: 'delete_screen_element',
177
+ description: 'Deletes a non-root JSX element from the screen .tsx AST.',
178
+ inputSchema: {
179
+ type: 'object',
180
+ properties: {
181
+ file: { type: 'string' },
182
+ astPath: { type: 'string' },
183
+ },
184
+ required: ['file', 'astPath'],
185
+ },
186
+ },
187
+ ];
129
188
 
189
+ async function executeMcpTool(
190
+ workspaceRoot: string,
191
+ name: string,
192
+ args: Record<string, unknown> | undefined
193
+ ): Promise<{ content: Array<{ type: 'text'; text: string }> }> {
130
194
  if (name === 'list_canvas_screens') {
131
- const screens = await discoverScreens(WORKSPACE_ROOT);
195
+ const screens = await discoverScreens(workspaceRoot);
132
196
  return {
133
197
  content: [{ type: 'text', text: JSON.stringify(screens, null, 2) }],
134
198
  };
@@ -137,7 +201,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
137
201
  if (name === 'inspect_screen_element') {
138
202
  const file = String(args?.file ?? '');
139
203
  const astPath = String(args?.astPath ?? '0');
140
- const element = await inspectElement(WORKSPACE_ROOT, { file, astPath });
204
+ const element = await inspectElement(workspaceRoot, { file, astPath });
141
205
  return {
142
206
  content: [{ type: 'text', text: JSON.stringify(element, null, 2) }],
143
207
  };
@@ -149,7 +213,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
149
213
  const text = typeof args?.text === 'string' ? args.text : undefined;
150
214
  const styles = (args?.styles as Record<string, StyleValue> | undefined) ?? undefined;
151
215
 
152
- const result = await patchElement(WORKSPACE_ROOT, {
216
+ const result = await patchElement(workspaceRoot, {
153
217
  file,
154
218
  astPath,
155
219
  text,
@@ -170,7 +234,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
170
234
  const placement =
171
235
  args?.placement === 'before' || args?.placement === 'after' ? args.placement : undefined;
172
236
 
173
- const result = await reorderElement(WORKSPACE_ROOT, {
237
+ const result = await reorderElement(workspaceRoot, {
174
238
  file,
175
239
  astPath,
176
240
  direction,
@@ -182,15 +246,201 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
182
246
  };
183
247
  }
184
248
 
249
+ if (name === 'insert_screen_element') {
250
+ const file = String(args?.file ?? '');
251
+ const preset = String(args?.preset ?? 'button') as InsertElementPreset;
252
+ const astPath = typeof args?.astPath === 'string' ? args.astPath : undefined;
253
+ const placement =
254
+ args?.placement === 'inside' || args?.placement === 'after' || args?.placement === 'before'
255
+ ? args.placement
256
+ : undefined;
257
+ const label = typeof args?.label === 'string' ? args.label : undefined;
258
+ const navigateTo = typeof args?.navigateTo === 'string' ? args.navigateTo : undefined;
259
+
260
+ const result = await insertElement(workspaceRoot, {
261
+ file,
262
+ astPath,
263
+ placement,
264
+ preset,
265
+ label,
266
+ navigateTo,
267
+ });
268
+ return {
269
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
270
+ };
271
+ }
272
+
273
+ if (name === 'duplicate_screen_element') {
274
+ const file = String(args?.file ?? '');
275
+ const astPath = String(args?.astPath ?? '1');
276
+ const result = await duplicateElement(workspaceRoot, { file, astPath });
277
+ return {
278
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
279
+ };
280
+ }
281
+
282
+ if (name === 'delete_screen_element') {
283
+ const file = String(args?.file ?? '');
284
+ const astPath = String(args?.astPath ?? '1');
285
+ const result = await deleteElement(workspaceRoot, { file, astPath });
286
+ return {
287
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
288
+ };
289
+ }
290
+
185
291
  throw new Error(`Unknown MCP tool: ${name}`);
186
- });
292
+ }
293
+
294
+ export async function handleMcpJsonRpcRequest(
295
+ request: JsonRpcRequest,
296
+ customWorkspaceRoot?: string
297
+ ): Promise<JsonRpcResponse | null> {
298
+ if (request.id === undefined) {
299
+ // Notification (e.g. notifications/initialized)
300
+ return null;
301
+ }
302
+
303
+ const workspaceRoot =
304
+ customWorkspaceRoot ||
305
+ process.env.ANTIGRAVITY_TARGET_PROJECT ||
306
+ process.env.ANTIGRAVITY_WORKSPACE_ROOT ||
307
+ DEFAULT_WORKSPACE_ROOT;
308
+
309
+ const method = request.method ?? '';
187
310
 
188
- async function main() {
189
- const transport = new StdioServerTransport();
190
- await server.connect(transport);
311
+ try {
312
+ if (method === 'initialize') {
313
+ const requestedProtocol =
314
+ typeof request.params?.protocolVersion === 'string'
315
+ ? request.params.protocolVersion
316
+ : '2024-11-05';
317
+ return {
318
+ jsonrpc: '2.0',
319
+ id: request.id,
320
+ result: {
321
+ protocolVersion: requestedProtocol,
322
+ capabilities: {
323
+ tools: {},
324
+ },
325
+ serverInfo: {
326
+ name: 'zelune',
327
+ version: '1.6.1',
328
+ },
329
+ },
330
+ };
331
+ }
332
+
333
+ if (method === 'ping') {
334
+ return {
335
+ jsonrpc: '2.0',
336
+ id: request.id,
337
+ result: {},
338
+ };
339
+ }
340
+
341
+ if (method === 'tools/list') {
342
+ return {
343
+ jsonrpc: '2.0',
344
+ id: request.id,
345
+ result: {
346
+ tools: MCP_TOOLS,
347
+ },
348
+ };
349
+ }
350
+
351
+ if (method === 'resources/list') {
352
+ return {
353
+ jsonrpc: '2.0',
354
+ id: request.id,
355
+ result: {
356
+ resources: [],
357
+ },
358
+ };
359
+ }
360
+
361
+ if (method === 'prompts/list') {
362
+ return {
363
+ jsonrpc: '2.0',
364
+ id: request.id,
365
+ result: {
366
+ prompts: [],
367
+ },
368
+ };
369
+ }
370
+
371
+ if (method === 'tools/call') {
372
+ const toolName = String(request.params?.name ?? '');
373
+ const toolArgs =
374
+ (request.params?.arguments as Record<string, unknown> | undefined) ?? undefined;
375
+ const toolResult = await executeMcpTool(workspaceRoot, toolName, toolArgs);
376
+ return {
377
+ jsonrpc: '2.0',
378
+ id: request.id,
379
+ result: toolResult,
380
+ };
381
+ }
382
+
383
+ return {
384
+ jsonrpc: '2.0',
385
+ id: request.id,
386
+ error: {
387
+ code: -32601,
388
+ message: `Method not found: ${method}`,
389
+ },
390
+ };
391
+ } catch (err) {
392
+ const message = err instanceof Error ? err.message : String(err);
393
+ return {
394
+ jsonrpc: '2.0',
395
+ id: request.id,
396
+ error: {
397
+ code: -32603,
398
+ message,
399
+ },
400
+ };
401
+ }
191
402
  }
192
403
 
193
- main().catch((err) => {
194
- console.error('MCP Server Error:', err);
195
- process.exit(1);
196
- });
404
+ export function startMcpServer(customWorkspaceRoot?: string): void {
405
+ const rl = readline.createInterface({
406
+ input: process.stdin,
407
+ terminal: false,
408
+ });
409
+
410
+ rl.on('line', async (line) => {
411
+ const trimmed = line.trim();
412
+ if (!trimmed) return;
413
+
414
+ let parsed: JsonRpcRequest;
415
+ try {
416
+ parsed = JSON.parse(trimmed) as JsonRpcRequest;
417
+ } catch {
418
+ const parseErr: JsonRpcResponse = {
419
+ jsonrpc: '2.0',
420
+ id: null,
421
+ error: { code: -32700, message: 'Parse error' },
422
+ };
423
+ process.stdout.write(`${JSON.stringify(parseErr)}\n`);
424
+ return;
425
+ }
426
+
427
+ const response = await handleMcpJsonRpcRequest(parsed, customWorkspaceRoot);
428
+ if (response) {
429
+ process.stdout.write(`${JSON.stringify(response)}\n`);
430
+ }
431
+ });
432
+ }
433
+
434
+ const isDirectExecution =
435
+ Boolean(process.argv[1]) &&
436
+ (() => {
437
+ try {
438
+ return import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href;
439
+ } catch {
440
+ return false;
441
+ }
442
+ })();
443
+
444
+ if (isDirectExecution) {
445
+ startMcpServer();
446
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zelune",
3
- "version": "1.5.7",
3
+ "version": "1.6.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -59,14 +59,12 @@
59
59
  "@babel/parser": "^7.25.0",
60
60
  "@babel/traverse": "^7.25.0",
61
61
  "@babel/types": "^7.25.0",
62
- "@modelcontextprotocol/sdk": "^1.0.1",
63
- "@vitejs/plugin-react": "^4.3.2",
62
+ "@vitejs/plugin-react": "^4.7.0",
64
63
  "lucide-react": "^0.454.0",
65
64
  "react": "^18.3.1",
66
65
  "react-dom": "^18.3.1",
67
66
  "react-native-web": "^0.19.12",
68
- "tsx": "^4.19.1",
69
- "vite": "^5.4.8"
67
+ "vite": "6.4.4"
70
68
  },
71
69
  "devDependencies": {
72
70
  "@types/babel__generator": "^7.6.8",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zelune",
3
3
  "displayName": "Zelune — React Native Visual Canvas",
4
- "version": "1.5.6",
4
+ "version": "1.6.2",
5
5
  "description": "Zelune MCP & Figma-style multi-screen React Native visual canvas with live surgical AST patching and navigation flow visualization for Google Antigravity.",
6
6
  "logo": "assets/logo.svg",
7
7
  "suggestedPrompts": [
@@ -1,6 +1,6 @@
1
1
  import { parse } from '@babel/parser';
2
+ import * as LucideIcons from 'lucide-react';
2
3
  import fs from 'node:fs';
3
- import { createRequire } from 'node:module';
4
4
  import path from 'node:path';
5
5
  import type { Plugin } from 'vite';
6
6
 
@@ -279,16 +279,7 @@ export function viteReactNativeFallbackPlugin(
279
279
  const zeluneDemoScreensDir = path.resolve(zelunePackageRoot, 'src/screens');
280
280
 
281
281
  const knownShimExports = extractModuleNamedExports(mockNavShimPath);
282
- const reqFromPlugin = createRequire(import.meta.url);
283
- const lucideExportKeys = new Set<string>();
284
- try {
285
- const lucideMod = reqFromPlugin('lucide-react') as Record<string, unknown>;
286
- for (const k of Object.keys(lucideMod)) {
287
- lucideExportKeys.add(k);
288
- }
289
- } catch {
290
- // Ignore if lucide-react cannot be eagerly inspected
291
- }
282
+ const lucideExportKeys = new Set<string>(Object.keys(LucideIcons));
292
283
 
293
284
  const uniqueRoots = Array.from(
294
285
  new Set([path.resolve(targetProjectRoot), path.resolve(workspaceRoot)])