@createlex/createlexgenai 1.0.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.
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ const axios = require('axios');
4
+ const configStore = require('./config-store');
5
+ const authManager = require('./auth-manager');
6
+
7
+ // In-memory subscription cache (5-minute TTL)
8
+ let subscriptionCache = null;
9
+ let subscriptionCacheTimestamp = null;
10
+ const CACHE_TTL_MS = 5 * 60 * 1000;
11
+
12
+ function isCacheValid() {
13
+ if (!subscriptionCache || !subscriptionCacheTimestamp) return false;
14
+ return (Date.now() - subscriptionCacheTimestamp) < CACHE_TTL_MS;
15
+ }
16
+
17
+ function clearCache() {
18
+ subscriptionCache = null;
19
+ subscriptionCacheTimestamp = null;
20
+ }
21
+
22
+ function getApiBaseUrl() {
23
+ return configStore.get('apiBaseUrl') || 'https://api.createlex.com/api';
24
+ }
25
+
26
+ async function makeApiRequest(endpoint, options = {}) {
27
+ const { method = 'GET', data, headers = {}, timeout = 15000 } = options;
28
+ const baseUrl = getApiBaseUrl();
29
+ const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
30
+ const relativeEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
31
+ const url = `${normalizedBase}${relativeEndpoint}`;
32
+
33
+ const response = await axios({
34
+ method,
35
+ url,
36
+ data,
37
+ timeout,
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ ...headers
41
+ },
42
+ validateStatus: status => status >= 200 && status < 500
43
+ });
44
+
45
+ if (response.status >= 200 && response.status < 300) {
46
+ return response;
47
+ }
48
+
49
+ const err = new Error(`HTTP ${response.status}: ${JSON.stringify(response.data)}`);
50
+ err.status = response.status;
51
+ throw err;
52
+ }
53
+
54
+ async function getSubscriptionStatus() {
55
+ const token = authManager.getToken();
56
+ if (!token) {
57
+ clearCache();
58
+ return { hasActiveSubscription: false, error: 'No authentication token' };
59
+ }
60
+
61
+ const validation = authManager.validateTokenFormat(token);
62
+ if (!validation.valid) {
63
+ clearCache();
64
+ return { hasActiveSubscription: false, error: validation.reason };
65
+ }
66
+
67
+ // Return cached if valid
68
+ if (isCacheValid()) {
69
+ return subscriptionCache;
70
+ }
71
+
72
+ try {
73
+ const response = await makeApiRequest('subscription/status', {
74
+ headers: { Authorization: `Bearer ${token}` }
75
+ });
76
+
77
+ const data = response.data || {};
78
+
79
+ const hasActiveSubscription = data.hasActiveSubscription === true ||
80
+ data.subscriptionStatus === 'active' ||
81
+ data.hasSubscription === true;
82
+
83
+ // Device seat check
84
+ if (hasActiveSubscription && data.userId) {
85
+ try {
86
+ const deviceInfo = authManager.getDeviceInfo();
87
+ const seatResponse = await makeApiRequest('device/check-seat', {
88
+ method: 'POST',
89
+ headers: { Authorization: `Bearer ${token}` },
90
+ data: {
91
+ deviceInfo,
92
+ subscriptionPlan: data.plan,
93
+ userId: data.userId
94
+ }
95
+ });
96
+ data.seatStatus = seatResponse.data;
97
+
98
+ if (seatResponse.data?.canUse === false) {
99
+ data.hasActiveSubscription = false;
100
+ data.seatError = seatResponse.data.error;
101
+ }
102
+ } catch (seatErr) {
103
+ data.seatStatus = { canUse: true, note: 'Seat check skipped: ' + seatErr.message };
104
+ }
105
+ }
106
+
107
+ data.deviceId = authManager.generateDeviceId();
108
+ data.deviceInfo = authManager.getDeviceInfo();
109
+
110
+ subscriptionCache = data;
111
+ subscriptionCacheTimestamp = Date.now();
112
+ return data;
113
+ } catch (error) {
114
+ // Bypass on 404/403/401 or explicit bypass
115
+ if (process.env.BYPASS_SUBSCRIPTION === 'true' ||
116
+ error.status === 404 || error.status === 403 || error.status === 401) {
117
+ const bypassData = {
118
+ hasActiveSubscription: true,
119
+ bypass: true,
120
+ error: error.message
121
+ };
122
+ subscriptionCache = bypassData;
123
+ subscriptionCacheTimestamp = Date.now();
124
+ return bypassData;
125
+ }
126
+
127
+ clearCache();
128
+ return { hasActiveSubscription: false, error: error.message };
129
+ }
130
+ }
131
+
132
+ function hasValidSubscription(status) {
133
+ if (!status) return false;
134
+
135
+ const hasSub = status.hasActiveSubscription === true ||
136
+ (status.subscriptionStatus && status.subscriptionStatus.toLowerCase() === 'active') ||
137
+ status.hasSubscription === true;
138
+
139
+ if (!hasSub) return false;
140
+
141
+ if (status.seatStatus && status.seatStatus.canUse === false) {
142
+ return false;
143
+ }
144
+
145
+ return true;
146
+ }
147
+
148
+ async function sendDeviceHeartbeat() {
149
+ const token = authManager.getToken();
150
+ const deviceId = authManager.generateDeviceId();
151
+ if (!token || !deviceId) return { success: false };
152
+
153
+ try {
154
+ const response = await makeApiRequest('device/heartbeat', {
155
+ method: 'POST',
156
+ headers: { Authorization: `Bearer ${token}` },
157
+ data: {
158
+ deviceId,
159
+ deviceInfo: authManager.getDeviceInfo()
160
+ },
161
+ timeout: 10000
162
+ });
163
+ return { success: true, data: response.data };
164
+ } catch (error) {
165
+ return { success: false, error: error.message };
166
+ }
167
+ }
168
+
169
+ module.exports = {
170
+ getSubscriptionStatus,
171
+ hasValidSubscription,
172
+ sendDeviceHeartbeat,
173
+ clearCache,
174
+ getApiBaseUrl,
175
+ makeApiRequest
176
+ };
@@ -0,0 +1,318 @@
1
+ 'use strict';
2
+
3
+ const net = require('net');
4
+
5
+ const DEFAULT_PORT = 9878;
6
+ const CONNECT_TIMEOUT = 5000;
7
+ const RECEIVE_TIMEOUT = 30000;
8
+
9
+ // Backend identifiers
10
+ const BACKEND = {
11
+ PLUGIN: 'plugin', // CreatelexGenAI plugin TCP socket (port 9878)
12
+ REMOTE_EXEC: 'remote-exec', // UE Python Remote Execution (UDP 6766 + TCP)
13
+ WEB_REMOTE: 'web-remote' // UE Web Remote Control HTTP API (port 30010)
14
+ };
15
+
16
+ /**
17
+ * Send a JSON command to Unreal Engine's socket server and return the response.
18
+ * This is the CreatelexGenAI plugin backend (port 9878).
19
+ */
20
+ function sendCommand(command, options = {}) {
21
+ const port = options.port || DEFAULT_PORT;
22
+ const host = options.host || '127.0.0.1';
23
+ const timeout = options.timeout || RECEIVE_TIMEOUT;
24
+
25
+ return new Promise((resolve, reject) => {
26
+ const socket = new net.Socket();
27
+ let responseBuffer = '';
28
+
29
+ socket.setTimeout(CONNECT_TIMEOUT);
30
+
31
+ socket.connect(port, host, () => {
32
+ socket.setTimeout(timeout);
33
+ const payload = typeof command === 'string' ? command : JSON.stringify(command);
34
+ socket.write(payload);
35
+ });
36
+
37
+ socket.on('data', (data) => {
38
+ responseBuffer += data.toString();
39
+ try {
40
+ const parsed = JSON.parse(responseBuffer);
41
+ socket.destroy();
42
+ resolve(parsed);
43
+ } catch {
44
+ // Keep buffering
45
+ }
46
+ });
47
+
48
+ socket.on('timeout', () => {
49
+ socket.destroy();
50
+ reject(new Error(`Connection timed out (port ${port})`));
51
+ });
52
+
53
+ socket.on('error', (err) => {
54
+ socket.destroy();
55
+ if (err.code === 'ECONNREFUSED') {
56
+ reject(new Error(`Cannot connect to Unreal Engine on port ${port}. Is the editor running with the CreatelexGenAI plugin?`));
57
+ } else {
58
+ reject(new Error(`Socket error: ${err.message}`));
59
+ }
60
+ });
61
+
62
+ socket.on('close', () => {
63
+ if (responseBuffer.trim()) {
64
+ try {
65
+ resolve(JSON.parse(responseBuffer));
66
+ } catch {
67
+ resolve({ raw: responseBuffer.trim() });
68
+ }
69
+ }
70
+ });
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Test if Unreal Engine is reachable on the given port (plugin backend).
76
+ */
77
+ async function testConnection(port = DEFAULT_PORT) {
78
+ return new Promise((resolve) => {
79
+ const socket = new net.Socket();
80
+ socket.setTimeout(CONNECT_TIMEOUT);
81
+
82
+ socket.connect(port, '127.0.0.1', () => {
83
+ socket.destroy();
84
+ resolve({ connected: true, port, backend: BACKEND.PLUGIN });
85
+ });
86
+
87
+ socket.on('timeout', () => {
88
+ socket.destroy();
89
+ resolve({ connected: false, port, error: 'Connection timed out' });
90
+ });
91
+
92
+ socket.on('error', (err) => {
93
+ socket.destroy();
94
+ resolve({ connected: false, port, error: err.message });
95
+ });
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Detect all available backends and return their status.
101
+ */
102
+ async function detectBackends(options = {}) {
103
+ const remoteExec = require('./remote-execution');
104
+ const webRemote = require('./web-remote-control');
105
+
106
+ const results = {};
107
+
108
+ // Check all three in parallel
109
+ const [pluginResult, remoteExecResult, webRemoteResult] = await Promise.all([
110
+ testConnection(options.pluginPort || DEFAULT_PORT),
111
+ remoteExec.testConnection(),
112
+ webRemote.testConnection({ port: options.webRemotePort })
113
+ ]);
114
+
115
+ results[BACKEND.PLUGIN] = {
116
+ name: 'CreatelexGenAI Plugin (TCP)',
117
+ ...pluginResult
118
+ };
119
+
120
+ results[BACKEND.REMOTE_EXEC] = {
121
+ name: 'Python Remote Execution (UDP/TCP)',
122
+ connected: remoteExecResult.available,
123
+ ...remoteExecResult
124
+ };
125
+
126
+ results[BACKEND.WEB_REMOTE] = {
127
+ name: 'Web Remote Control (HTTP)',
128
+ connected: webRemoteResult.available,
129
+ ...webRemoteResult
130
+ };
131
+
132
+ return results;
133
+ }
134
+
135
+ /**
136
+ * Auto-select the best available backend.
137
+ * Priority: Plugin > Web Remote Control > Python Remote Execution
138
+ */
139
+ async function autoSelectBackend(options = {}) {
140
+ const backends = await detectBackends(options);
141
+
142
+ if (backends[BACKEND.PLUGIN].connected) {
143
+ return { backend: BACKEND.PLUGIN, info: backends[BACKEND.PLUGIN] };
144
+ }
145
+ if (backends[BACKEND.WEB_REMOTE].connected) {
146
+ return { backend: BACKEND.WEB_REMOTE, info: backends[BACKEND.WEB_REMOTE] };
147
+ }
148
+ if (backends[BACKEND.REMOTE_EXEC].connected) {
149
+ return { backend: BACKEND.REMOTE_EXEC, info: backends[BACKEND.REMOTE_EXEC] };
150
+ }
151
+
152
+ return { backend: null, info: null, backends };
153
+ }
154
+
155
+ /**
156
+ * Execute a tool/command using the specified or auto-detected backend.
157
+ */
158
+ async function executeWithBackend(toolName, params = {}, options = {}) {
159
+ const backend = options.backend || null;
160
+
161
+ if (backend === BACKEND.PLUGIN || (!backend && options.port)) {
162
+ // Plugin backend — send JSON-RPC to port 9878
163
+ const command = {
164
+ jsonrpc: '2.0',
165
+ id: 1,
166
+ method: 'tools/call',
167
+ params: { name: toolName, arguments: params }
168
+ };
169
+ return sendCommand(command, options);
170
+ }
171
+
172
+ if (backend === BACKEND.WEB_REMOTE) {
173
+ // Web Remote Control — translate tool calls to HTTP API calls
174
+ const webRemote = require('./web-remote-control');
175
+ return executeViaWebRemote(webRemote, toolName, params, options);
176
+ }
177
+
178
+ if (backend === BACKEND.REMOTE_EXEC) {
179
+ // Python Remote Execution — generate Python code and execute
180
+ const remoteExec = require('./remote-execution');
181
+ return executeViaRemoteExec(remoteExec, toolName, params, options);
182
+ }
183
+
184
+ // Auto-detect
185
+ const selected = await autoSelectBackend(options);
186
+ if (!selected.backend) {
187
+ throw new Error('No Unreal Engine connection available. See `createlex connect` for details.');
188
+ }
189
+
190
+ return executeWithBackend(toolName, params, { ...options, backend: selected.backend });
191
+ }
192
+
193
+ /**
194
+ * Execute a tool via Web Remote Control by translating to appropriate API calls.
195
+ */
196
+ async function executeViaWebRemote(webRemote, toolName, params, options) {
197
+ // Map common tool names to Web Remote Control API calls
198
+ switch (toolName) {
199
+ case 'get_all_scene_objects':
200
+ return webRemote.getAllLevelActors(options);
201
+
202
+ case 'execute_python_script':
203
+ return webRemote.executePythonScript(params.script, options);
204
+
205
+ case 'execute_unreal_command':
206
+ return webRemote.executeConsoleCommand(params.command, options);
207
+
208
+ default: {
209
+ // For tools that don't have a direct mapping, try to execute via Python
210
+ 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.`);
215
+ }
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Execute a tool via Python Remote Execution by generating Python code.
221
+ */
222
+ async function executeViaRemoteExec(remoteExec, toolName, params, options) {
223
+ switch (toolName) {
224
+ case 'execute_python_script':
225
+ return remoteExec.executeCommand(params.script, {
226
+ execMode: 'ExecuteStatement',
227
+ ...options
228
+ });
229
+
230
+ case 'execute_unreal_command': {
231
+ const pyCode = `import unreal; unreal.SystemLibrary.execute_console_command(None, "${params.command.replace(/"/g, '\\"')}")`;
232
+ return remoteExec.executeCommand(pyCode, {
233
+ execMode: 'ExecuteStatement',
234
+ ...options
235
+ });
236
+ }
237
+
238
+ case 'get_all_scene_objects': {
239
+ const pyCode = `
240
+ import unreal, json
241
+ actors = unreal.EditorLevelLibrary.get_all_level_actors()
242
+ 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
+ print(json.dumps(result))
244
+ `;
245
+ return remoteExec.executeCommand(pyCode, {
246
+ execMode: 'ExecuteStatement',
247
+ ...options
248
+ });
249
+ }
250
+
251
+ case 'spawn_object': {
252
+ const loc = params.location || '[0,0,0]';
253
+ const rot = params.rotation || '[0,0,0]';
254
+ const scale = params.scale || '[1,1,1]';
255
+ const pyCode = `
256
+ import unreal, json
257
+ loc = ${loc}
258
+ rot = ${rot}
259
+ 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))
261
+ if actor:
262
+ actor.set_actor_scale3d(unreal.Vector(*scale))
263
+ ${params.actor_label ? `actor.set_actor_label("${params.actor_label}")` : ''}
264
+ print(json.dumps({"success": True, "actor": actor.get_actor_label()}))
265
+ else:
266
+ print(json.dumps({"success": False, "error": "Failed to spawn actor"}))
267
+ `;
268
+ return remoteExec.executeCommand(pyCode, {
269
+ execMode: 'ExecuteStatement',
270
+ ...options
271
+ });
272
+ }
273
+
274
+ default: {
275
+ const script = buildPythonForTool(toolName, params);
276
+ if (script) {
277
+ return remoteExec.executeCommand(script, {
278
+ execMode: 'ExecuteStatement',
279
+ ...options
280
+ });
281
+ }
282
+ throw new Error(`Tool '${toolName}' is not supported via Remote Execution. Use the CreatelexGenAI plugin for full tool access.`);
283
+ }
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Build a generic Python script to execute a tool by name.
289
+ * Falls back to importing from the MCP server module if available.
290
+ */
291
+ 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
+ `;
308
+ }
309
+
310
+ module.exports = {
311
+ BACKEND,
312
+ sendCommand,
313
+ testConnection,
314
+ detectBackends,
315
+ autoSelectBackend,
316
+ executeWithBackend,
317
+ DEFAULT_PORT
318
+ };