@createlex/createlexgenai 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,22 +63,22 @@ You need **at least one** of these enabled in your Unreal project. Both are buil
63
63
 
64
64
  1. Open your UE project
65
65
  2. Go to **Edit > Plugins**
66
- 3. Search for **"Web Remote Control"** and enable it
66
+ 3. Enable **"Remote Control API"** and **"Remote Control Web Interface"**
67
67
  4. Restart the editor
68
68
  5. Go to **Edit > Project Settings > Plugins > Remote Control**
69
- 6. Check **"Enable Remote Control Web Server"**
70
- 7. Set port to **30010** (default)
71
- 8. Ensure **"Enable HTTP Server"** is checked
69
+ 6. Under **Remote Control Web Server**, check **"Auto Start Web Server"**
70
+ 7. Set **"Remote Control HTTP Server Port"** to **30010** (default)
71
+ 8. Under **Remote Control > Security**, check **"Enable Remote Python Execution"**
72
72
 
73
73
  #### Option B: Python Remote Execution
74
74
 
75
75
  1. Open your UE project
76
76
  2. Go to **Edit > Plugins**
77
- 3. Search for **"Python Editor Script Plugin"** and enable it
77
+ 3. Enable **"Python Editor Script Plugin"**
78
78
  4. Restart the editor
79
79
  5. Go to **Edit > Project Settings > Plugins > Python**
80
- 6. Check **"Enable Remote Execution"**
81
- 7. Multicast group: `239.0.0.1`, port: `6766` (defaults)
80
+ 6. Under **Python Remote Execution**, check **"Enable Remote Execution?"**
81
+ 7. Multicast Group Endpoint: `239.0.0.1:6766`, Multicast Bind Address: `127.0.0.1` (defaults)
82
82
 
83
83
  > **Tip:** Enable both for maximum reliability. If one backend is unavailable, the CLI automatically falls back to the other.
