vortix 1.0.0 → 1.0.1
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/agent/agent.js +210 -0
- package/agent/auth.js +32 -0
- package/agent/config.json +3 -0
- package/agent/package-lock.json +37 -0
- package/agent/package.json +17 -0
- package/backend/package-lock.json +354 -0
- package/backend/package.json +21 -0
- package/backend/railway.json +11 -0
- package/backend/server.js +500 -0
- package/bin/vortix.js +23 -19
- package/package.json +6 -8
package/agent/agent.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const WebSocket = require("ws");
|
|
2
|
+
const { exec } = require("child_process");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
|
|
5
|
+
process.on("uncaughtException", (err) => {
|
|
6
|
+
console.error("Uncaught Exception:", err);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
process.on("unhandledRejection", (err) => {
|
|
10
|
+
console.error("Unhandled Rejection:", err);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const command = process.argv[2];
|
|
14
|
+
|
|
15
|
+
if (command === "login") {
|
|
16
|
+
console.log("Login is not required in this version.");
|
|
17
|
+
console.log("Just run: vortix start");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (command === "start") {
|
|
22
|
+
startAgent();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log("Available commands:");
|
|
27
|
+
console.log(" vortix start - Start the agent");
|
|
28
|
+
console.log(" vortix help - Show help");
|
|
29
|
+
|
|
30
|
+
function startAgent() {
|
|
31
|
+
const deviceName = os.hostname();
|
|
32
|
+
const token = `device-${deviceName.toLowerCase()}`;
|
|
33
|
+
|
|
34
|
+
console.log(`Connecting as device: ${deviceName}`);
|
|
35
|
+
console.log(`Using token: ${token}`);
|
|
36
|
+
|
|
37
|
+
// Production backend URL - Render deployment
|
|
38
|
+
const BACKEND_URL = process.env.BACKEND_URL || 'wss://vortix.onrender.com';
|
|
39
|
+
|
|
40
|
+
const ws = new WebSocket(`${BACKEND_URL}?token=${token}`);
|
|
41
|
+
|
|
42
|
+
ws.on("open", () => {
|
|
43
|
+
console.log("Authenticated and connected to backend");
|
|
44
|
+
|
|
45
|
+
setInterval(() => {
|
|
46
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
47
|
+
console.log("Sending heartbeat...");
|
|
48
|
+
ws.send(
|
|
49
|
+
JSON.stringify({
|
|
50
|
+
type: "HEARTBEAT"
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}, 5000);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
ws.on("close", () => {
|
|
58
|
+
console.log("Disconnected from backend");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
ws.on("error", (err) => {
|
|
62
|
+
console.error("WebSocket error:", err.message);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
let commandQueue = [];
|
|
66
|
+
let isRunning = false;
|
|
67
|
+
let currentCwd = require("os").homedir(); // Track current working directory
|
|
68
|
+
|
|
69
|
+
ws.on("message", (message) => {
|
|
70
|
+
try {
|
|
71
|
+
const data = JSON.parse(message.toString());
|
|
72
|
+
|
|
73
|
+
if (data.type === "EXECUTE") {
|
|
74
|
+
console.log("Agent received EXECUTE:", data.command);
|
|
75
|
+
commandQueue.push(data.command);
|
|
76
|
+
processQueue();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error("Invalid message received:", message.toString());
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
function extractDirectoryFromCommand(command) {
|
|
85
|
+
// Check for cd commands and extract the target directory
|
|
86
|
+
// Handle: cd /d D:, cd /D D:\, cd D:\folder, etc.
|
|
87
|
+
|
|
88
|
+
// Match: cd /d D: or cd /D D: or similar
|
|
89
|
+
let match = command.match(/^\s*cd\s+\/[dD]\s+([^\s]+)\s*$/i);
|
|
90
|
+
if (match) {
|
|
91
|
+
let path = match[1].trim();
|
|
92
|
+
path = expandPath(path);
|
|
93
|
+
// Ensure it ends with backslash if it's just a drive letter
|
|
94
|
+
if (/^[A-Za-z]:$/.test(path)) {
|
|
95
|
+
path = path + "\\";
|
|
96
|
+
}
|
|
97
|
+
console.log("CD detected, new path:", path);
|
|
98
|
+
return path;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Match: cd D:\folder or cd folder
|
|
102
|
+
match = command.match(/^\s*cd\s+([^\s][^\s]*)\s*$/i);
|
|
103
|
+
if (match) {
|
|
104
|
+
let path = match[1].trim();
|
|
105
|
+
path = expandPath(path);
|
|
106
|
+
// Only treat as cd if it looks like a path (has : or starts with \)
|
|
107
|
+
if (/[:\\]/.test(path)) {
|
|
108
|
+
if (/^[A-Za-z]:$/.test(path)) {
|
|
109
|
+
path = path + "\\";
|
|
110
|
+
}
|
|
111
|
+
console.log("CD detected, new path:", path);
|
|
112
|
+
return path;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function expandPath(path) {
|
|
120
|
+
// Expand environment variables
|
|
121
|
+
if (path.includes("%USERPROFILE%")) {
|
|
122
|
+
path = path.replace(/%USERPROFILE%/gi, os.homedir());
|
|
123
|
+
}
|
|
124
|
+
if (path.includes("%HOMEPATH%")) {
|
|
125
|
+
path = path.replace(/%HOMEPATH%/gi, os.homedir());
|
|
126
|
+
}
|
|
127
|
+
if (path.includes("%HOMEDRIVE%")) {
|
|
128
|
+
const homedir = os.homedir();
|
|
129
|
+
const drive = homedir.split("\\")[0];
|
|
130
|
+
path = path.replace(/%HOMEDRIVE%/gi, drive);
|
|
131
|
+
}
|
|
132
|
+
return path;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function processQueue() {
|
|
136
|
+
if (isRunning || commandQueue.length === 0) return;
|
|
137
|
+
|
|
138
|
+
isRunning = true;
|
|
139
|
+
let command = commandQueue.shift();
|
|
140
|
+
|
|
141
|
+
console.log("Executing command:", command);
|
|
142
|
+
|
|
143
|
+
// Update current working directory if this is a cd command
|
|
144
|
+
const newCwd = extractDirectoryFromCommand(command);
|
|
145
|
+
if (newCwd) {
|
|
146
|
+
currentCwd = newCwd;
|
|
147
|
+
ws.send(JSON.stringify({ type: "LOG", deviceName, message: `Directory changed to: ${currentCwd}` }));
|
|
148
|
+
console.log("Directory state updated to:", currentCwd);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// report current cwd for diagnostics
|
|
152
|
+
ws.send(JSON.stringify({ type: "LOG", deviceName, message: `Working from: ${currentCwd}` }));
|
|
153
|
+
ws.send(JSON.stringify({ type: "LOG", deviceName, message: `Executing: ${command}` }));
|
|
154
|
+
|
|
155
|
+
// Use cmd.exe with /c flag to properly execute Windows commands
|
|
156
|
+
const process = exec(command, {
|
|
157
|
+
cwd: currentCwd,
|
|
158
|
+
shell: "cmd.exe",
|
|
159
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large outputs
|
|
160
|
+
encoding: "utf8"
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Track if we've received any output
|
|
164
|
+
let hasOutput = false;
|
|
165
|
+
|
|
166
|
+
process.stdout.on("data", (data) => {
|
|
167
|
+
hasOutput = true;
|
|
168
|
+
ws.send(JSON.stringify({
|
|
169
|
+
type: "LOG",
|
|
170
|
+
deviceName,
|
|
171
|
+
message: data.toString()
|
|
172
|
+
}));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
process.stderr.on("data", (data) => {
|
|
176
|
+
hasOutput = true;
|
|
177
|
+
ws.send(JSON.stringify({
|
|
178
|
+
type: "LOG",
|
|
179
|
+
deviceName,
|
|
180
|
+
message: `[ERROR] ${data.toString()}`
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
process.on("error", (err) => {
|
|
185
|
+
ws.send(JSON.stringify({
|
|
186
|
+
type: "LOG",
|
|
187
|
+
deviceName,
|
|
188
|
+
message: `[EXEC ERROR] ${err.message}`
|
|
189
|
+
}));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
process.on("close", (code) => {
|
|
193
|
+
ws.send(JSON.stringify({
|
|
194
|
+
type: "LOG",
|
|
195
|
+
deviceName,
|
|
196
|
+
message: `Command execution finished with code: ${code}`
|
|
197
|
+
}));
|
|
198
|
+
|
|
199
|
+
// Send structured result so server can orchestrate sequential steps
|
|
200
|
+
ws.send(JSON.stringify({
|
|
201
|
+
type: "EXECUTE_RESULT",
|
|
202
|
+
command,
|
|
203
|
+
code: typeof code === 'number' ? code : 0
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
isRunning = false;
|
|
207
|
+
processQueue();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
package/agent/auth.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const readline = require("readline");
|
|
4
|
+
|
|
5
|
+
const CONFIG_PATH = path.join(__dirname, "config.json");
|
|
6
|
+
|
|
7
|
+
function login() {
|
|
8
|
+
const rl = readline.createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stdout
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
rl.question("Enter device token: ", (token) => {
|
|
14
|
+
const config = { token: token.trim() };
|
|
15
|
+
|
|
16
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
17
|
+
|
|
18
|
+
console.log("Token saved successfully.");
|
|
19
|
+
rl.close();
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getToken() {
|
|
24
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH));
|
|
29
|
+
return config.token;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { login, getToken };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "agent",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"ws": "^8.19.0"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"node_modules/ws": {
|
|
16
|
+
"version": "8.19.0",
|
|
17
|
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
|
18
|
+
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=10.0.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"bufferutil": "^4.0.1",
|
|
25
|
+
"utf-8-validate": ">=5.0.2"
|
|
26
|
+
},
|
|
27
|
+
"peerDependenciesMeta": {
|
|
28
|
+
"bufferutil": {
|
|
29
|
+
"optional": true
|
|
30
|
+
},
|
|
31
|
+
"utf-8-validate": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node agent.js",
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [],
|
|
11
|
+
"author": "",
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"type": "commonjs",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"ws": "^8.19.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vortix-backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "vortix-backend",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axios": "^1.13.5",
|
|
13
|
+
"ws": "^8.19.0"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=14.0.0"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"node_modules/asynckit": {
|
|
20
|
+
"version": "0.4.0",
|
|
21
|
+
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
|
22
|
+
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
|
23
|
+
"license": "MIT"
|
|
24
|
+
},
|
|
25
|
+
"node_modules/axios": {
|
|
26
|
+
"version": "1.13.5",
|
|
27
|
+
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
|
28
|
+
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"follow-redirects": "^1.15.11",
|
|
32
|
+
"form-data": "^4.0.5",
|
|
33
|
+
"proxy-from-env": "^1.1.0"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"node_modules/call-bind-apply-helpers": {
|
|
37
|
+
"version": "1.0.2",
|
|
38
|
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
|
39
|
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"es-errors": "^1.3.0",
|
|
43
|
+
"function-bind": "^1.1.2"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">= 0.4"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"node_modules/combined-stream": {
|
|
50
|
+
"version": "1.0.8",
|
|
51
|
+
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
|
52
|
+
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"delayed-stream": "~1.0.0"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">= 0.8"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"node_modules/delayed-stream": {
|
|
62
|
+
"version": "1.0.0",
|
|
63
|
+
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
|
64
|
+
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
|
65
|
+
"license": "MIT",
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=0.4.0"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"node_modules/dunder-proto": {
|
|
71
|
+
"version": "1.0.1",
|
|
72
|
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
|
73
|
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
|
74
|
+
"license": "MIT",
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"call-bind-apply-helpers": "^1.0.1",
|
|
77
|
+
"es-errors": "^1.3.0",
|
|
78
|
+
"gopd": "^1.2.0"
|
|
79
|
+
},
|
|
80
|
+
"engines": {
|
|
81
|
+
"node": ">= 0.4"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"node_modules/es-define-property": {
|
|
85
|
+
"version": "1.0.1",
|
|
86
|
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
|
87
|
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
|
88
|
+
"license": "MIT",
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">= 0.4"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"node_modules/es-errors": {
|
|
94
|
+
"version": "1.3.0",
|
|
95
|
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
|
96
|
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
|
97
|
+
"license": "MIT",
|
|
98
|
+
"engines": {
|
|
99
|
+
"node": ">= 0.4"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"node_modules/es-object-atoms": {
|
|
103
|
+
"version": "1.1.1",
|
|
104
|
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
|
105
|
+
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
|
106
|
+
"license": "MIT",
|
|
107
|
+
"dependencies": {
|
|
108
|
+
"es-errors": "^1.3.0"
|
|
109
|
+
},
|
|
110
|
+
"engines": {
|
|
111
|
+
"node": ">= 0.4"
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"node_modules/es-set-tostringtag": {
|
|
115
|
+
"version": "2.1.0",
|
|
116
|
+
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
|
117
|
+
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
|
118
|
+
"license": "MIT",
|
|
119
|
+
"dependencies": {
|
|
120
|
+
"es-errors": "^1.3.0",
|
|
121
|
+
"get-intrinsic": "^1.2.6",
|
|
122
|
+
"has-tostringtag": "^1.0.2",
|
|
123
|
+
"hasown": "^2.0.2"
|
|
124
|
+
},
|
|
125
|
+
"engines": {
|
|
126
|
+
"node": ">= 0.4"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"node_modules/follow-redirects": {
|
|
130
|
+
"version": "1.15.11",
|
|
131
|
+
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
|
132
|
+
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
|
133
|
+
"funding": [
|
|
134
|
+
{
|
|
135
|
+
"type": "individual",
|
|
136
|
+
"url": "https://github.com/sponsors/RubenVerborgh"
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
"license": "MIT",
|
|
140
|
+
"engines": {
|
|
141
|
+
"node": ">=4.0"
|
|
142
|
+
},
|
|
143
|
+
"peerDependenciesMeta": {
|
|
144
|
+
"debug": {
|
|
145
|
+
"optional": true
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"node_modules/form-data": {
|
|
150
|
+
"version": "4.0.5",
|
|
151
|
+
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
|
152
|
+
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
|
153
|
+
"license": "MIT",
|
|
154
|
+
"dependencies": {
|
|
155
|
+
"asynckit": "^0.4.0",
|
|
156
|
+
"combined-stream": "^1.0.8",
|
|
157
|
+
"es-set-tostringtag": "^2.1.0",
|
|
158
|
+
"hasown": "^2.0.2",
|
|
159
|
+
"mime-types": "^2.1.12"
|
|
160
|
+
},
|
|
161
|
+
"engines": {
|
|
162
|
+
"node": ">= 6"
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
"node_modules/function-bind": {
|
|
166
|
+
"version": "1.1.2",
|
|
167
|
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
|
168
|
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
|
169
|
+
"license": "MIT",
|
|
170
|
+
"funding": {
|
|
171
|
+
"url": "https://github.com/sponsors/ljharb"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"node_modules/get-intrinsic": {
|
|
175
|
+
"version": "1.3.0",
|
|
176
|
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
|
177
|
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
|
178
|
+
"license": "MIT",
|
|
179
|
+
"dependencies": {
|
|
180
|
+
"call-bind-apply-helpers": "^1.0.2",
|
|
181
|
+
"es-define-property": "^1.0.1",
|
|
182
|
+
"es-errors": "^1.3.0",
|
|
183
|
+
"es-object-atoms": "^1.1.1",
|
|
184
|
+
"function-bind": "^1.1.2",
|
|
185
|
+
"get-proto": "^1.0.1",
|
|
186
|
+
"gopd": "^1.2.0",
|
|
187
|
+
"has-symbols": "^1.1.0",
|
|
188
|
+
"hasown": "^2.0.2",
|
|
189
|
+
"math-intrinsics": "^1.1.0"
|
|
190
|
+
},
|
|
191
|
+
"engines": {
|
|
192
|
+
"node": ">= 0.4"
|
|
193
|
+
},
|
|
194
|
+
"funding": {
|
|
195
|
+
"url": "https://github.com/sponsors/ljharb"
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
"node_modules/get-proto": {
|
|
199
|
+
"version": "1.0.1",
|
|
200
|
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
|
201
|
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
|
202
|
+
"license": "MIT",
|
|
203
|
+
"dependencies": {
|
|
204
|
+
"dunder-proto": "^1.0.1",
|
|
205
|
+
"es-object-atoms": "^1.0.0"
|
|
206
|
+
},
|
|
207
|
+
"engines": {
|
|
208
|
+
"node": ">= 0.4"
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
"node_modules/gopd": {
|
|
212
|
+
"version": "1.2.0",
|
|
213
|
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
|
214
|
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
|
215
|
+
"license": "MIT",
|
|
216
|
+
"engines": {
|
|
217
|
+
"node": ">= 0.4"
|
|
218
|
+
},
|
|
219
|
+
"funding": {
|
|
220
|
+
"url": "https://github.com/sponsors/ljharb"
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
"node_modules/has-symbols": {
|
|
224
|
+
"version": "1.1.0",
|
|
225
|
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
|
226
|
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
|
227
|
+
"license": "MIT",
|
|
228
|
+
"engines": {
|
|
229
|
+
"node": ">= 0.4"
|
|
230
|
+
},
|
|
231
|
+
"funding": {
|
|
232
|
+
"url": "https://github.com/sponsors/ljharb"
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
"node_modules/has-tostringtag": {
|
|
236
|
+
"version": "1.0.2",
|
|
237
|
+
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
|
238
|
+
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
|
239
|
+
"license": "MIT",
|
|
240
|
+
"dependencies": {
|
|
241
|
+
"has-symbols": "^1.0.3"
|
|
242
|
+
},
|
|
243
|
+
"engines": {
|
|
244
|
+
"node": ">= 0.4"
|
|
245
|
+
},
|
|
246
|
+
"funding": {
|
|
247
|
+
"url": "https://github.com/sponsors/ljharb"
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
"node_modules/hasown": {
|
|
251
|
+
"version": "2.0.2",
|
|
252
|
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
|
253
|
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
|
254
|
+
"license": "MIT",
|
|
255
|
+
"dependencies": {
|
|
256
|
+
"function-bind": "^1.1.2"
|
|
257
|
+
},
|
|
258
|
+
"engines": {
|
|
259
|
+
"node": ">= 0.4"
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
"node_modules/math-intrinsics": {
|
|
263
|
+
"version": "1.1.0",
|
|
264
|
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
|
265
|
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
|
266
|
+
"license": "MIT",
|
|
267
|
+
"engines": {
|
|
268
|
+
"node": ">= 0.4"
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
"node_modules/mime-db": {
|
|
272
|
+
"version": "1.52.0",
|
|
273
|
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
|
274
|
+
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
|
275
|
+
"license": "MIT",
|
|
276
|
+
"engines": {
|
|
277
|
+
"node": ">= 0.6"
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"node_modules/mime-types": {
|
|
281
|
+
"version": "2.1.35",
|
|
282
|
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
|
283
|
+
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
|
284
|
+
"license": "MIT",
|
|
285
|
+
"dependencies": {
|
|
286
|
+
"mime-db": "1.52.0"
|
|
287
|
+
},
|
|
288
|
+
"engines": {
|
|
289
|
+
"node": ">= 0.6"
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
"node_modules/openai": {
|
|
293
|
+
"version": "6.18.0",
|
|
294
|
+
"resolved": "https://registry.npmjs.org/openai/-/openai-6.18.0.tgz",
|
|
295
|
+
"integrity": "sha512-odLRYyz9rlzz6g8gKn61RM2oP5UUm428sE2zOxZqS9MzVfD5/XW8UoEjpnRkzTuScXP7ZbP/m7fC+bl8jCOZZw==",
|
|
296
|
+
"license": "Apache-2.0",
|
|
297
|
+
"bin": {
|
|
298
|
+
"openai": "bin/cli"
|
|
299
|
+
},
|
|
300
|
+
"peerDependencies": {
|
|
301
|
+
"ws": "^8.18.0",
|
|
302
|
+
"zod": "^3.25 || ^4.0"
|
|
303
|
+
},
|
|
304
|
+
"peerDependenciesMeta": {
|
|
305
|
+
"ws": {
|
|
306
|
+
"optional": true
|
|
307
|
+
},
|
|
308
|
+
"zod": {
|
|
309
|
+
"optional": true
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
"node_modules/proxy-from-env": {
|
|
314
|
+
"version": "1.1.0",
|
|
315
|
+
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
|
316
|
+
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
|
317
|
+
"license": "MIT"
|
|
318
|
+
},
|
|
319
|
+
"node_modules/uuid": {
|
|
320
|
+
"version": "13.0.0",
|
|
321
|
+
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
|
322
|
+
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
|
323
|
+
"funding": [
|
|
324
|
+
"https://github.com/sponsors/broofa",
|
|
325
|
+
"https://github.com/sponsors/ctavan"
|
|
326
|
+
],
|
|
327
|
+
"license": "MIT",
|
|
328
|
+
"bin": {
|
|
329
|
+
"uuid": "dist-node/bin/uuid"
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
"node_modules/ws": {
|
|
333
|
+
"version": "8.19.0",
|
|
334
|
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
|
335
|
+
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
|
336
|
+
"license": "MIT",
|
|
337
|
+
"engines": {
|
|
338
|
+
"node": ">=10.0.0"
|
|
339
|
+
},
|
|
340
|
+
"peerDependencies": {
|
|
341
|
+
"bufferutil": "^4.0.1",
|
|
342
|
+
"utf-8-validate": ">=5.0.2"
|
|
343
|
+
},
|
|
344
|
+
"peerDependenciesMeta": {
|
|
345
|
+
"bufferutil": {
|
|
346
|
+
"optional": true
|
|
347
|
+
},
|
|
348
|
+
"utf-8-validate": {
|
|
349
|
+
"optional": true
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vortix-backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vortix WebSocket backend server",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node server.js",
|
|
8
|
+
"dev": "node server.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": ["websocket", "remote-control", "ai"],
|
|
11
|
+
"author": "",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "commonjs",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=14.0.0"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"axios": "^1.13.5",
|
|
19
|
+
"ws": "^8.19.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
const WebSocket = require("ws");
|
|
2
|
+
const readline = require("readline");
|
|
3
|
+
const axios = require("axios");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
// Use environment PORT for cloud deployment, fallback to 8080 for local
|
|
8
|
+
const PORT = process.env.PORT || 8080;
|
|
9
|
+
|
|
10
|
+
const dashboardClients = new Set();
|
|
11
|
+
const devices = new Map();
|
|
12
|
+
// pendingResults holds awaiting resolvers for EXECUTE_RESULT from agents
|
|
13
|
+
const pendingResults = new Map(); // deviceName -> [{ command, resolve, reject, timer }]
|
|
14
|
+
|
|
15
|
+
const wss = new WebSocket.Server({ port: PORT });
|
|
16
|
+
|
|
17
|
+
console.log(`Backend running on port ${PORT}`);
|
|
18
|
+
|
|
19
|
+
function waitForExecuteResult(deviceName, command, timeoutMs = 120000) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const list = pendingResults.get(deviceName) || [];
|
|
22
|
+
const entry = { command, resolve, reject, timer: null };
|
|
23
|
+
// timeout
|
|
24
|
+
entry.timer = setTimeout(() => {
|
|
25
|
+
// remove entry
|
|
26
|
+
const arr = pendingResults.get(deviceName) || [];
|
|
27
|
+
const idx = arr.indexOf(entry);
|
|
28
|
+
if (idx !== -1) arr.splice(idx, 1);
|
|
29
|
+
pendingResults.set(deviceName, arr);
|
|
30
|
+
reject(new Error('Timed out waiting for EXECUTE_RESULT'));
|
|
31
|
+
}, timeoutMs);
|
|
32
|
+
|
|
33
|
+
list.push(entry);
|
|
34
|
+
pendingResults.set(deviceName, list);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------- WebSocket Handling ----------
|
|
39
|
+
wss.on("connection", (ws, req) => {
|
|
40
|
+
const url = new URL(req.url, "http://localhost");
|
|
41
|
+
|
|
42
|
+
const token = url.searchParams.get("token");
|
|
43
|
+
const clientType = url.searchParams.get("type");
|
|
44
|
+
|
|
45
|
+
// ===== DASHBOARD CONNECTION =====
|
|
46
|
+
if (clientType === "dashboard") {
|
|
47
|
+
dashboardClients.add(ws);
|
|
48
|
+
console.log("Dashboard connected");
|
|
49
|
+
// Send initial device list to newly connected dashboard
|
|
50
|
+
broadcastDevices();
|
|
51
|
+
|
|
52
|
+
ws.on("message", async (message) => {
|
|
53
|
+
const data = JSON.parse(message);
|
|
54
|
+
if (data.type === "FORCE_EXECUTE") {
|
|
55
|
+
const targetDevice = [...devices.values()].find(
|
|
56
|
+
(d) =>
|
|
57
|
+
d.deviceName === data.deviceName &&
|
|
58
|
+
d.status === "online"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (!targetDevice) return;
|
|
62
|
+
|
|
63
|
+
targetDevice.ws.send(
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
type: "EXECUTE",
|
|
66
|
+
command: data.command,
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
console.log("Force executed dangerous command");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// if (data.type === "COMMAND") {
|
|
74
|
+
// const targetDevice = [...devices.values()].find(
|
|
75
|
+
// (d) =>
|
|
76
|
+
// d.deviceName === data.deviceName &&
|
|
77
|
+
// d.status === "online"
|
|
78
|
+
// );
|
|
79
|
+
|
|
80
|
+
// if (!targetDevice) {
|
|
81
|
+
// console.log("Target device not found or offline");
|
|
82
|
+
// return;
|
|
83
|
+
// }
|
|
84
|
+
// if (isDangerousCommand(data.command)) {
|
|
85
|
+
// ws.send(
|
|
86
|
+
// JSON.stringify({
|
|
87
|
+
// type: "APPROVAL_REQUIRED",
|
|
88
|
+
// deviceName: data.deviceName,
|
|
89
|
+
// command: data.command
|
|
90
|
+
// })
|
|
91
|
+
// );
|
|
92
|
+
|
|
93
|
+
// console.log("Dangerous command detected, approval required");
|
|
94
|
+
// return;
|
|
95
|
+
// }
|
|
96
|
+
|
|
97
|
+
// targetDevice.ws.send(
|
|
98
|
+
// JSON.stringify({
|
|
99
|
+
// type: "EXECUTE",
|
|
100
|
+
// command: data.command,
|
|
101
|
+
// })
|
|
102
|
+
// );
|
|
103
|
+
|
|
104
|
+
// console.log(
|
|
105
|
+
// `Dashboard sent command to ${data.deviceName}`
|
|
106
|
+
// );
|
|
107
|
+
// }
|
|
108
|
+
if (data.type === "APPROVE_PLAN") {
|
|
109
|
+
const targetDevice = [...devices.values()].find(
|
|
110
|
+
(d) =>
|
|
111
|
+
d.deviceName === data.deviceName &&
|
|
112
|
+
d.status === "online"
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (!targetDevice) {
|
|
116
|
+
console.log("Target device not found or offline");
|
|
117
|
+
dashboardClients.forEach((client) => {
|
|
118
|
+
client.send(
|
|
119
|
+
JSON.stringify({
|
|
120
|
+
type: "EXECUTION_FINISHED",
|
|
121
|
+
deviceName: data.deviceName,
|
|
122
|
+
error: "Device not found or offline"
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log("APPROVE_PLAN received:", data.steps);
|
|
130
|
+
|
|
131
|
+
// Notify all dashboard clients execution started
|
|
132
|
+
dashboardClients.forEach((client) => {
|
|
133
|
+
client.send(
|
|
134
|
+
JSON.stringify({
|
|
135
|
+
type: "EXECUTION_STARTED",
|
|
136
|
+
deviceName: data.deviceName
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Execute steps sequentially: wait for agent to report EXECUTE_RESULT for each step
|
|
142
|
+
(async () => {
|
|
143
|
+
let aborted = false;
|
|
144
|
+
for (const step of data.steps) {
|
|
145
|
+
// Send command EXACTLY as provided - don't modify it
|
|
146
|
+
const commandToExecute = step.command || step;
|
|
147
|
+
console.log("Sending EXECUTE to agent for:", commandToExecute);
|
|
148
|
+
|
|
149
|
+
targetDevice.ws.send(
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
type: "EXECUTE",
|
|
152
|
+
command: commandToExecute
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const result = await waitForExecuteResult(targetDevice.deviceName, commandToExecute);
|
|
158
|
+
console.log("Step result:", result);
|
|
159
|
+
|
|
160
|
+
// send step log to dashboards
|
|
161
|
+
dashboardClients.forEach((client) => {
|
|
162
|
+
client.send(
|
|
163
|
+
JSON.stringify({
|
|
164
|
+
type: "LOG",
|
|
165
|
+
deviceName: targetDevice.deviceName,
|
|
166
|
+
message: `✓ Step completed: ${commandToExecute} (exit code: ${result.code})`
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Continue even if code is not 0 - let user see the output
|
|
172
|
+
// but notify about non-zero exit
|
|
173
|
+
if (typeof result.code === 'number' && result.code !== 0) {
|
|
174
|
+
console.log(`Step exited with code ${result.code}: ${commandToExecute}`);
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error("Step execution error:", err.message);
|
|
178
|
+
dashboardClients.forEach((client) => {
|
|
179
|
+
client.send(
|
|
180
|
+
JSON.stringify({
|
|
181
|
+
type: "LOG",
|
|
182
|
+
deviceName: targetDevice.deviceName,
|
|
183
|
+
message: `✗ Step failed: ${commandToExecute} - ${err.message}`
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
dashboardClients.forEach((client) => {
|
|
188
|
+
client.send(
|
|
189
|
+
JSON.stringify({
|
|
190
|
+
type: "EXECUTION_FINISHED",
|
|
191
|
+
deviceName: data.deviceName,
|
|
192
|
+
error: err.message
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
aborted = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!aborted) {
|
|
202
|
+
dashboardClients.forEach((client) => {
|
|
203
|
+
client.send(
|
|
204
|
+
JSON.stringify({
|
|
205
|
+
type: "EXECUTION_FINISHED",
|
|
206
|
+
deviceName: data.deviceName,
|
|
207
|
+
success: true
|
|
208
|
+
})
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (data.type === "PLAN") {
|
|
216
|
+
|
|
217
|
+
const targetDevice = [...devices.values()].find(
|
|
218
|
+
(d) =>
|
|
219
|
+
d.deviceName === data.deviceName &&
|
|
220
|
+
d.status === "online"
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (!targetDevice) {
|
|
224
|
+
console.log("Device not found or offline");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const plan = await generatePlan(
|
|
230
|
+
data.command,
|
|
231
|
+
targetDevice.platform || "win32"
|
|
232
|
+
);
|
|
233
|
+
console.log("Generated plan:", plan.steps);
|
|
234
|
+
|
|
235
|
+
// SEND PLAN PREVIEW TO DASHBOARD - wait for approval
|
|
236
|
+
ws.send(
|
|
237
|
+
JSON.stringify({
|
|
238
|
+
type: "PLAN_PREVIEW",
|
|
239
|
+
deviceName: data.deviceName,
|
|
240
|
+
steps: plan.steps
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
console.log("AI Plan sent for approval:", plan);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.error("AI planning error:", err.message);
|
|
247
|
+
// Notify dashboard of error
|
|
248
|
+
dashboardClients.forEach((client) => {
|
|
249
|
+
client.send(
|
|
250
|
+
JSON.stringify({
|
|
251
|
+
type: "EXECUTION_FINISHED",
|
|
252
|
+
deviceName: data.deviceName,
|
|
253
|
+
error: err.message
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
ws.on("close", () => {
|
|
263
|
+
dashboardClients.delete(ws);
|
|
264
|
+
console.log("Dashboard disconnected");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
// ===== AGENT CONNECTION =====
|
|
272
|
+
if (!token || !devices.has(token)) {
|
|
273
|
+
console.log("Unauthorized connection attempt");
|
|
274
|
+
ws.close();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const device = devices.get(token);
|
|
279
|
+
|
|
280
|
+
device.ws = ws;
|
|
281
|
+
device.status = "online";
|
|
282
|
+
device.lastSeen = Date.now();
|
|
283
|
+
|
|
284
|
+
console.log(`Authenticated device: ${device.deviceName}`);
|
|
285
|
+
|
|
286
|
+
broadcastDevices(); // 🔥 notify dashboard
|
|
287
|
+
|
|
288
|
+
ws.on("message", (message) => {
|
|
289
|
+
const data = JSON.parse(message);
|
|
290
|
+
|
|
291
|
+
if (data.type === "HEARTBEAT") {
|
|
292
|
+
device.lastSeen = Date.now();
|
|
293
|
+
console.log(`Heartbeat received from ${device.deviceName}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (data.type === "LOG") {
|
|
297
|
+
console.log(`[${device.deviceName}] ${data.message}`);
|
|
298
|
+
|
|
299
|
+
// 🔥 send logs to dashboard
|
|
300
|
+
dashboardClients.forEach((client) => {
|
|
301
|
+
client.send(
|
|
302
|
+
JSON.stringify({
|
|
303
|
+
type: "LOG",
|
|
304
|
+
deviceName: device.deviceName,
|
|
305
|
+
message: data.message,
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (data.type === "EXECUTE_RESULT") {
|
|
312
|
+
console.log(`EXECUTE_RESULT received from ${device.deviceName}:`, data);
|
|
313
|
+
const list = pendingResults.get(device.deviceName) || [];
|
|
314
|
+
for (let i = 0; i < list.length; i++) {
|
|
315
|
+
const entry = list[i];
|
|
316
|
+
if (entry.command === data.command) {
|
|
317
|
+
clearTimeout(entry.timer);
|
|
318
|
+
try {
|
|
319
|
+
entry.resolve(data);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
entry.reject(e);
|
|
322
|
+
}
|
|
323
|
+
list.splice(i, 1);
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
pendingResults.set(device.deviceName, list);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
ws.on("close", () => {
|
|
332
|
+
device.status = "offline";
|
|
333
|
+
console.log(`${device.deviceName} disconnected`);
|
|
334
|
+
broadcastDevices(); // 🔥 notify dashboard
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ---------- Device Registration ----------
|
|
339
|
+
function registerDevice(deviceName, token) {
|
|
340
|
+
// If no token provided, use the hostname-based token for consistency
|
|
341
|
+
if (!token) {
|
|
342
|
+
token = `device-${deviceName.toLowerCase()}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
devices.set(token, {
|
|
346
|
+
deviceName,
|
|
347
|
+
status: "offline",
|
|
348
|
+
ws: null,
|
|
349
|
+
lastSeen: null
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
console.log(`Registered device: ${deviceName}`);
|
|
353
|
+
console.log(`Token: ${token}`);
|
|
354
|
+
|
|
355
|
+
return token;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Register one device manually for testing
|
|
359
|
+
registerDevice("Test-Device");
|
|
360
|
+
registerDevice("VAIBHAV-PC");
|
|
361
|
+
|
|
362
|
+
// ---------- Heartbeat Monitor ----------
|
|
363
|
+
setInterval(() => {
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
|
|
366
|
+
devices.forEach((device) => {
|
|
367
|
+
if (
|
|
368
|
+
device.status === "online" &&
|
|
369
|
+
device.lastSeen &&
|
|
370
|
+
now - device.lastSeen > 15000
|
|
371
|
+
) {
|
|
372
|
+
device.status = "offline";
|
|
373
|
+
console.log(`${device.deviceName} marked offline`);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}, 5000);
|
|
377
|
+
|
|
378
|
+
// ---------- Terminal Command Interface (only in local development) ----------
|
|
379
|
+
if (process.env.NODE_ENV !== 'production' && !process.env.RAILWAY_ENVIRONMENT) {
|
|
380
|
+
const rl = readline.createInterface({
|
|
381
|
+
input: process.stdin,
|
|
382
|
+
output: process.stdout
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
console.log("Type: send <DeviceName> <command>");
|
|
386
|
+
|
|
387
|
+
rl.on("line", (input) => {
|
|
388
|
+
const [keyword, deviceName, ...cmdParts] = input.split(" ");
|
|
389
|
+
const command = cmdParts.join(" ");
|
|
390
|
+
|
|
391
|
+
if (keyword !== "send") {
|
|
392
|
+
console.log("Invalid command. Use: send <DeviceName> <command>");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let targetDevice = null;
|
|
397
|
+
|
|
398
|
+
devices.forEach((device) => {
|
|
399
|
+
if (device.deviceName === deviceName && device.status === "online") {
|
|
400
|
+
targetDevice = device;
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (!targetDevice) {
|
|
405
|
+
console.log("Device not found or offline.");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
targetDevice.ws.send(
|
|
410
|
+
JSON.stringify({
|
|
411
|
+
type: "EXECUTE",
|
|
412
|
+
command
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
console.log(`Command sent to ${deviceName}`);
|
|
417
|
+
});
|
|
418
|
+
} else {
|
|
419
|
+
console.log("Running in production mode - terminal interface disabled");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function generatePlan(userInput, platform) {
|
|
423
|
+
const homeDir = os.homedir();
|
|
424
|
+
const desktopPath = path.join(homeDir, "Desktop");
|
|
425
|
+
|
|
426
|
+
const prompt = `
|
|
427
|
+
You are an AI OS command planner. Generate EXACT Windows commands for the user's request.
|
|
428
|
+
|
|
429
|
+
System Information:
|
|
430
|
+
- Home Directory: ${homeDir}
|
|
431
|
+
- Desktop Path: ${desktopPath}
|
|
432
|
+
- Platform: ${platform}
|
|
433
|
+
|
|
434
|
+
CRITICAL RULES:
|
|
435
|
+
1. Return ONLY valid JSON in this format - nothing else
|
|
436
|
+
2. Use ABSOLUTE paths, NOT relative paths
|
|
437
|
+
3. Use echo command for file creation (NOT type, NOT powershell)
|
|
438
|
+
4. One command per step
|
|
439
|
+
5. Never create unnecessary directories before files
|
|
440
|
+
6. Format: echo "content" > full_path_to_file.txt
|
|
441
|
+
|
|
442
|
+
CORRECT Examples:
|
|
443
|
+
- "create hello.txt with html on desktop"
|
|
444
|
+
Response: {"steps": [{"command": "echo <!DOCTYPE html><html><head><title>Hello</title></head><body><h1>Hello</h1></body></html> > ${desktopPath}\\hello.txt"}]}
|
|
445
|
+
|
|
446
|
+
- "create test file with hello world"
|
|
447
|
+
Response: {"steps": [{"command": "echo hello world > ${homeDir}\\Desktop\\test.txt"}]}
|
|
448
|
+
|
|
449
|
+
- "list desktop files"
|
|
450
|
+
Response: {"steps": [{"command": "dir ${desktopPath}"}]}
|
|
451
|
+
|
|
452
|
+
User Request: ${userInput}
|
|
453
|
+
|
|
454
|
+
Return ONLY JSON:
|
|
455
|
+
`;
|
|
456
|
+
|
|
457
|
+
const response = await axios.post(
|
|
458
|
+
"http://localhost:11434/api/generate",
|
|
459
|
+
{
|
|
460
|
+
model: "qwen2.5:7b",
|
|
461
|
+
prompt: prompt,
|
|
462
|
+
stream: false
|
|
463
|
+
}
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
const text = response.data.response.trim();
|
|
467
|
+
|
|
468
|
+
console.log("LLM raw output:", text);
|
|
469
|
+
|
|
470
|
+
// Extract JSON safely
|
|
471
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
472
|
+
|
|
473
|
+
if (!jsonMatch) {
|
|
474
|
+
throw new Error("No valid JSON found in LLM output");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return JSON.parse(jsonMatch[0]);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
function broadcastDevices() {
|
|
483
|
+
const deviceList = [];
|
|
484
|
+
|
|
485
|
+
devices.forEach((device) => {
|
|
486
|
+
deviceList.push({
|
|
487
|
+
deviceName: device.deviceName,
|
|
488
|
+
status: device.status,
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
dashboardClients.forEach((client) => {
|
|
493
|
+
client.send(
|
|
494
|
+
JSON.stringify({
|
|
495
|
+
type: "DEVICES",
|
|
496
|
+
devices: deviceList,
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
}
|
package/bin/vortix.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { spawn } = require('child_process');
|
|
4
3
|
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
4
|
|
|
7
5
|
const command = process.argv[2];
|
|
8
|
-
const args = process.argv.slice(3);
|
|
9
6
|
|
|
10
|
-
//
|
|
11
|
-
const
|
|
12
|
-
const
|
|
7
|
+
// When installed via npm, files are in the package directory
|
|
8
|
+
const packageRoot = path.join(__dirname, '..');
|
|
9
|
+
const agentPath = path.join(packageRoot, 'agent', 'agent.js');
|
|
10
|
+
const backendPath = path.join(packageRoot, 'backend', 'server.js');
|
|
13
11
|
|
|
14
12
|
function showHelp() {
|
|
15
13
|
console.log(`
|
|
@@ -29,28 +27,34 @@ Examples:
|
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
function runAgent(cmd) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
cwd: path.dirname(agentPath)
|
|
35
|
-
});
|
|
30
|
+
// Set the command argument for the agent
|
|
31
|
+
process.argv[2] = cmd;
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
// Change to agent directory
|
|
34
|
+
process.chdir(path.dirname(agentPath));
|
|
35
|
+
|
|
36
|
+
// Require and run the agent
|
|
37
|
+
try {
|
|
38
|
+
require(agentPath);
|
|
39
|
+
} catch (err) {
|
|
38
40
|
console.error('Failed to start agent:', err.message);
|
|
39
41
|
process.exit(1);
|
|
40
|
-
}
|
|
42
|
+
}
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
function runBackend() {
|
|
44
46
|
console.log('Starting Vortix backend server...');
|
|
45
|
-
const proc = spawn('node', [backendPath], {
|
|
46
|
-
stdio: 'inherit',
|
|
47
|
-
cwd: path.dirname(backendPath)
|
|
48
|
-
});
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
// Change to backend directory
|
|
49
|
+
process.chdir(path.dirname(backendPath));
|
|
50
|
+
|
|
51
|
+
// Require and run the backend
|
|
52
|
+
try {
|
|
53
|
+
require(backendPath);
|
|
54
|
+
} catch (err) {
|
|
51
55
|
console.error('Failed to start backend:', err.message);
|
|
52
56
|
process.exit(1);
|
|
53
|
-
}
|
|
57
|
+
}
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
switch (command) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vortix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "AI-powered OS control system with remote command execution",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,25 +14,23 @@
|
|
|
14
14
|
"os-control",
|
|
15
15
|
"agent"
|
|
16
16
|
],
|
|
17
|
-
"author": "
|
|
17
|
+
"author": "Vaibhav Rajpoot <vaibhavrajpoot2626@gmail.com>",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/
|
|
21
|
+
"url": "https://github.com/Vaibhav262610/vortix.git"
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=14.0.0"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"bin/",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
28
|
+
"agent/",
|
|
29
|
+
"backend/",
|
|
30
30
|
"README.md"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"axios": "^1.6.0",
|
|
34
|
-
"ws": "^8.19.0"
|
|
35
|
-
"uuid": "^13.0.0",
|
|
36
|
-
"openai": "^6.18.0"
|
|
34
|
+
"ws": "^8.19.0"
|
|
37
35
|
}
|
|
38
36
|
}
|