@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,243 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+
5
+ const DEFAULT_HTTP_PORT = 30010;
6
+ const DEFAULT_HOST = '127.0.0.1';
7
+ const REQUEST_TIMEOUT = 15000;
8
+
9
+ /**
10
+ * Make an HTTP request to the UE Web Remote Control API.
11
+ */
12
+ function request(method, path, body = null, options = {}) {
13
+ const port = options.port || DEFAULT_HTTP_PORT;
14
+ const host = options.host || DEFAULT_HOST;
15
+ const timeout = options.timeout || REQUEST_TIMEOUT;
16
+
17
+ return new Promise((resolve, reject) => {
18
+ const reqOptions = {
19
+ hostname: host,
20
+ port,
21
+ path,
22
+ method,
23
+ headers: { 'Content-Type': 'application/json' },
24
+ timeout
25
+ };
26
+
27
+ const req = http.request(reqOptions, (res) => {
28
+ let data = '';
29
+ res.on('data', (chunk) => { data += chunk; });
30
+ res.on('end', () => {
31
+ try {
32
+ resolve({ status: res.statusCode, data: JSON.parse(data) });
33
+ } catch {
34
+ resolve({ status: res.statusCode, data: data || null });
35
+ }
36
+ });
37
+ });
38
+
39
+ req.on('timeout', () => {
40
+ req.destroy();
41
+ reject(new Error(`Request timed out (port ${port})`));
42
+ });
43
+
44
+ req.on('error', (err) => {
45
+ if (err.code === 'ECONNREFUSED') {
46
+ reject(new Error(`Web Remote Control not available on port ${port}. Enable the "Web Remote Control" plugin in UE and run WebControl.StartServer.`));
47
+ } else {
48
+ reject(new Error(`HTTP error: ${err.message}`));
49
+ }
50
+ });
51
+
52
+ if (body) {
53
+ req.write(JSON.stringify(body));
54
+ }
55
+
56
+ req.end();
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Health check — GET /remote/api/v1/health
62
+ */
63
+ async function healthCheck(options = {}) {
64
+ try {
65
+ // Simple TCP connect test first (faster than full HTTP)
66
+ const net = require('net');
67
+ const port = options.port || DEFAULT_HTTP_PORT;
68
+ const host = options.host || DEFAULT_HOST;
69
+
70
+ const connected = await new Promise((resolve) => {
71
+ const socket = new net.Socket();
72
+ socket.setTimeout(3000);
73
+ socket.connect(port, host, () => {
74
+ socket.destroy();
75
+ resolve(true);
76
+ });
77
+ socket.on('timeout', () => { socket.destroy(); resolve(false); });
78
+ socket.on('error', () => { socket.destroy(); resolve(false); });
79
+ });
80
+
81
+ if (!connected) {
82
+ return { available: false, error: `Port ${port} not reachable` };
83
+ }
84
+
85
+ // Full health check
86
+ const res = await request('GET', '/remote/api/v1/health', null, options);
87
+ return {
88
+ available: res.status === 200,
89
+ status: res.status,
90
+ data: res.data,
91
+ protocol: 'Web Remote Control (HTTP)',
92
+ port
93
+ };
94
+ } catch (err) {
95
+ return { available: false, error: err.message };
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Call a UObject function.
101
+ *
102
+ * PUT /remote/object/call
103
+ * { objectPath, functionName, parameters, generateTransaction }
104
+ */
105
+ async function callFunction(objectPath, functionName, parameters = {}, options = {}) {
106
+ const body = {
107
+ objectPath,
108
+ functionName,
109
+ parameters,
110
+ generateTransaction: options.generateTransaction || false
111
+ };
112
+
113
+ const res = await request('PUT', '/remote/object/call', body, options);
114
+ return res.data;
115
+ }
116
+
117
+ /**
118
+ * Get a UObject property.
119
+ *
120
+ * PUT /remote/object/property (with READ_ACCESS)
121
+ */
122
+ async function getProperty(objectPath, propertyName, options = {}) {
123
+ const body = {
124
+ objectPath,
125
+ propertyName,
126
+ access: 'READ_ACCESS'
127
+ };
128
+
129
+ const res = await request('PUT', '/remote/object/property', body, options);
130
+ return res.data;
131
+ }
132
+
133
+ /**
134
+ * Set a UObject property.
135
+ *
136
+ * PUT /remote/object/property
137
+ */
138
+ async function setProperty(objectPath, propertyName, propertyValue, options = {}) {
139
+ const body = {
140
+ objectPath,
141
+ propertyName,
142
+ propertyValue,
143
+ access: 'WRITE_ACCESS'
144
+ };
145
+
146
+ const res = await request('PUT', '/remote/object/property', body, options);
147
+ return res.data;
148
+ }
149
+
150
+ /**
151
+ * Execute a console command via GameplayStatics.
152
+ */
153
+ async function executeConsoleCommand(command, options = {}) {
154
+ return callFunction(
155
+ '/Script/Engine.Default__KismetSystemLibrary',
156
+ 'ExecuteConsoleCommand',
157
+ {
158
+ WorldContextObject: '/Engine/Transient.GameEngine',
159
+ Command: command
160
+ },
161
+ options
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Get all actors in the current level.
167
+ */
168
+ async function getAllLevelActors(options = {}) {
169
+ return callFunction(
170
+ '/Script/EditorScriptingUtilities.Default__EditorLevelLibrary',
171
+ 'GetAllLevelActors',
172
+ {},
173
+ options
174
+ );
175
+ }
176
+
177
+ /**
178
+ * Execute a Python script via the PythonScriptLibrary.
179
+ * This uses the Web Remote Control API to call into UE's Python subsystem.
180
+ */
181
+ async function executePythonScript(script, options = {}) {
182
+ return callFunction(
183
+ '/Script/PythonScriptPlugin.Default__PythonScriptLibrary',
184
+ 'ExecutePythonCommandEx',
185
+ {
186
+ PythonCommand: script,
187
+ ExecutionMode: 'EvaluateStatement',
188
+ FileExecutionScope: 'Public'
189
+ },
190
+ options
191
+ );
192
+ }
193
+
194
+ /**
195
+ * List available Remote Control presets.
196
+ */
197
+ async function listPresets(options = {}) {
198
+ const res = await request('GET', '/remote/presets', null, options);
199
+ return res.data;
200
+ }
201
+
202
+ /**
203
+ * Test connection — combines health check with a simple function call.
204
+ */
205
+ async function testConnection(options = {}) {
206
+ const health = await healthCheck(options);
207
+ if (!health.available) {
208
+ return health;
209
+ }
210
+
211
+ // Try to get actors as a real test
212
+ try {
213
+ await getAllLevelActors(options);
214
+ return {
215
+ available: true,
216
+ protocol: 'Web Remote Control (HTTP)',
217
+ port: options.port || DEFAULT_HTTP_PORT,
218
+ verified: true
219
+ };
220
+ } catch (err) {
221
+ return {
222
+ available: true,
223
+ protocol: 'Web Remote Control (HTTP)',
224
+ port: options.port || DEFAULT_HTTP_PORT,
225
+ verified: false,
226
+ warning: `Server responds but function calls may not work: ${err.message}`
227
+ };
228
+ }
229
+ }
230
+
231
+ module.exports = {
232
+ request,
233
+ healthCheck,
234
+ callFunction,
235
+ getProperty,
236
+ setProperty,
237
+ executeConsoleCommand,
238
+ getAllLevelActors,
239
+ executePythonScript,
240
+ listPresets,
241
+ testConnection,
242
+ DEFAULT_HTTP_PORT
243
+ };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+
5
+ // In MCP stdio mode, all logging must go to stderr (stdout is reserved for JSON-RPC)
6
+ let useStderr = false;
7
+
8
+ function setStdioMode(enabled) {
9
+ useStderr = enabled;
10
+ }
11
+
12
+ function write(msg) {
13
+ if (useStderr) {
14
+ process.stderr.write(msg + '\n');
15
+ } else {
16
+ console.log(msg);
17
+ }
18
+ }
19
+
20
+ function info(msg) {
21
+ write(chalk.blue('i') + ' ' + msg);
22
+ }
23
+
24
+ function success(msg) {
25
+ write(chalk.green('✓') + ' ' + msg);
26
+ }
27
+
28
+ function warn(msg) {
29
+ write(chalk.yellow('!') + ' ' + msg);
30
+ }
31
+
32
+ function error(msg) {
33
+ write(chalk.red('✗') + ' ' + msg);
34
+ }
35
+
36
+ function debug(msg) {
37
+ if (process.env.CREATELEX_DEBUG === 'true') {
38
+ write(chalk.gray('[debug] ' + msg));
39
+ }
40
+ }
41
+
42
+ function header(msg) {
43
+ write('');
44
+ write(chalk.bold.cyan(msg));
45
+ write(chalk.cyan('─'.repeat(msg.length)));
46
+ }
47
+
48
+ function keyValue(key, value) {
49
+ write(` ${chalk.gray(key + ':')} ${value}`);
50
+ }
51
+
52
+ function blank() {
53
+ write('');
54
+ }
55
+
56
+ module.exports = {
57
+ setStdioMode,
58
+ info,
59
+ success,
60
+ warn,
61
+ error,
62
+ debug,
63
+ header,
64
+ keyValue,
65
+ blank
66
+ };
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn, execSync } = require('child_process');
6
+ const os = require('os');
7
+ const log = require('./logger');
8
+
9
+ class PythonManager {
10
+ constructor(rootPath) {
11
+ this.rootPath = rootPath;
12
+ this.pythonDir = path.join(rootPath, 'python');
13
+ this.venvPath = path.join(this.pythonDir, 'venv');
14
+ this.requirementsPath = path.join(this.pythonDir, 'requirements.txt');
15
+ this.platform = os.platform();
16
+ }
17
+
18
+ getPythonCommand() {
19
+ const venvPython = this.platform === 'win32'
20
+ ? path.join(this.venvPath, 'Scripts', 'python.exe')
21
+ : path.join(this.venvPath, 'bin', 'python');
22
+
23
+ if (fs.existsSync(venvPython)) {
24
+ try {
25
+ execSync(`"${venvPython}" -c "import websockets, fastmcp"`, { stdio: 'pipe' });
26
+ log.debug(`Using venv Python: ${venvPython}`);
27
+ return venvPython;
28
+ } catch {
29
+ log.debug('Venv exists but packages missing');
30
+ }
31
+ }
32
+
33
+ return this.findSystemPython();
34
+ }
35
+
36
+ findSystemPython() {
37
+ const candidates = this.platform === 'win32'
38
+ ? ['python', 'python3', 'py -3']
39
+ : ['python3.12', 'python3.11', 'python3.10', 'python3', 'python'];
40
+
41
+ for (const cmd of candidates) {
42
+ try {
43
+ const output = execSync(`${cmd} --version`, { stdio: 'pipe', encoding: 'utf8' });
44
+ const version = output.match(/Python (\d+\.\d+)/)?.[1];
45
+ if (version && parseFloat(version) >= 3.10) {
46
+ log.debug(`Found system Python: ${cmd} (${version})`);
47
+ return cmd;
48
+ }
49
+ } catch {
50
+ // Continue
51
+ }
52
+ }
53
+
54
+ throw new Error('No compatible Python found (3.10+ required). Install Python from https://python.org');
55
+ }
56
+
57
+ async checkDependencies() {
58
+ try {
59
+ const pythonCmd = this.getPythonCommand();
60
+ execSync(`"${pythonCmd}" -c "import websockets, fastmcp, requests"`, { stdio: 'pipe' });
61
+ return true;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ async installDependencies(onProgress = null) {
68
+ if (!fs.existsSync(this.pythonDir)) {
69
+ fs.mkdirSync(this.pythonDir, { recursive: true });
70
+ }
71
+
72
+ const systemPython = this.findSystemPython();
73
+
74
+ if (onProgress) onProgress('Creating Python virtual environment...');
75
+ log.info('Creating Python virtual environment...');
76
+ await this.runCommand(systemPython, ['-m', 'venv', this.venvPath]);
77
+
78
+ const venvPython = this.platform === 'win32'
79
+ ? path.join(this.venvPath, 'Scripts', 'python.exe')
80
+ : path.join(this.venvPath, 'bin', 'python');
81
+
82
+ if (onProgress) onProgress('Upgrading pip...');
83
+ log.info('Upgrading pip...');
84
+ await this.runCommand(venvPython, ['-m', 'pip', 'install', '--upgrade', 'pip']);
85
+
86
+ if (onProgress) onProgress('Installing Python packages...');
87
+ log.info('Installing Python packages...');
88
+ await this.runCommand(venvPython, ['-m', 'pip', 'install', '-r', this.requirementsPath]);
89
+
90
+ log.success('Python dependencies installed');
91
+ return true;
92
+ }
93
+
94
+ runCommand(command, args, options = {}) {
95
+ return new Promise((resolve, reject) => {
96
+ const child = spawn(command, args, { stdio: 'pipe', ...options });
97
+ let stdout = '';
98
+ let stderr = '';
99
+
100
+ child.stdout?.on('data', (data) => { stdout += data.toString(); });
101
+ child.stderr?.on('data', (data) => { stderr += data.toString(); });
102
+
103
+ child.on('close', (code) => {
104
+ if (code === 0) {
105
+ resolve({ stdout, stderr });
106
+ } else {
107
+ reject(new Error(`Command failed (code ${code}): ${stderr || stdout}`));
108
+ }
109
+ });
110
+
111
+ child.on('error', reject);
112
+ });
113
+ }
114
+
115
+ async ensureDependencies() {
116
+ const hasCorrectDeps = await this.checkDependencies();
117
+ if (hasCorrectDeps) {
118
+ log.debug('Python dependencies already installed');
119
+ return this.getPythonCommand();
120
+ }
121
+
122
+ await this.installDependencies();
123
+ return this.getPythonCommand();
124
+ }
125
+
126
+ findMCPServerScript() {
127
+ const candidates = [
128
+ path.join(this.pythonDir, 'mcp_server_stdio.py'),
129
+ path.join(this.pythonDir, 'mcp_server_protected.py')
130
+ ];
131
+
132
+ for (const scriptPath of candidates) {
133
+ if (fs.existsSync(scriptPath)) {
134
+ return scriptPath;
135
+ }
136
+ }
137
+
138
+ return null;
139
+ }
140
+ }
141
+
142
+ module.exports = { PythonManager };