84
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@createlex/createlexgenai",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "CLI tool and MCP server for CreatelexGenAI — Unreal Engine AI integration",
5
5
  "bin": {
6
6
  "createlex": "./bin/createlex.js"
@@ -4,21 +4,58 @@ const dgram = require('dgram');
4
4
  const net = require('net');
5
5
  const crypto = require('crypto');
6
6
 
7
- // UE Python Remote Execution protocol constants
7
+ // UE Python Remote Execution protocol constants (from remote_execution.py)
8
8
  const PROTOCOL_MAGIC = 'ue_py';
9
9
  const PROTOCOL_VERSION = 1;
10
10
 
11
11
  const MULTICAST_GROUP = '239.0.0.1';
12
12
  const MULTICAST_PORT = 6766;
13
- const COMMAND_PORT = 6776;
13
+ const MULTICAST_BIND_ADDRESS = '127.0.0.1';
14
+ const MULTICAST_TTL = 0;
15
+
16
+ const DEFAULT_COMMAND_ENDPOINT = ['127.0.0.1', 6776];
17
+ const DEFAULT_RECEIVE_BUFFER_SIZE = 8192;
14
18
 
15
19
  const DISCOVERY_TIMEOUT = 3000;
16
- const CONNECT_TIMEOUT = 10000;
17
20
  const COMMAND_TIMEOUT = 30000;
18
21
 
22
+ /**
23
+ * Build a protocol message matching UE's _RemoteExecutionMessage.to_json() format.
24
+ */
25
+ function buildMessage(type, source, dest, data) {
26
+ const msg = {
27
+ version: PROTOCOL_VERSION,
28
+ magic: PROTOCOL_MAGIC,
29
+ type: type,
30
+ source: source
31
+ };
32
+ if (dest) {
33
+ msg.dest = dest;
34
+ }
35
+ if (data) {
36
+ msg.data = data;
37
+ }
38
+ return JSON.stringify(msg);
39
+ }
40
+
41
+ /**
42
+ * Parse and validate a received protocol message.
43
+ */
44
+ function parseMessage(buf) {
45
+ try {
46
+ const msg = JSON.parse(buf.toString('utf-8'));
47
+ if (msg.version !== PROTOCOL_VERSION) return null;
48
+ if (msg.magic !== PROTOCOL_MAGIC) return null;
49
+ if (!msg.type || !msg.source) return null;
50
+ return msg;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
19
56
  /**
20
57
  * Discover running Unreal Engine instances via UDP multicast ping.
21
- * Returns an array of discovered nodes.
58
+ * Matches UE's _RemoteExecutionBroadcastConnection protocol exactly.
22
59
  */
23
60
  function discover(timeout = DISCOVERY_TIMEOUT) {
24
61
  return new Promise((resolve) => {
@@ -28,56 +65,62 @@ function discover(timeout = DISCOVERY_TIMEOUT) {
28
65
  const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
29
66
 
30
67
  const timer = setTimeout(() => {
31
- socket.close();
68
+ try { socket.close(); } catch {}
32
69
  resolve(nodes);
33
70
  }, timeout);
34
71
 
35
72
  socket.on('message', (msg) => {
36
- try {
37
- const data = JSON.parse(msg.toString());
38
- if (data.magic === PROTOCOL_MAGIC && data.type === 'pong') {
39
- // Avoid duplicates
40
- if (!nodes.find(n => n.node_id === data.source)) {
41
- nodes.push({
42
- node_id: data.source,
43
- engine_version: data.engine_version || 'unknown',
44
- machine: data.machine || 'unknown',
45
- user: data.user || 'unknown'
46
- });
47
- }
48
- }
49
- } catch {
50
- // Ignore non-JSON or malformed messages
73
+ const message = parseMessage(msg);
74
+ if (!message) return;
75
+
76
+ // Filter: must not be from self, must be a pong
77
+ if (message.source === nodeId) return;
78
+ if (message.dest && message.dest !== nodeId) return;
79
+ if (message.type !== 'pong') return;
80
+
81
+ // Avoid duplicates
82
+ if (!nodes.find(n => n.node_id === message.source)) {
83
+ nodes.push({
84
+ node_id: message.source,
85
+ ...(message.data || {})
86
+ });
51
87
  }
52
88
  });
53
89
 
54
90
  socket.on('error', () => {
55
91
  clearTimeout(timer);
56
- socket.close();
92
+ try { socket.close(); } catch {}
57
93
  resolve(nodes);
58
94
  });
59
95
 
60
- socket.bind(0, '0.0.0.0', () => {
96
+ // Bind to the same port and address as UE expects
97
+ socket.bind(MULTICAST_PORT, MULTICAST_BIND_ADDRESS, () => {
61
98
  try {
62
- socket.addMembership(MULTICAST_GROUP);
99
+ // Match UE's socket options
100
+ socket.setMulticastLoopback(true);
101
+ socket.setMulticastTTL(MULTICAST_TTL);
102
+ socket.setMulticastInterface(MULTICAST_BIND_ADDRESS);
103
+ socket.addMembership(MULTICAST_GROUP, MULTICAST_BIND_ADDRESS);
63
104
  } catch {
64
105
  // Multicast may not be available
65
106
  }
66
107
 
67
- const ping = JSON.stringify({
68
- magic: PROTOCOL_MAGIC,
69
- version: PROTOCOL_VERSION,
70
- type: 'ping',
71
- source: nodeId
72
- });
73
-
108
+ // Send ping (matches UE's _broadcast_ping)
109
+ const ping = buildMessage('ping', nodeId);
74
110
  socket.send(ping, 0, ping.length, MULTICAST_PORT, MULTICAST_GROUP, (err) => {
75
111
  if (err) {
76
112
  clearTimeout(timer);
77
- socket.close();
113
+ try { socket.close(); } catch {}
78
114
  resolve(nodes);
79
115
  }
80
116
  });
117
+
118
+ // Send a second ping after 1 second (UE pings every _NODE_PING_SECONDS = 1)
119
+ setTimeout(() => {
120
+ try {
121
+ socket.send(ping, 0, ping.length, MULTICAST_PORT, MULTICAST_GROUP);
122
+ } catch {}
123
+ }, 1000);
81
124
  });
82
125
  });
83
126
  }
@@ -85,22 +128,18 @@ function discover(timeout = DISCOVERY_TIMEOUT) {
85
128
  /**
86
129
  * Execute a Python command on a discovered UE instance via the Remote Execution protocol.
87
130
  *
88
- * Steps:
89
- * 1. Send open_connection via UDP multicast targeting the node
90
- * 2. Listen on a TCP socket for the UE instance to connect back
91
- * 3. Send the command as JSON over TCP
92
- * 4. Receive the result
93
- *
94
- * @param {string} command - Python code to execute
95
- * @param {object} options
96
- * @param {string} options.nodeId - Target node ID from discovery (optional, broadcasts if omitted)
97
- * @param {string} options.execMode - 'ExecuteStatement', 'EvaluateStatement', or 'ExecuteFile'
98
- * @param {number} options.timeout - Command timeout in ms
131
+ * Matches UE's _RemoteExecutionCommandConnection flow:
132
+ * 1. Start TCP listen server on command_endpoint
133
+ * 2. Send open_connection via UDP multicast targeting the node
134
+ * 3. Accept TCP connection from UE
135
+ * 4. Send command message over TCP
136
+ * 5. Receive command_result over TCP
99
137
  */
100
138
  function executeCommand(command, options = {}) {
101
139
  const {
102
140
  nodeId = null,
103
- execMode = 'ExecuteStatement',
141
+ execMode = 'ExecuteFile',
142
+ unattended = true,
104
143
  timeout = COMMAND_TIMEOUT
105
144
  } = options;
106
145
 
@@ -108,104 +147,162 @@ function executeCommand(command, options = {}) {
108
147
 
109
148
  return new Promise((resolve, reject) => {
110
149
  const timer = setTimeout(() => {
111
- reject(new Error('Remote execution timed out. Ensure "Allow Python Remote Execution" is enabled in UE Project Settings > Python.'));
150
+ cleanup();
151
+ reject(new Error('Remote execution timed out. Ensure "Enable Remote Execution" is checked in UE Project Settings > Python.'));
112
152
  }, timeout);
113
153
 
114
- // Step 1: Create TCP server to receive the connection from UE
115
- const tcpServer = net.createServer((socket) => {
116
- let responseBuffer = '';
154
+ let tcpServer = null;
155
+ let udpSocket = null;
117
156
 
157
+ function cleanup() {
158
+ clearTimeout(timer);
159
+ try { if (tcpServer) tcpServer.close(); } catch {}
160
+ try { if (udpSocket) udpSocket.close(); } catch {}
161
+ }
162
+
163
+ // Step 1: Create TCP server to accept connection from UE
164
+ tcpServer = net.createServer((socket) => {
165
+ socket.setNoDelay(true);
166
+ let responseBuffer = Buffer.alloc(0);
167
+
168
+ // Step 4: Send command once UE connects
169
+ const commandMsg = buildMessage('command', clientId, nodeId || '', {
170
+ command: command,
171
+ unattended: unattended,
172
+ exec_mode: execMode
173
+ });
174
+ socket.write(commandMsg, 'utf-8');
175
+
176
+ // Step 5: Receive command_result
118
177
  socket.on('data', (data) => {
119
- responseBuffer += data.toString();
178
+ responseBuffer = Buffer.concat([responseBuffer, data]);
120
179
 
121
- // Try to parse complete JSON
122
- try {
123
- const result = JSON.parse(responseBuffer);
124
- clearTimeout(timer);
125
- tcpServer.close();
180
+ const message = parseMessage(responseBuffer);
181
+ if (message && message.type === 'command_result') {
182
+ cleanup();
126
183
  socket.destroy();
127
-
128
- if (result.type === 'command_result') {
129
- resolve({
130
- success: result.success !== false,
131
- output: result.output || '',
132
- result: result.result || null,
133
- outputLog: result.output_log || []
134
- });
135
- } else {
136
- resolve({ success: true, raw: result });
137
- }
138
- } catch {
139
- // Keep buffering
184
+ resolve({
185
+ success: message.data ? message.data.success !== false : true,
186
+ output: message.data?.output || '',
187
+ result: message.data?.result || null,
188
+ outputLog: message.data?.output_log || []
189
+ });
140
190
  }
141
191
  });
142
192
 
143
193
  socket.on('error', (err) => {
144
- clearTimeout(timer);
145
- tcpServer.close();
146
- reject(new Error(`TCP socket error: ${err.message}`));
194
+ cleanup();
195
+ reject(new Error(`TCP command socket error: ${err.message}`));
147
196
  });
148
-
149
- // Step 3: Once UE connects, send the command
150
- const commandMsg = JSON.stringify({
151
- magic: PROTOCOL_MAGIC,
152
- version: PROTOCOL_VERSION,
153
- type: 'command',
154
- source: clientId,
155
- command: command,
156
- unattended: false,
157
- exec_mode: execMode
158
- });
159
-
160
- socket.write(commandMsg);
161
197
  });
162
198
 
163
199
  tcpServer.on('error', (err) => {
164
- clearTimeout(timer);
200
+ cleanup();
165
201
  reject(new Error(`TCP server error: ${err.message}`));
166
202
  });
167
203
 
168
- // Step 2: Start TCP server, then send open_connection via UDP
169
- tcpServer.listen(0, '127.0.0.1', () => {
170
- const tcpPort = tcpServer.address().port;
204
+ // Listen on the command endpoint
205
+ const commandPort = DEFAULT_COMMAND_ENDPOINT[1];
206
+ const commandHost = DEFAULT_COMMAND_ENDPOINT[0];
171
207
 
172
- const udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
208
+ tcpServer.listen(commandPort, commandHost, () => {
209
+ // Step 2: Send open_connection via UDP multicast
210
+ udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
173
211
 
174
- udpSocket.bind(0, '0.0.0.0', () => {
212
+ udpSocket.bind(0, MULTICAST_BIND_ADDRESS, () => {
175
213
  try {
176
- udpSocket.addMembership(MULTICAST_GROUP);
177
- } catch {
178
- // OK if multicast not available
179
- }
214
+ udpSocket.setMulticastLoopback(true);
215
+ udpSocket.setMulticastTTL(MULTICAST_TTL);
216
+ udpSocket.setMulticastInterface(MULTICAST_BIND_ADDRESS);
217
+ udpSocket.addMembership(MULTICAST_GROUP, MULTICAST_BIND_ADDRESS);
218
+ } catch {}
180
219
 
181
- const openMsg = JSON.stringify({
182
- magic: PROTOCOL_MAGIC,
183
- version: PROTOCOL_VERSION,
184
- type: 'open_connection',
185
- source: clientId,
186
- dest: nodeId || '',
187
- command_ip: '127.0.0.1',
188
- command_port: tcpPort
220
+ // Send open_connection (UE retries 6 times with 5s waits)
221
+ const openMsg = buildMessage('open_connection', clientId, nodeId || '', {
222
+ command_ip: commandHost,
223
+ command_port: commandPort
189
224
  });
190
225
 
191
226
  udpSocket.send(openMsg, 0, openMsg.length, MULTICAST_PORT, MULTICAST_GROUP, () => {
192
- udpSocket.close();
227
+ try { udpSocket.close(); } catch {}
228
+ udpSocket = null;
193
229
  });
194
230
  });
195
231
  });
232
+
233
+ // If command port is in use, try a random port
234
+ tcpServer.on('error', (err) => {
235
+ if (err.code === 'EADDRINUSE') {
236
+ tcpServer = net.createServer((socket) => {
237
+ socket.setNoDelay(true);
238
+ let responseBuffer = Buffer.alloc(0);
239
+
240
+ const commandMsg = buildMessage('command', clientId, nodeId || '', {
241
+ command: command,
242
+ unattended: unattended,
243
+ exec_mode: execMode
244
+ });
245
+ socket.write(commandMsg, 'utf-8');
246
+
247
+ socket.on('data', (data) => {
248
+ responseBuffer = Buffer.concat([responseBuffer, data]);
249
+ const message = parseMessage(responseBuffer);
250
+ if (message && message.type === 'command_result') {
251
+ cleanup();
252
+ socket.destroy();
253
+ resolve({
254
+ success: message.data ? message.data.success !== false : true,
255
+ output: message.data?.output || '',
256
+ result: message.data?.result || null,
257
+ outputLog: message.data?.output_log || []
258
+ });
259
+ }
260
+ });
261
+
262
+ socket.on('error', (err2) => {
263
+ cleanup();
264
+ reject(new Error(`TCP command socket error: ${err2.message}`));
265
+ });
266
+ });
267
+
268
+ tcpServer.listen(0, commandHost, () => {
269
+ const actualPort = tcpServer.address().port;
270
+ udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
271
+
272
+ udpSocket.bind(0, MULTICAST_BIND_ADDRESS, () => {
273
+ try {
274
+ udpSocket.setMulticastLoopback(true);
275
+ udpSocket.setMulticastTTL(MULTICAST_TTL);
276
+ udpSocket.setMulticastInterface(MULTICAST_BIND_ADDRESS);
277
+ udpSocket.addMembership(MULTICAST_GROUP, MULTICAST_BIND_ADDRESS);
278
+ } catch {}
279
+
280
+ const openMsg = buildMessage('open_connection', clientId, nodeId || '', {
281
+ command_ip: commandHost,
282
+ command_port: actualPort
283
+ });
284
+
285
+ udpSocket.send(openMsg, 0, openMsg.length, MULTICAST_PORT, MULTICAST_GROUP, () => {
286
+ try { udpSocket.close(); } catch {}
287
+ udpSocket = null;
288
+ });
289
+ });
290
+ });
291
+ }
292
+ });
196
293
  });
197
294
  }
198
295
 
199
296
  /**
200
- * Quick test: discover + execute a simple command.
297
+ * Quick test: discover remote nodes.
201
298
  */
202
299
  async function testConnection() {
203
300
  try {
204
- const nodes = await discover(2000);
301
+ const nodes = await discover(3000);
205
302
  if (nodes.length === 0) {
206
303
  return {
207
304
  available: false,
208
- error: 'No UE instances found via Remote Execution. Enable "Python Editor Script Plugin" and "Allow Python Remote Execution" in Project Settings.'
305
+ error: 'No UE instances found via Remote Execution. Enable "Python Editor Script Plugin" and check "Enable Remote Execution" in Project Settings > Python.'
209
306
  };
210
307
  }
211
308
  return {
@@ -223,6 +320,5 @@ module.exports = {
223
320
  executeCommand,
224
321
  testConnection,
225
322
  MULTICAST_GROUP,
226
- MULTICAST_PORT,
227
- COMMAND_PORT
323
+ MULTICAST_PORT
228
324
  };
@@ -171,14 +171,18 @@ async function executeWithBackend(toolName, params = {}, options = {}) {
171
171
 
172
172
  if (backend === BACKEND.WEB_REMOTE) {
173
173
  // Web Remote Control — translate tool calls to HTTP API calls
174
+ // Don't pass plugin port to web remote; let it use its own default (30010)
174
175
  const webRemote = require('./web-remote-control');
175
- return executeViaWebRemote(webRemote, toolName, params, options);
176
+ const webOpts = { ...options, port: options.webRemotePort || undefined };
177
+ return executeViaWebRemote(webRemote, toolName, params, webOpts);
176
178
  }
177
179
 
178
180
  if (backend === BACKEND.REMOTE_EXEC) {
179
181
  // Python Remote Execution — generate Python code and execute
182
+ // Don't pass plugin port to remote exec; it uses UDP multicast
180
183
  const remoteExec = require('./remote-execution');
181
- return executeViaRemoteExec(remoteExec, toolName, params, options);
184
+ const execOpts = { ...options, port: undefined };
185
+ return executeViaRemoteExec(remoteExec, toolName, params, execOpts);
182
186
  }
183
187
 
184
188
  // Auto-detect
@@ -196,9 +200,6 @@ async function executeWithBackend(toolName, params = {}, options = {}) {
196
200
  async function executeViaWebRemote(webRemote, toolName, params, options) {
197
201
  // Map common tool names to Web Remote Control API calls
198
202
  switch (toolName) {
199
- case 'get_all_scene_objects':
200
- return webRemote.getAllLevelActors(options);
201
-
202
203
  case 'execute_python_script':
203
204
  return webRemote.executePythonScript(params.script, options);
204
205
 
@@ -206,12 +207,10 @@ async function executeViaWebRemote(webRemote, toolName, params, options) {
206
207
  return webRemote.executeConsoleCommand(params.command, options);
207
208
 
208
209
  default: {
209
- // For tools that don't have a direct mapping, try to execute via Python
210
+ // For all tools (including get_all_scene_objects, spawn_object, etc.),
211
+ // generate standalone UE Python and execute via Python subsystem
210
212
  const script = buildPythonForTool(toolName, params);
211
- if (script) {
212
- return webRemote.executePythonScript(script, options);
213
- }
214
- throw new Error(`Tool '${toolName}' is not supported via Web Remote Control. Use the CreatelexGenAI plugin for full tool access.`);
213
+ return webRemote.executePythonScript(script, options);
215
214
  }
216
215
  }
217
216
  }
@@ -238,7 +237,7 @@ async function executeViaRemoteExec(remoteExec, toolName, params, options) {
238
237
  case 'get_all_scene_objects': {
239
238
  const pyCode = `
240
239
  import unreal, json
241
- actors = unreal.EditorLevelLibrary.get_all_level_actors()
240
+ actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).get_all_level_actors()
242
241
  result = [{"name": a.get_actor_label(), "class": a.get_class().get_name(), "location": [a.get_actor_location().x, a.get_actor_location().y, a.get_actor_location().z]} for a in actors]
243
242
  print(json.dumps(result))
244
243
  `;
@@ -257,7 +256,7 @@ import unreal, json
257
256
  loc = ${loc}
258
257
  rot = ${rot}
259
258
  scale = ${scale}
260
- actor = unreal.EditorLevelLibrary.spawn_actor_from_class(unreal.EditorAssetLibrary.load_asset('/Script/Engine.${params.actor_class}').get_class(), unreal.Vector(*loc), unreal.Rotator(*rot))
259
+ actor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).spawn_actor_from_class(unreal.EditorAssetLibrary.load_asset('/Script/Engine.${params.actor_class}').get_class(), unreal.Vector(*loc), unreal.Rotator(*rot))
261
260
  if actor:
262
261
  actor.set_actor_scale3d(unreal.Vector(*scale))
263
262
  ${params.actor_label ? `actor.set_actor_label("${params.actor_label}")` : ''}
@@ -285,26 +284,127 @@ else:
285
284
  }
286
285
 
287
286
  /**
288
- * Build a generic Python script to execute a tool by name.
289
- * Falls back to importing from the MCP server module if available.
287
+ * Build a standalone UE Python script to execute a tool by name.
288
+ * These scripts use only the `unreal` module available inside UE's Python interpreter.
290
289
  */
291
290
  function buildPythonForTool(toolName, params) {
292
- // Generic approach: serialize params and call the tool function in the MCP server
293
- // This works if the mcp_server_stdio.py module is importable
294
- const paramsJson = JSON.stringify(params).replace(/"/g, '\\"');
295
-
296
- return `
297
- import json, sys
298
- try:
299
- # Try to call the tool function directly
300
- from mcp_server_stdio import ${toolName}
301
- result = ${toolName}(**json.loads("${paramsJson}"))
302
- print(result if isinstance(result, str) else json.dumps(result))
303
- except ImportError:
304
- print(json.dumps({"error": "Tool '${toolName}' requires the CreatelexGenAI plugin or MCP server."}))
305
- except Exception as e:
306
- print(json.dumps({"error": str(e)}))
307
- `;
291
+ const paramsJson = JSON.stringify(params);
292
+
293
+ switch (toolName) {
294
+ case 'spawn_object': {
295
+ const actorClass = params.actor_class || 'StaticMeshActor';
296
+ const loc = params.location || '0,0,0';
297
+ const rot = params.rotation || '0,0,0';
298
+ const scale = params.scale || '1,1,1';
299
+ const label = params.actor_label || '';
300
+ const locParts = loc.split(',').map(s => s.trim());
301
+ const rotParts = rot.split(',').map(s => s.trim());
302
+ const scaleParts = scale.split(',').map(s => s.trim());
303
+ return `import unreal, json
304
+ actor = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).spawn_actor_from_class(unreal.${actorClass}, unreal.Vector(${locParts.join(',')}), unreal.Rotator(${rotParts.join(',')}))
305
+ if actor:
306
+ actor.set_actor_scale3d(unreal.Vector(${scaleParts.join(',')}))
307
+ ${label ? `actor.set_actor_label("${label}")` : ''}
308
+ mesh_path = '/Engine/BasicShapes/Cube'
309
+ if hasattr(actor, 'static_mesh_component'):
310
+ mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
311
+ if mesh:
312
+ actor.static_mesh_component.set_static_mesh(mesh)
313
+ print(json.dumps({"success": True, "actor": actor.get_actor_label(), "class": "${actorClass}"}))
314
+ else:
315
+ print(json.dumps({"success": False, "error": "Failed to spawn ${actorClass}"}))`;
316
+ }
317
+
318
+ case 'delete_actor': {
319
+ const name = params.actor_name || params.name || '';
320
+ return `import unreal, json
321
+ actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).get_all_level_actors()
322
+ deleted = []
323
+ for a in actors:
324
+ if a.get_actor_label() == "${name}":
325
+ a.destroy_actor()
326
+ deleted.append("${name}")
327
+ print(json.dumps({"success": len(deleted) > 0, "deleted": deleted}))`;
328
+ }
329
+
330
+ case 'find_actors_by_name': {
331
+ const pattern = params.pattern || params.name || '';
332
+ return `import unreal, json
333
+ actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).get_all_level_actors()
334
+ found = []
335
+ for a in actors:
336
+ label = a.get_actor_label()
337
+ if "${pattern}".lower() in label.lower():
338
+ loc = a.get_actor_location()
339
+ found.append({"name": label, "class": a.get_class().get_name(), "location": [loc.x, loc.y, loc.z]})
340
+ print(json.dumps({"success": True, "actors": found, "count": len(found)}))`;
341
+ }
342
+
343
+ case 'set_actor_transform': {
344
+ const name = params.actor_name || params.name || '';
345
+ const loc = params.location || '';
346
+ const rot = params.rotation || '';
347
+ const scale = params.scale || '';
348
+ return `import unreal, json
349
+ actors = unreal.get_editor_subsystem(unreal.EditorActorSubsystem).get_all_level_actors()
350
+ target = None
351
+ for a in actors:
352
+ if a.get_actor_label() == "${name}":
353
+ target = a
354
+ break
355
+ if target:
356
+ ${loc ? `target.set_actor_location(unreal.Vector(${loc}), False, False)` : '# no location change'}
357
+ ${rot ? `target.set_actor_rotation(unreal.Rotator(${rot}), False, False)` : '# no rotation change'}
358
+ ${scale ? `target.set_actor_scale3d(unreal.Vector(${scale}))` : '# no scale change'}
359
+ print(json.dumps({"success": True, "actor": "${name}"}))
360
+ else:
361
+ print(json.dumps({"success": False, "error": "Actor '${name}' not found"}))`;
362
+ }
363
+
364
+ case 'create_blueprint': {
365
+ const name = params.name || 'NewBlueprint';
366
+ const parentClass = params.parent_class || 'Actor';
367
+ const path = params.path || '/Game/Blueprints';
368
+ return `import unreal, json
369
+ factory = unreal.BlueprintFactory()
370
+ factory.set_editor_property("parent_class", unreal.${parentClass})
371
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
372
+ bp = asset_tools.create_asset("${name}", "${path}", unreal.Blueprint, factory)
373
+ if bp:
374
+ print(json.dumps({"success": True, "name": "${name}", "path": "${path}/${name}"}))
375
+ else:
376
+ print(json.dumps({"success": False, "error": "Failed to create blueprint"}))`;
377
+ }
378
+
379
+ case 'create_material': {
380
+ const name = params.name || 'NewMaterial';
381
+ const path = params.path || '/Game/Materials';
382
+ return `import unreal, json
383
+ factory = unreal.MaterialFactoryNew()
384
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
385
+ mat = asset_tools.create_asset("${name}", "${path}", unreal.Material, factory)
386
+ if mat:
387
+ print(json.dumps({"success": True, "name": "${name}", "path": "${path}/${name}"}))
388
+ else:
389
+ print(json.dumps({"success": False, "error": "Failed to create material"}))`;
390
+ }
391
+
392
+ case 'get_all_scene_objects':
393
+ return `import unreal, json
394
+ subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
395
+ actors = subsystem.get_all_level_actors()
396
+ result = []
397
+ for a in actors:
398
+ loc = a.get_actor_location()
399
+ result.append({"name": a.get_actor_label(), "class": a.get_class().get_name(), "location": [loc.x, loc.y, loc.z]})
400
+ print(json.dumps(result))`;
401
+
402
+ default:
403
+ // Generic: pass tool name and params as JSON, let a generic handler try
404
+ return `import unreal, json
405
+ params = json.loads('${paramsJson.replace(/'/g, "\\'")}')
406
+ print(json.dumps({"success": False, "error": "Tool '${toolName}' is not directly supported in no-plugin mode via CLI exec. Use 'createlex serve' with an AI tool for full 71-tool support."}))`;
407
+ }
308
408
  }
309
409
 
310
410
  module.exports = {
@@ -15,12 +15,18 @@ function request(method, path, body = null, options = {}) {
15
15
  const timeout = options.timeout || REQUEST_TIMEOUT;
16
16
 
17
17
  return new Promise((resolve, reject) => {
18
+ const bodyStr = body ? JSON.stringify(body) : null;
19
+ const headers = { 'Content-Type': 'application/json' };
20
+ if (bodyStr) {
21
+ headers['Content-Length'] = Buffer.byteLength(bodyStr);
22
+ }
23
+
18
24
  const reqOptions = {
19
25
  hostname: host,
20
26
  port,
21
27
  path,
22
28
  method,
23
- headers: { 'Content-Type': 'application/json' },
29
+ headers,
24
30
  timeout
25
31
  };
26
32
 
@@ -49,8 +55,8 @@ function request(method, path, body = null, options = {}) {
49
55
  }
50
56
  });
51
57
 
52
- if (body) {
53
- req.write(JSON.stringify(body));
58
+ if (bodyStr) {
59
+ req.write(bodyStr);
54
60
  }
55
61
 
56
62
  req.end();
@@ -184,7 +190,7 @@ async function executePythonScript(script, options = {}) {
184
190
  'ExecutePythonCommandEx',
185
191
  {
186
192
  PythonCommand: script,
187
- ExecutionMode: 'EvaluateStatement',
193
+ ExecutionMode: 'ExecuteFile',
188
194
  FileExecutionScope: 'Public'
189
195
  },
190
196
  options