@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,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
+ };