@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.
- package/README.md +272 -0
- package/bin/createlex.js +5 -0
- package/package.json +45 -0
- package/python/activity_tracker.py +280 -0
- package/python/fastmcp.py +768 -0
- package/python/mcp_server_stdio.py +4720 -0
- package/python/requirements.txt +7 -0
- package/python/subscription_validator.py +199 -0
- package/python/ue_native_handler.py +573 -0
- package/python/ui_slice_host.py +637 -0
- package/src/cli.js +109 -0
- package/src/commands/config.js +56 -0
- package/src/commands/connect.js +100 -0
- package/src/commands/exec.js +148 -0
- package/src/commands/login.js +111 -0
- package/src/commands/logout.js +17 -0
- package/src/commands/serve.js +237 -0
- package/src/commands/setup.js +65 -0
- package/src/commands/status.js +126 -0
- package/src/commands/tools.js +133 -0
- package/src/core/auth-manager.js +147 -0
- package/src/core/config-store.js +81 -0
- package/src/core/discovery.js +71 -0
- package/src/core/ide-configurator.js +189 -0
- package/src/core/remote-execution.js +228 -0
- package/src/core/subscription.js +176 -0
- package/src/core/unreal-connection.js +318 -0
- package/src/core/web-remote-control.js +243 -0
- package/src/utils/logger.js +66 -0
- package/src/utils/python-manager.js +142 -0
|
@@ -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 };
|