@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,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { CONFIG_DIR } = require('./config-store');
|
|
8
|
+
|
|
9
|
+
const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
|
|
10
|
+
|
|
11
|
+
function ensureDir() {
|
|
12
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
13
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadAuth() {
|
|
18
|
+
ensureDir();
|
|
19
|
+
if (!fs.existsSync(AUTH_FILE)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(fs.readFileSync(AUTH_FILE, 'utf8'));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function saveAuth(data) {
|
|
30
|
+
ensureDir();
|
|
31
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function clearAuth() {
|
|
35
|
+
if (fs.existsSync(AUTH_FILE)) {
|
|
36
|
+
fs.unlinkSync(AUTH_FILE);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getToken() {
|
|
41
|
+
const auth = loadAuth();
|
|
42
|
+
return auth?.token || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setToken(token, extra = {}) {
|
|
46
|
+
saveAuth({ token, ...extra, savedAt: new Date().toISOString() });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function validateTokenFormat(token) {
|
|
50
|
+
if (!token) {
|
|
51
|
+
return { valid: false, reason: 'missing_token' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parts = token.split('.');
|
|
55
|
+
if (parts.length !== 3) {
|
|
56
|
+
return { valid: false, reason: 'invalid_token_format' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (parts.some(part => !part)) {
|
|
60
|
+
return { valid: false, reason: 'invalid_token_structure' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
65
|
+
if (payload.exp) {
|
|
66
|
+
const expiryTime = payload.exp * 1000;
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const clockSkewBuffer = 30 * 1000;
|
|
69
|
+
|
|
70
|
+
if (now >= expiryTime + clockSkewBuffer) {
|
|
71
|
+
return { valid: false, reason: 'token_expired' };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { valid: true, payload };
|
|
75
|
+
} catch {
|
|
76
|
+
return { valid: false, reason: 'invalid_token_structure' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getTokenPayload() {
|
|
81
|
+
const token = getToken();
|
|
82
|
+
if (!token) return null;
|
|
83
|
+
const result = validateTokenFormat(token);
|
|
84
|
+
return result.valid ? result.payload : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function generateDeviceId() {
|
|
88
|
+
try {
|
|
89
|
+
const cpus = os.cpus();
|
|
90
|
+
const networkInterfaces = os.networkInterfaces();
|
|
91
|
+
const hostname = os.hostname();
|
|
92
|
+
const platform = os.platform();
|
|
93
|
+
const arch = os.arch();
|
|
94
|
+
|
|
95
|
+
const hardwareString = JSON.stringify({
|
|
96
|
+
hostname,
|
|
97
|
+
platform,
|
|
98
|
+
arch,
|
|
99
|
+
cpuModel: cpus[0]?.model || 'unknown',
|
|
100
|
+
cpuCount: cpus.length,
|
|
101
|
+
macAddresses: Object.values(networkInterfaces)
|
|
102
|
+
.flat()
|
|
103
|
+
.filter(iface => !iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00')
|
|
104
|
+
.map(iface => iface.mac)
|
|
105
|
+
.sort()
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return crypto.createHash('sha256').update(hardwareString).digest('hex');
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getDeviceInfo() {
|
|
115
|
+
const platform = os.platform();
|
|
116
|
+
const arch = os.arch();
|
|
117
|
+
const hostname = os.hostname();
|
|
118
|
+
|
|
119
|
+
let platformName;
|
|
120
|
+
if (platform === 'win32') {
|
|
121
|
+
platformName = `Windows-${arch}`;
|
|
122
|
+
} else if (platform === 'darwin') {
|
|
123
|
+
platformName = `macOS-${arch}`;
|
|
124
|
+
} else {
|
|
125
|
+
platformName = `Linux-${arch}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
deviceId: generateDeviceId(),
|
|
130
|
+
deviceName: hostname,
|
|
131
|
+
platform: platformName,
|
|
132
|
+
osVersion: os.release()
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
AUTH_FILE,
|
|
138
|
+
loadAuth,
|
|
139
|
+
saveAuth,
|
|
140
|
+
clearAuth,
|
|
141
|
+
getToken,
|
|
142
|
+
setToken,
|
|
143
|
+
validateTokenFormat,
|
|
144
|
+
getTokenPayload,
|
|
145
|
+
generateDeviceId,
|
|
146
|
+
getDeviceInfo
|
|
147
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), '.createlex');
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
9
|
+
|
|
10
|
+
const DEFAULTS = {
|
|
11
|
+
unrealPort: 9878,
|
|
12
|
+
mcpPort: 38080,
|
|
13
|
+
apiBaseUrl: 'https://api.createlex.com/api',
|
|
14
|
+
webBaseUrl: 'https://createlex.com',
|
|
15
|
+
debug: false
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function ensureConfigDir() {
|
|
19
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
20
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function load() {
|
|
25
|
+
ensureConfigDir();
|
|
26
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
27
|
+
return { ...DEFAULTS };
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
31
|
+
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
32
|
+
} catch {
|
|
33
|
+
return { ...DEFAULTS };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function save(config) {
|
|
38
|
+
ensureConfigDir();
|
|
39
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function get(key) {
|
|
43
|
+
const config = load();
|
|
44
|
+
return config[key];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function set(key, value) {
|
|
48
|
+
const config = load();
|
|
49
|
+
// Auto-parse numbers
|
|
50
|
+
if (/^\d+$/.test(value)) {
|
|
51
|
+
value = parseInt(value, 10);
|
|
52
|
+
} else if (value === 'true') {
|
|
53
|
+
value = true;
|
|
54
|
+
} else if (value === 'false') {
|
|
55
|
+
value = false;
|
|
56
|
+
}
|
|
57
|
+
config[key] = value;
|
|
58
|
+
save(config);
|
|
59
|
+
return config;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function reset() {
|
|
63
|
+
save({ ...DEFAULTS });
|
|
64
|
+
return DEFAULTS;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function list() {
|
|
68
|
+
return load();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
CONFIG_DIR,
|
|
73
|
+
CONFIG_FILE,
|
|
74
|
+
DEFAULTS,
|
|
75
|
+
load,
|
|
76
|
+
save,
|
|
77
|
+
get,
|
|
78
|
+
set,
|
|
79
|
+
reset,
|
|
80
|
+
list
|
|
81
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const net = require('net');
|
|
4
|
+
|
|
5
|
+
const UE_PORTS = [9878, 9879, 9880];
|
|
6
|
+
const WEB_REMOTE_PORTS = [30010, 30011];
|
|
7
|
+
const SCAN_TIMEOUT = 2000;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scan a single port for TCP connectivity.
|
|
11
|
+
*/
|
|
12
|
+
function scanPort(port) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const socket = new net.Socket();
|
|
15
|
+
socket.setTimeout(SCAN_TIMEOUT);
|
|
16
|
+
|
|
17
|
+
socket.connect(port, '127.0.0.1', () => {
|
|
18
|
+
socket.destroy();
|
|
19
|
+
resolve({ port, available: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
socket.on('timeout', () => {
|
|
23
|
+
socket.destroy();
|
|
24
|
+
resolve({ port, available: false });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
socket.on('error', () => {
|
|
28
|
+
socket.destroy();
|
|
29
|
+
resolve({ port, available: false });
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Scan known ports for running Unreal Engine instances (plugin socket).
|
|
36
|
+
*/
|
|
37
|
+
async function discoverInstances() {
|
|
38
|
+
const checks = UE_PORTS.map(scanPort);
|
|
39
|
+
const results = await Promise.all(checks);
|
|
40
|
+
return results.filter(r => r.available).map(r => ({ ...r, type: 'plugin' }));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scan Web Remote Control ports.
|
|
45
|
+
*/
|
|
46
|
+
async function discoverWebRemote() {
|
|
47
|
+
const checks = WEB_REMOTE_PORTS.map(scanPort);
|
|
48
|
+
const results = await Promise.all(checks);
|
|
49
|
+
return results.filter(r => r.available).map(r => ({ ...r, type: 'web-remote' }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Discover all available connection points.
|
|
54
|
+
*/
|
|
55
|
+
async function discoverAll() {
|
|
56
|
+
const remoteExec = require('./remote-execution');
|
|
57
|
+
|
|
58
|
+
const [pluginInstances, webRemoteInstances, remoteExecResult] = await Promise.all([
|
|
59
|
+
discoverInstances(),
|
|
60
|
+
discoverWebRemote(),
|
|
61
|
+
remoteExec.discover(SCAN_TIMEOUT)
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
plugin: pluginInstances,
|
|
66
|
+
webRemote: webRemoteInstances,
|
|
67
|
+
remoteExec: remoteExecResult.map(node => ({ ...node, type: 'remote-exec' }))
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { discoverInstances, discoverWebRemote, discoverAll, UE_PORTS, WEB_REMOTE_PORTS };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const log = require('../utils/logger');
|
|
7
|
+
|
|
8
|
+
const HOME = os.homedir();
|
|
9
|
+
const APPDATA = process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming');
|
|
10
|
+
const LOCALAPPDATA = process.env.LOCALAPPDATA || path.join(HOME, 'AppData', 'Local');
|
|
11
|
+
|
|
12
|
+
const MCP_COMMAND = 'npx';
|
|
13
|
+
const MCP_ARGS = ['@createlex/createlexgenai', 'serve'];
|
|
14
|
+
const SERVER_NAME = 'createlex-unreal';
|
|
15
|
+
|
|
16
|
+
// IDE config paths and formats
|
|
17
|
+
const IDE_CONFIGS = {
|
|
18
|
+
'claude-code': {
|
|
19
|
+
name: 'Claude Code',
|
|
20
|
+
type: 'cli',
|
|
21
|
+
setupCommand: `claude mcp add ${SERVER_NAME} -- ${MCP_COMMAND} ${MCP_ARGS.join(' ')}`
|
|
22
|
+
},
|
|
23
|
+
'cursor': {
|
|
24
|
+
name: 'Cursor',
|
|
25
|
+
type: 'json',
|
|
26
|
+
configPath: path.join(HOME, '.cursor', 'mcp.json')
|
|
27
|
+
},
|
|
28
|
+
'windsurf': {
|
|
29
|
+
name: 'Windsurf',
|
|
30
|
+
type: 'json',
|
|
31
|
+
configPath: path.join(HOME, '.codeium', 'windsurf', 'mcp_config.json')
|
|
32
|
+
},
|
|
33
|
+
'claude-desktop': {
|
|
34
|
+
name: 'Claude Desktop',
|
|
35
|
+
type: 'json',
|
|
36
|
+
configPath: path.join(APPDATA, 'Claude', 'claude_desktop_config.json')
|
|
37
|
+
},
|
|
38
|
+
'antigravity': {
|
|
39
|
+
name: 'Antigravity',
|
|
40
|
+
type: 'json',
|
|
41
|
+
configPath: path.join(HOME, '.gemini', 'antigravity', 'mcp_config.json')
|
|
42
|
+
},
|
|
43
|
+
'kiro': {
|
|
44
|
+
name: 'Kiro',
|
|
45
|
+
type: 'json',
|
|
46
|
+
configPath: path.join(HOME, '.kiro', 'settings', 'mcp.json')
|
|
47
|
+
},
|
|
48
|
+
'trae': {
|
|
49
|
+
name: 'Trae',
|
|
50
|
+
type: 'json',
|
|
51
|
+
configPath: path.join(APPDATA, 'Trae', 'User', 'mcp.json')
|
|
52
|
+
},
|
|
53
|
+
'augment': {
|
|
54
|
+
name: 'Augment',
|
|
55
|
+
type: 'json',
|
|
56
|
+
configPath: path.join(HOME, '.augment', 'mcp.json')
|
|
57
|
+
},
|
|
58
|
+
'codex': {
|
|
59
|
+
name: 'Codex',
|
|
60
|
+
type: 'toml',
|
|
61
|
+
configPath: path.join(HOME, '.codex', 'config.toml')
|
|
62
|
+
},
|
|
63
|
+
'gemini-cli': {
|
|
64
|
+
name: 'Gemini CLI',
|
|
65
|
+
type: 'json',
|
|
66
|
+
configPath: path.join(HOME, '.gemini', 'settings.json')
|
|
67
|
+
},
|
|
68
|
+
'vscode': {
|
|
69
|
+
name: 'VS Code',
|
|
70
|
+
type: 'json-settings',
|
|
71
|
+
configPath: path.join(APPDATA, 'Code', 'User', 'settings.json')
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function getMcpServerEntry() {
|
|
76
|
+
return {
|
|
77
|
+
command: MCP_COMMAND,
|
|
78
|
+
args: MCP_ARGS
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function configureIde(ideKey) {
|
|
83
|
+
const ide = IDE_CONFIGS[ideKey];
|
|
84
|
+
if (!ide) {
|
|
85
|
+
return { success: false, error: `Unknown IDE: ${ideKey}. Available: ${Object.keys(IDE_CONFIGS).join(', ')}` };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (ide.type === 'cli') {
|
|
89
|
+
return { success: true, message: `Run this command:\n ${ide.setupCommand}`, manual: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (ide.type === 'toml') {
|
|
93
|
+
return configureToml(ide);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (ide.type === 'json-settings') {
|
|
97
|
+
return configureVsCodeSettings(ide);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return configureJson(ide);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function configureJson(ide) {
|
|
104
|
+
const configPath = ide.configPath;
|
|
105
|
+
const dir = path.dirname(configPath);
|
|
106
|
+
|
|
107
|
+
if (!fs.existsSync(dir)) {
|
|
108
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let config = {};
|
|
112
|
+
if (fs.existsSync(configPath)) {
|
|
113
|
+
try {
|
|
114
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
115
|
+
} catch {
|
|
116
|
+
config = {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!config.mcpServers) {
|
|
121
|
+
config.mcpServers = {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
config.mcpServers[SERVER_NAME] = getMcpServerEntry();
|
|
125
|
+
|
|
126
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
127
|
+
return { success: true, message: `Configured ${ide.name} at ${configPath}` };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function configureVsCodeSettings(ide) {
|
|
131
|
+
const configPath = ide.configPath;
|
|
132
|
+
|
|
133
|
+
if (!fs.existsSync(configPath)) {
|
|
134
|
+
return { success: false, error: `VS Code settings not found at ${configPath}. Open VS Code first.` };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let settings;
|
|
138
|
+
try {
|
|
139
|
+
settings = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
140
|
+
} catch {
|
|
141
|
+
return { success: false, error: 'Could not parse VS Code settings.json' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!settings['mcp.servers']) {
|
|
145
|
+
settings['mcp.servers'] = {};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
settings['mcp.servers'][SERVER_NAME] = getMcpServerEntry();
|
|
149
|
+
|
|
150
|
+
fs.writeFileSync(configPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
151
|
+
return { success: true, message: `Configured ${ide.name} at ${configPath}` };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function configureToml(ide) {
|
|
155
|
+
const configPath = ide.configPath;
|
|
156
|
+
const dir = path.dirname(configPath);
|
|
157
|
+
|
|
158
|
+
if (!fs.existsSync(dir)) {
|
|
159
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let content = '';
|
|
163
|
+
if (fs.existsSync(configPath)) {
|
|
164
|
+
content = fs.readFileSync(configPath, 'utf8');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const tomlSection = `\n[mcp_servers.${SERVER_NAME.replace(/-/g, '_')}]\ncommand = "${MCP_COMMAND}"\nargs = [${MCP_ARGS.map(a => `"${a}"`).join(', ')}]\n`;
|
|
168
|
+
|
|
169
|
+
// Check if already configured
|
|
170
|
+
if (content.includes(`mcp_servers.${SERVER_NAME.replace(/-/g, '_')}`)) {
|
|
171
|
+
return { success: true, message: `${ide.name} already configured at ${configPath}` };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
content += tomlSection;
|
|
175
|
+
fs.writeFileSync(configPath, content, 'utf8');
|
|
176
|
+
return { success: true, message: `Configured ${ide.name} at ${configPath}` };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getAvailableIdes() {
|
|
180
|
+
return Object.entries(IDE_CONFIGS).map(([key, ide]) => ({
|
|
181
|
+
key,
|
|
182
|
+
name: ide.name,
|
|
183
|
+
type: ide.type,
|
|
184
|
+
configPath: ide.configPath,
|
|
185
|
+
exists: ide.configPath ? fs.existsSync(path.dirname(ide.configPath)) : true
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { IDE_CONFIGS, configureIde, getAvailableIdes };
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const dgram = require('dgram');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
// UE Python Remote Execution protocol constants
|
|
8
|
+
const PROTOCOL_MAGIC = 'ue_py';
|
|
9
|
+
const PROTOCOL_VERSION = 1;
|
|
10
|
+
|
|
11
|
+
const MULTICAST_GROUP = '239.0.0.1';
|
|
12
|
+
const MULTICAST_PORT = 6766;
|
|
13
|
+
const COMMAND_PORT = 6776;
|
|
14
|
+
|
|
15
|
+
const DISCOVERY_TIMEOUT = 3000;
|
|
16
|
+
const CONNECT_TIMEOUT = 10000;
|
|
17
|
+
const COMMAND_TIMEOUT = 30000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Discover running Unreal Engine instances via UDP multicast ping.
|
|
21
|
+
* Returns an array of discovered nodes.
|
|
22
|
+
*/
|
|
23
|
+
function discover(timeout = DISCOVERY_TIMEOUT) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const nodes = [];
|
|
26
|
+
const nodeId = crypto.randomUUID();
|
|
27
|
+
|
|
28
|
+
const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
29
|
+
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
socket.close();
|
|
32
|
+
resolve(nodes);
|
|
33
|
+
}, timeout);
|
|
34
|
+
|
|
35
|
+
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
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
socket.on('error', () => {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
socket.close();
|
|
57
|
+
resolve(nodes);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
socket.bind(0, '0.0.0.0', () => {
|
|
61
|
+
try {
|
|
62
|
+
socket.addMembership(MULTICAST_GROUP);
|
|
63
|
+
} catch {
|
|
64
|
+
// Multicast may not be available
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ping = JSON.stringify({
|
|
68
|
+
magic: PROTOCOL_MAGIC,
|
|
69
|
+
version: PROTOCOL_VERSION,
|
|
70
|
+
type: 'ping',
|
|
71
|
+
source: nodeId
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
socket.send(ping, 0, ping.length, MULTICAST_PORT, MULTICAST_GROUP, (err) => {
|
|
75
|
+
if (err) {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
socket.close();
|
|
78
|
+
resolve(nodes);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Execute a Python command on a discovered UE instance via the Remote Execution protocol.
|
|
87
|
+
*
|
|
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
|
|
99
|
+
*/
|
|
100
|
+
function executeCommand(command, options = {}) {
|
|
101
|
+
const {
|
|
102
|
+
nodeId = null,
|
|
103
|
+
execMode = 'ExecuteStatement',
|
|
104
|
+
timeout = COMMAND_TIMEOUT
|
|
105
|
+
} = options;
|
|
106
|
+
|
|
107
|
+
const clientId = crypto.randomUUID();
|
|
108
|
+
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const timer = setTimeout(() => {
|
|
111
|
+
reject(new Error('Remote execution timed out. Ensure "Allow Python Remote Execution" is enabled in UE Project Settings > Python.'));
|
|
112
|
+
}, timeout);
|
|
113
|
+
|
|
114
|
+
// Step 1: Create TCP server to receive the connection from UE
|
|
115
|
+
const tcpServer = net.createServer((socket) => {
|
|
116
|
+
let responseBuffer = '';
|
|
117
|
+
|
|
118
|
+
socket.on('data', (data) => {
|
|
119
|
+
responseBuffer += data.toString();
|
|
120
|
+
|
|
121
|
+
// Try to parse complete JSON
|
|
122
|
+
try {
|
|
123
|
+
const result = JSON.parse(responseBuffer);
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
tcpServer.close();
|
|
126
|
+
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
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
socket.on('error', (err) => {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
tcpServer.close();
|
|
146
|
+
reject(new Error(`TCP socket error: ${err.message}`));
|
|
147
|
+
});
|
|
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
|
+
});
|
|
162
|
+
|
|
163
|
+
tcpServer.on('error', (err) => {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
reject(new Error(`TCP server error: ${err.message}`));
|
|
166
|
+
});
|
|
167
|
+
|
|
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;
|
|
171
|
+
|
|
172
|
+
const udpSocket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
173
|
+
|
|
174
|
+
udpSocket.bind(0, '0.0.0.0', () => {
|
|
175
|
+
try {
|
|
176
|
+
udpSocket.addMembership(MULTICAST_GROUP);
|
|
177
|
+
} catch {
|
|
178
|
+
// OK if multicast not available
|
|
179
|
+
}
|
|
180
|
+
|
|
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
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
udpSocket.send(openMsg, 0, openMsg.length, MULTICAST_PORT, MULTICAST_GROUP, () => {
|
|
192
|
+
udpSocket.close();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Quick test: discover + execute a simple command.
|
|
201
|
+
*/
|
|
202
|
+
async function testConnection() {
|
|
203
|
+
try {
|
|
204
|
+
const nodes = await discover(2000);
|
|
205
|
+
if (nodes.length === 0) {
|
|
206
|
+
return {
|
|
207
|
+
available: false,
|
|
208
|
+
error: 'No UE instances found via Remote Execution. Enable "Python Editor Script Plugin" and "Allow Python Remote Execution" in Project Settings.'
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
available: true,
|
|
213
|
+
nodes,
|
|
214
|
+
protocol: 'Python Remote Execution (UDP/TCP)'
|
|
215
|
+
};
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return { available: false, error: err.message };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
discover,
|
|
223
|
+
executeCommand,
|
|
224
|
+
testConnection,
|
|
225
|
+
MULTICAST_GROUP,
|
|
226
|
+
MULTICAST_PORT,
|
|
227
|
+
COMMAND_PORT
|
|
228
|
+
};
|