vortix 1.2.0 → 1.2.2
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 +49 -35
- package/agent/package-lock.json +165 -0
- package/agent/start-local.bat +3 -0
- package/agent/start-production.bat +3 -0
- package/agent/test-screenshot.js +19 -0
- package/backend/DEPLOY.md +261 -0
- package/backend/Procfile +1 -0
- package/backend/README.md +80 -0
- package/backend/server.js +788 -237
- package/package.json +1 -1
- package/CHANGELOG.md +0 -86
package/backend/server.js
CHANGED
|
@@ -1,20 +1,46 @@
|
|
|
1
1
|
const WebSocket = require("ws");
|
|
2
|
+
const http = require("http");
|
|
2
3
|
const readline = require("readline");
|
|
3
4
|
const axios = require("axios");
|
|
4
5
|
const os = require("os");
|
|
5
6
|
const path = require("path");
|
|
7
|
+
const crypto = require("crypto");
|
|
6
8
|
|
|
7
9
|
// Use environment PORT for cloud deployment, fallback to 8080 for local
|
|
8
10
|
const PORT = process.env.PORT || 8080;
|
|
9
11
|
|
|
10
12
|
const dashboardClients = new Set();
|
|
11
|
-
const devices = new Map();
|
|
12
|
-
|
|
13
|
-
const
|
|
13
|
+
const devices = new Map(); // deviceId -> { deviceName, password, status, ws, lastSeen, platform }
|
|
14
|
+
const pendingResults = new Map();
|
|
15
|
+
const screenShareSessions = new Map(); // deviceName -> Set of dashboard websockets
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
// Helper to hash passwords
|
|
18
|
+
function hashPassword(password) {
|
|
19
|
+
return crypto.createHash('sha256').update(password).digest('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Create HTTP server for health checks and WebSocket upgrade
|
|
23
|
+
const server = http.createServer((req, res) => {
|
|
24
|
+
if (req.url === '/health' || req.url === '/') {
|
|
25
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
26
|
+
res.end(JSON.stringify({
|
|
27
|
+
status: 'ok',
|
|
28
|
+
devices: devices.size,
|
|
29
|
+
dashboards: dashboardClients.size,
|
|
30
|
+
uptime: process.uptime()
|
|
31
|
+
}));
|
|
32
|
+
} else {
|
|
33
|
+
res.writeHead(404);
|
|
34
|
+
res.end('Not Found');
|
|
35
|
+
}
|
|
36
|
+
});
|
|
16
37
|
|
|
17
|
-
|
|
38
|
+
const wss = new WebSocket.Server({ server });
|
|
39
|
+
|
|
40
|
+
server.listen(PORT, () => {
|
|
41
|
+
console.log(`Backend running on port ${PORT}`);
|
|
42
|
+
console.log(`Health check: http://localhost:${PORT}/health`);
|
|
43
|
+
});
|
|
18
44
|
|
|
19
45
|
function waitForExecuteResult(deviceName, command, timeoutMs = 120000) {
|
|
20
46
|
return new Promise((resolve, reject) => {
|
|
@@ -44,21 +70,62 @@ wss.on("connection", (ws, req) => {
|
|
|
44
70
|
|
|
45
71
|
// ===== DASHBOARD CONNECTION =====
|
|
46
72
|
if (clientType === "dashboard") {
|
|
73
|
+
ws.authenticatedDevices = new Set(); // Track which devices this dashboard can access
|
|
47
74
|
dashboardClients.add(ws);
|
|
48
75
|
console.log("Dashboard connected");
|
|
49
|
-
// Send initial device list to newly connected dashboard
|
|
50
|
-
broadcastDevices();
|
|
51
76
|
|
|
52
77
|
ws.on("message", async (message) => {
|
|
53
78
|
const data = JSON.parse(message);
|
|
79
|
+
|
|
80
|
+
// Dashboard authenticates to view a specific device
|
|
81
|
+
if (data.type === "AUTH_DEVICE") {
|
|
82
|
+
const { deviceName, password } = data;
|
|
83
|
+
const deviceId = `device-${deviceName.toLowerCase()}`;
|
|
84
|
+
const device = devices.get(deviceId);
|
|
85
|
+
|
|
86
|
+
if (!device) {
|
|
87
|
+
ws.send(JSON.stringify({
|
|
88
|
+
type: "AUTH_ERROR",
|
|
89
|
+
deviceName,
|
|
90
|
+
error: "Device not found"
|
|
91
|
+
}));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (device.passwordHash !== hashPassword(password)) {
|
|
96
|
+
ws.send(JSON.stringify({
|
|
97
|
+
type: "AUTH_ERROR",
|
|
98
|
+
deviceName,
|
|
99
|
+
error: "Invalid password"
|
|
100
|
+
}));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Authentication successful
|
|
105
|
+
ws.authenticatedDevices.add(deviceId);
|
|
106
|
+
ws.send(JSON.stringify({
|
|
107
|
+
type: "AUTH_SUCCESS",
|
|
108
|
+
deviceName
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
// Send updated device list
|
|
112
|
+
broadcastDevicesToDashboard(ws);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
54
116
|
if (data.type === "FORCE_EXECUTE") {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
117
|
+
const deviceId = `device-${data.deviceName.toLowerCase()}`;
|
|
118
|
+
|
|
119
|
+
if (!ws.authenticatedDevices.has(deviceId)) {
|
|
120
|
+
ws.send(JSON.stringify({
|
|
121
|
+
type: "ERROR",
|
|
122
|
+
message: "Not authenticated for this device"
|
|
123
|
+
}));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
60
126
|
|
|
61
|
-
|
|
127
|
+
const targetDevice = devices.get(deviceId);
|
|
128
|
+
if (!targetDevice || targetDevice.status !== "online") return;
|
|
62
129
|
|
|
63
130
|
targetDevice.ws.send(
|
|
64
131
|
JSON.stringify({
|
|
@@ -66,85 +133,40 @@ wss.on("connection", (ws, req) => {
|
|
|
66
133
|
command: data.command,
|
|
67
134
|
})
|
|
68
135
|
);
|
|
69
|
-
|
|
70
|
-
console.log("Force executed dangerous command");
|
|
71
136
|
}
|
|
72
137
|
|
|
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
138
|
if (data.type === "APPROVE_PLAN") {
|
|
109
|
-
const
|
|
110
|
-
(d) =>
|
|
111
|
-
d.deviceName === data.deviceName &&
|
|
112
|
-
d.status === "online"
|
|
113
|
-
);
|
|
139
|
+
const deviceId = `device-${data.deviceName.toLowerCase()}`;
|
|
114
140
|
|
|
115
|
-
if (!
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
type: "EXECUTION_FINISHED",
|
|
121
|
-
deviceName: data.deviceName,
|
|
122
|
-
error: "Device not found or offline"
|
|
123
|
-
})
|
|
124
|
-
);
|
|
125
|
-
});
|
|
141
|
+
if (!ws.authenticatedDevices.has(deviceId)) {
|
|
142
|
+
ws.send(JSON.stringify({
|
|
143
|
+
type: "ERROR",
|
|
144
|
+
message: "Not authenticated for this device"
|
|
145
|
+
}));
|
|
126
146
|
return;
|
|
127
147
|
}
|
|
128
148
|
|
|
129
|
-
|
|
149
|
+
const targetDevice = devices.get(deviceId);
|
|
150
|
+
if (!targetDevice || targetDevice.status !== "online") {
|
|
151
|
+
ws.send(JSON.stringify({
|
|
152
|
+
type: "EXECUTION_FINISHED",
|
|
153
|
+
deviceName: data.deviceName,
|
|
154
|
+
error: "Device not found or offline"
|
|
155
|
+
}));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
130
158
|
|
|
131
|
-
// Notify
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
deviceName: data.deviceName
|
|
137
|
-
})
|
|
138
|
-
);
|
|
139
|
-
});
|
|
159
|
+
// Notify dashboard execution started
|
|
160
|
+
ws.send(JSON.stringify({
|
|
161
|
+
type: "EXECUTION_STARTED",
|
|
162
|
+
deviceName: data.deviceName
|
|
163
|
+
}));
|
|
140
164
|
|
|
141
|
-
// Execute steps sequentially
|
|
165
|
+
// Execute steps sequentially
|
|
142
166
|
(async () => {
|
|
143
167
|
let aborted = false;
|
|
144
168
|
for (const step of data.steps) {
|
|
145
|
-
// Send command EXACTLY as provided - don't modify it
|
|
146
169
|
const commandToExecute = step.command || step;
|
|
147
|
-
console.log("Sending EXECUTE to agent for:", commandToExecute);
|
|
148
170
|
|
|
149
171
|
targetDevice.ws.send(
|
|
150
172
|
JSON.stringify({
|
|
@@ -155,161 +177,507 @@ wss.on("connection", (ws, req) => {
|
|
|
155
177
|
|
|
156
178
|
try {
|
|
157
179
|
const result = await waitForExecuteResult(targetDevice.deviceName, commandToExecute);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
180
|
+
|
|
181
|
+
ws.send(JSON.stringify({
|
|
182
|
+
type: "LOG",
|
|
183
|
+
deviceName: targetDevice.deviceName,
|
|
184
|
+
message: `✓ Step completed: ${commandToExecute} (exit code: ${result.code})`
|
|
185
|
+
}));
|
|
186
|
+
|
|
173
187
|
if (typeof result.code === 'number' && result.code !== 0) {
|
|
174
|
-
console.log(`Step exited with code ${result.code}
|
|
188
|
+
console.log(`Step exited with code ${result.code}`);
|
|
175
189
|
}
|
|
176
190
|
} catch (err) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
});
|
|
191
|
+
ws.send(JSON.stringify({
|
|
192
|
+
type: "LOG",
|
|
193
|
+
deviceName: targetDevice.deviceName,
|
|
194
|
+
message: `✗ Step failed: ${commandToExecute} - ${err.message}`
|
|
195
|
+
}));
|
|
196
|
+
ws.send(JSON.stringify({
|
|
197
|
+
type: "EXECUTION_FINISHED",
|
|
198
|
+
deviceName: data.deviceName,
|
|
199
|
+
error: err.message
|
|
200
|
+
}));
|
|
196
201
|
aborted = true;
|
|
197
202
|
break;
|
|
198
203
|
}
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
if (!aborted) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
success: true
|
|
208
|
-
})
|
|
209
|
-
);
|
|
210
|
-
});
|
|
207
|
+
ws.send(JSON.stringify({
|
|
208
|
+
type: "EXECUTION_FINISHED",
|
|
209
|
+
deviceName: data.deviceName,
|
|
210
|
+
success: true
|
|
211
|
+
}));
|
|
211
212
|
}
|
|
212
213
|
})();
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
if (data.type === "PLAN") {
|
|
217
|
+
const deviceId = `device-${data.deviceName.toLowerCase()}`;
|
|
216
218
|
|
|
217
|
-
|
|
218
|
-
(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (!targetDevice) {
|
|
224
|
-
console.log("Device not found or offline");
|
|
219
|
+
if (!ws.authenticatedDevices.has(deviceId)) {
|
|
220
|
+
ws.send(JSON.stringify({
|
|
221
|
+
type: "ERROR",
|
|
222
|
+
message: "Not authenticated for this device"
|
|
223
|
+
}));
|
|
225
224
|
return;
|
|
226
225
|
}
|
|
227
226
|
|
|
227
|
+
const targetDevice = devices.get(deviceId);
|
|
228
|
+
if (!targetDevice || targetDevice.status !== "online") return;
|
|
229
|
+
|
|
228
230
|
try {
|
|
231
|
+
const userApiKey = data.apiKey || null;
|
|
232
|
+
|
|
229
233
|
const plan = await generatePlan(
|
|
230
234
|
data.command,
|
|
231
|
-
targetDevice.platform
|
|
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
|
-
})
|
|
235
|
+
targetDevice.platform && targetDevice.platform !== "unknown" ? targetDevice.platform : "win32",
|
|
236
|
+
userApiKey
|
|
242
237
|
);
|
|
243
238
|
|
|
244
|
-
|
|
239
|
+
ws.send(JSON.stringify({
|
|
240
|
+
type: "PLAN_PREVIEW",
|
|
241
|
+
deviceName: data.deviceName,
|
|
242
|
+
steps: plan.steps
|
|
243
|
+
}));
|
|
245
244
|
} catch (err) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
);
|
|
256
|
-
});
|
|
245
|
+
const errorMessage = err.message.includes("Ollama")
|
|
246
|
+
? "AI planning is not available. Please enter commands directly."
|
|
247
|
+
: `AI planning error: ${err.message}`;
|
|
248
|
+
|
|
249
|
+
ws.send(JSON.stringify({
|
|
250
|
+
type: "PLAN_ERROR",
|
|
251
|
+
deviceName: data.deviceName,
|
|
252
|
+
error: errorMessage
|
|
253
|
+
}));
|
|
257
254
|
}
|
|
258
255
|
}
|
|
259
256
|
|
|
257
|
+
// Screen sharing
|
|
258
|
+
if (data.type === "START_SCREEN_SHARE") {
|
|
259
|
+
const deviceId = `device-${data.deviceName.toLowerCase()}`;
|
|
260
|
+
|
|
261
|
+
if (!ws.authenticatedDevices.has(deviceId)) {
|
|
262
|
+
ws.send(JSON.stringify({
|
|
263
|
+
type: "ERROR",
|
|
264
|
+
message: "Not authenticated for this device"
|
|
265
|
+
}));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const targetDevice = devices.get(deviceId);
|
|
270
|
+
if (!targetDevice || targetDevice.status !== "online") {
|
|
271
|
+
ws.send(JSON.stringify({
|
|
272
|
+
type: "ERROR",
|
|
273
|
+
message: "Device not found or offline"
|
|
274
|
+
}));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Add this dashboard to screen share sessions
|
|
279
|
+
if (!screenShareSessions.has(data.deviceName)) {
|
|
280
|
+
screenShareSessions.set(data.deviceName, new Set());
|
|
281
|
+
}
|
|
282
|
+
screenShareSessions.get(data.deviceName).add(ws);
|
|
283
|
+
|
|
284
|
+
// Request screen share from agent
|
|
285
|
+
targetDevice.ws.send(JSON.stringify({
|
|
286
|
+
type: "START_SCREEN_CAPTURE"
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (data.type === "STOP_SCREEN_SHARE") {
|
|
291
|
+
const deviceId = `device-${data.deviceName.toLowerCase()}`;
|
|
292
|
+
const targetDevice = devices.get(deviceId);
|
|
293
|
+
|
|
294
|
+
// Remove this dashboard from screen share sessions
|
|
295
|
+
if (screenShareSessions.has(data.deviceName)) {
|
|
296
|
+
screenShareSessions.get(data.deviceName).delete(ws);
|
|
297
|
+
|
|
298
|
+
// If no more dashboards watching, stop capture on agent
|
|
299
|
+
if (screenShareSessions.get(data.deviceName).size === 0) {
|
|
300
|
+
screenShareSessions.delete(data.deviceName);
|
|
301
|
+
if (targetDevice && targetDevice.ws) {
|
|
302
|
+
targetDevice.ws.send(JSON.stringify({
|
|
303
|
+
type: "STOP_SCREEN_CAPTURE"
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Auto-start control
|
|
311
|
+
if (data.type === "ENABLE_AUTOSTART") {
|
|
312
|
+
const deviceId = `device-${data.deviceName.toLowerCase()}`;
|
|
313
|
+
|
|
314
|
+
if (!ws.authenticatedDevices.has(deviceId)) {
|
|
315
|
+
ws.send(JSON.stringify({
|
|
316
|
+
type: "ERROR",
|
|
317
|
+
message: "Not authenticated for this device"
|
|
318
|
+
}));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const targetDevice = devices.get(deviceId);
|
|
323
|
+
if (!targetDevice || targetDevice.status !== "online") {
|
|
324
|
+
ws.send(JSON.stringify({
|
|
325
|
+
type: "ERROR",
|
|
326
|
+
message: "Device not found or offline"
|
|
327
|
+
}));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
targetDevice.ws.send(JSON.stringify({
|
|
332
|
+
type: "ENABLE_AUTOSTART"
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (data.type === "DISABLE_AUTOSTART") {
|
|
337
|
+
const deviceId = `device-${data.deviceName.toLowerCase()}`;
|
|
338
|
+
|
|
339
|
+
if (!ws.authenticatedDevices.has(deviceId)) {
|
|
340
|
+
ws.send(JSON.stringify({
|
|
341
|
+
type: "ERROR",
|
|
342
|
+
message: "Not authenticated for this device"
|
|
343
|
+
}));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const targetDevice = devices.get(deviceId);
|
|
348
|
+
if (!targetDevice || targetDevice.status !== "online") {
|
|
349
|
+
ws.send(JSON.stringify({
|
|
350
|
+
type: "ERROR",
|
|
351
|
+
message: "Device not found or offline"
|
|
352
|
+
}));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
targetDevice.ws.send(JSON.stringify({
|
|
357
|
+
type: "DISABLE_AUTOSTART"
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (data.type === "GET_AUTOSTART_STATUS") {
|
|
362
|
+
const deviceId = `device-${data.deviceName.toLowerCase()}`;
|
|
363
|
+
|
|
364
|
+
if (!ws.authenticatedDevices.has(deviceId)) {
|
|
365
|
+
ws.send(JSON.stringify({
|
|
366
|
+
type: "ERROR",
|
|
367
|
+
message: "Not authenticated for this device"
|
|
368
|
+
}));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const targetDevice = devices.get(deviceId);
|
|
373
|
+
if (!targetDevice || targetDevice.status !== "online") {
|
|
374
|
+
ws.send(JSON.stringify({
|
|
375
|
+
type: "ERROR",
|
|
376
|
+
message: "Device not found or offline"
|
|
377
|
+
}));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
targetDevice.ws.send(JSON.stringify({
|
|
382
|
+
type: "GET_AUTOSTART_STATUS"
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Multi-device execution
|
|
387
|
+
if (data.type === "MULTI_DEVICE_EXECUTE") {
|
|
388
|
+
const { deviceNames, command } = data;
|
|
389
|
+
|
|
390
|
+
deviceNames.forEach(deviceName => {
|
|
391
|
+
const deviceId = `device-${deviceName.toLowerCase()}`;
|
|
392
|
+
|
|
393
|
+
if (!ws.authenticatedDevices.has(deviceId)) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const targetDevice = devices.get(deviceId);
|
|
398
|
+
if (!targetDevice || targetDevice.status !== "online") {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Send command to each device
|
|
403
|
+
targetDevice.ws.send(JSON.stringify({
|
|
404
|
+
type: "EXECUTE",
|
|
405
|
+
command
|
|
406
|
+
}));
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// System stats request
|
|
411
|
+
if (data.type === "GET_SYSTEM_STATS") {
|
|
412
|
+
const deviceId = `device-${data.deviceName.toLowerCase()}`;
|
|
413
|
+
|
|
414
|
+
if (!ws.authenticatedDevices.has(deviceId)) {
|
|
415
|
+
console.log("Backend: Device not authenticated:", deviceId);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const targetDevice = devices.get(deviceId);
|
|
420
|
+
if (!targetDevice || targetDevice.status !== "online") {
|
|
421
|
+
console.log("Backend: Device not found or offline:", deviceId);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
targetDevice.ws.send(JSON.stringify({
|
|
426
|
+
type: "GET_SYSTEM_STATS"
|
|
427
|
+
}));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// File transfer - Browse files
|
|
431
|
+
if (data.type === "BROWSE_FILES") {
|
|
432
|
+
console.log("Backend: Received BROWSE_FILES request for", data.deviceName, "path:", data.path);
|
|
433
|
+
const deviceId = `device-${data.deviceName.toLowerCase()}`;
|
|
434
|
+
|
|
435
|
+
if (!ws.authenticatedDevices.has(deviceId)) {
|
|
436
|
+
console.log("Backend: Not authenticated for device:", data.deviceName);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const targetDevice = devices.get(deviceId);
|
|
441
|
+
if (!targetDevice || targetDevice.status !== "online") {
|
|
442
|
+
console.log("Backend: Device not found or offline:", data.deviceName);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
console.log("Backend: Forwarding BROWSE_FILES to agent");
|
|
447
|
+
targetDevice.ws.send(JSON.stringify({
|
|
448
|
+
type: "BROWSE_FILES",
|
|
449
|
+
path: data.path
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// File transfer - Upload file
|
|
454
|
+
if (data.type === "UPLOAD_FILE") {
|
|
455
|
+
const deviceId = `device-${data.deviceName.toLowerCase()}`;
|
|
456
|
+
|
|
457
|
+
if (!ws.authenticatedDevices.has(deviceId)) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const targetDevice = devices.get(deviceId);
|
|
462
|
+
if (!targetDevice || targetDevice.status !== "online") {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
targetDevice.ws.send(JSON.stringify({
|
|
467
|
+
type: "UPLOAD_FILE",
|
|
468
|
+
fileName: data.fileName,
|
|
469
|
+
fileData: data.fileData,
|
|
470
|
+
targetPath: data.targetPath
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// File transfer - Download file
|
|
475
|
+
if (data.type === "DOWNLOAD_FILE") {
|
|
476
|
+
const deviceId = `device-${data.deviceName.toLowerCase()}`;
|
|
477
|
+
|
|
478
|
+
if (!ws.authenticatedDevices.has(deviceId)) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const targetDevice = devices.get(deviceId);
|
|
483
|
+
if (!targetDevice || targetDevice.status !== "online") {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
targetDevice.ws.send(JSON.stringify({
|
|
488
|
+
type: "DOWNLOAD_FILE",
|
|
489
|
+
filePath: data.filePath
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
260
492
|
});
|
|
261
493
|
|
|
262
494
|
ws.on("close", () => {
|
|
263
495
|
dashboardClients.delete(ws);
|
|
496
|
+
|
|
497
|
+
// Clean up screen share sessions
|
|
498
|
+
screenShareSessions.forEach((sessions, deviceName) => {
|
|
499
|
+
if (sessions.has(ws)) {
|
|
500
|
+
sessions.delete(ws);
|
|
501
|
+
if (sessions.size === 0) {
|
|
502
|
+
screenShareSessions.delete(deviceName);
|
|
503
|
+
const deviceId = `device-${deviceName.toLowerCase()}`;
|
|
504
|
+
const device = devices.get(deviceId);
|
|
505
|
+
if (device && device.ws) {
|
|
506
|
+
device.ws.send(JSON.stringify({
|
|
507
|
+
type: "STOP_SCREEN_CAPTURE"
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
264
514
|
console.log("Dashboard disconnected");
|
|
265
515
|
});
|
|
266
516
|
|
|
517
|
+
// Send list of all devices (without sensitive info)
|
|
518
|
+
broadcastDevicesToDashboard(ws);
|
|
267
519
|
return;
|
|
268
520
|
}
|
|
269
521
|
|
|
270
|
-
|
|
271
522
|
// ===== AGENT CONNECTION =====
|
|
272
|
-
if (!token
|
|
273
|
-
console.log("
|
|
523
|
+
if (!token) {
|
|
524
|
+
console.log("No token provided");
|
|
525
|
+
ws.close();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Token format: device-hostname:password
|
|
530
|
+
const tokenParts = token.split(':');
|
|
531
|
+
|
|
532
|
+
if (tokenParts.length < 2) {
|
|
533
|
+
console.log("Invalid token format - missing password");
|
|
274
534
|
ws.close();
|
|
275
535
|
return;
|
|
276
536
|
}
|
|
277
537
|
|
|
278
|
-
|
|
538
|
+
// First part is device token, rest is password (in case password contains ':')
|
|
539
|
+
const deviceToken = tokenParts[0];
|
|
540
|
+
const password = tokenParts.slice(1).join(':');
|
|
541
|
+
|
|
542
|
+
if (!deviceToken || !password) {
|
|
543
|
+
console.log("Invalid token format");
|
|
544
|
+
ws.close();
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const deviceId = deviceToken;
|
|
549
|
+
const deviceName = deviceToken.replace('device-', '').toUpperCase();
|
|
550
|
+
|
|
551
|
+
// Auto-register device or verify password
|
|
552
|
+
if (!devices.has(deviceId)) {
|
|
553
|
+
console.log(`Registering new device: ${deviceName}`);
|
|
554
|
+
devices.set(deviceId, {
|
|
555
|
+
deviceName,
|
|
556
|
+
passwordHash: hashPassword(password),
|
|
557
|
+
status: "offline",
|
|
558
|
+
ws: null,
|
|
559
|
+
lastSeen: null,
|
|
560
|
+
platform: "unknown" // Will be updated when agent connects
|
|
561
|
+
});
|
|
562
|
+
} else {
|
|
563
|
+
// Verify password
|
|
564
|
+
const device = devices.get(deviceId);
|
|
565
|
+
if (device.passwordHash !== hashPassword(password)) {
|
|
566
|
+
console.log(`Invalid password for device: ${deviceName}`);
|
|
567
|
+
ws.close();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
279
571
|
|
|
572
|
+
const device = devices.get(deviceId);
|
|
280
573
|
device.ws = ws;
|
|
281
574
|
device.status = "online";
|
|
282
575
|
device.lastSeen = Date.now();
|
|
283
576
|
|
|
284
|
-
console.log(`
|
|
285
|
-
|
|
286
|
-
broadcastDevices(); // 🔥 notify dashboard
|
|
577
|
+
console.log(`Device connected: ${deviceName}`);
|
|
578
|
+
broadcastDevices();
|
|
287
579
|
|
|
288
580
|
ws.on("message", (message) => {
|
|
289
581
|
const data = JSON.parse(message);
|
|
290
582
|
|
|
291
583
|
if (data.type === "HEARTBEAT") {
|
|
292
584
|
device.lastSeen = Date.now();
|
|
293
|
-
|
|
585
|
+
// Update platform info if provided
|
|
586
|
+
if (data.platform) {
|
|
587
|
+
device.platform = data.platform;
|
|
588
|
+
}
|
|
294
589
|
}
|
|
295
590
|
|
|
296
591
|
if (data.type === "LOG") {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
// 🔥 send logs to dashboard
|
|
592
|
+
// Send logs only to authenticated dashboards
|
|
300
593
|
dashboardClients.forEach((client) => {
|
|
301
|
-
client.
|
|
302
|
-
JSON.stringify({
|
|
594
|
+
if (client.authenticatedDevices.has(deviceId)) {
|
|
595
|
+
client.send(JSON.stringify({
|
|
303
596
|
type: "LOG",
|
|
304
597
|
deviceName: device.deviceName,
|
|
305
598
|
message: data.message,
|
|
306
|
-
})
|
|
307
|
-
|
|
599
|
+
}));
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (data.type === "SCREEN_FRAME") {
|
|
605
|
+
// Broadcast screen frame to all dashboards watching this device
|
|
606
|
+
if (screenShareSessions.has(device.deviceName)) {
|
|
607
|
+
screenShareSessions.get(device.deviceName).forEach((dashboardWs) => {
|
|
608
|
+
if (dashboardWs.readyState === WebSocket.OPEN) {
|
|
609
|
+
dashboardWs.send(JSON.stringify({
|
|
610
|
+
type: "SCREEN_FRAME",
|
|
611
|
+
deviceName: device.deviceName,
|
|
612
|
+
frame: data.frame
|
|
613
|
+
}));
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (data.type === "AUTOSTART_STATUS" || data.type === "AUTOSTART_ERROR") {
|
|
620
|
+
// Forward auto-start status to authenticated dashboards
|
|
621
|
+
dashboardClients.forEach((dashboardWs) => {
|
|
622
|
+
const deviceId = `device-${device.deviceName.toLowerCase()}`;
|
|
623
|
+
if (dashboardWs.authenticatedDevices.has(deviceId)) {
|
|
624
|
+
dashboardWs.send(JSON.stringify({
|
|
625
|
+
type: data.type,
|
|
626
|
+
deviceName: device.deviceName,
|
|
627
|
+
enabled: data.enabled,
|
|
628
|
+
message: data.message
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Forward system stats to authenticated dashboards
|
|
635
|
+
if (data.type === "SYSTEM_STATS") {
|
|
636
|
+
dashboardClients.forEach((dashboardWs) => {
|
|
637
|
+
const deviceId = `device-${device.deviceName.toLowerCase()}`;
|
|
638
|
+
if (dashboardWs.authenticatedDevices.has(deviceId)) {
|
|
639
|
+
dashboardWs.send(JSON.stringify({
|
|
640
|
+
type: "SYSTEM_STATS",
|
|
641
|
+
deviceName: device.deviceName,
|
|
642
|
+
stats: data.stats
|
|
643
|
+
}));
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Forward file list to authenticated dashboards
|
|
649
|
+
if (data.type === "FILE_LIST") {
|
|
650
|
+
console.log("Backend: Received FILE_LIST from agent", device.deviceName, "with", data.files?.length, "files");
|
|
651
|
+
dashboardClients.forEach((dashboardWs) => {
|
|
652
|
+
const deviceId = `device-${device.deviceName.toLowerCase()}`;
|
|
653
|
+
if (dashboardWs.authenticatedDevices.has(deviceId)) {
|
|
654
|
+
console.log("Backend: Forwarding FILE_LIST to dashboard for", device.deviceName);
|
|
655
|
+
dashboardWs.send(JSON.stringify({
|
|
656
|
+
type: "FILE_LIST",
|
|
657
|
+
deviceName: device.deviceName,
|
|
658
|
+
files: data.files,
|
|
659
|
+
path: data.path
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Forward file download data to authenticated dashboards
|
|
666
|
+
if (data.type === "FILE_DATA") {
|
|
667
|
+
dashboardClients.forEach((dashboardWs) => {
|
|
668
|
+
const deviceId = `device-${device.deviceName.toLowerCase()}`;
|
|
669
|
+
if (dashboardWs.authenticatedDevices.has(deviceId)) {
|
|
670
|
+
dashboardWs.send(JSON.stringify({
|
|
671
|
+
type: "FILE_DATA",
|
|
672
|
+
deviceName: device.deviceName,
|
|
673
|
+
fileName: data.fileName,
|
|
674
|
+
fileData: data.fileData
|
|
675
|
+
}));
|
|
676
|
+
}
|
|
308
677
|
});
|
|
309
678
|
}
|
|
310
679
|
|
|
311
680
|
if (data.type === "EXECUTE_RESULT") {
|
|
312
|
-
console.log(`EXECUTE_RESULT received from ${device.deviceName}:`, data);
|
|
313
681
|
const list = pendingResults.get(device.deviceName) || [];
|
|
314
682
|
for (let i = 0; i < list.length; i++) {
|
|
315
683
|
const entry = list[i];
|
|
@@ -331,34 +699,10 @@ wss.on("connection", (ws, req) => {
|
|
|
331
699
|
ws.on("close", () => {
|
|
332
700
|
device.status = "offline";
|
|
333
701
|
console.log(`${device.deviceName} disconnected`);
|
|
334
|
-
broadcastDevices();
|
|
702
|
+
broadcastDevices();
|
|
335
703
|
});
|
|
336
704
|
});
|
|
337
705
|
|
|
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
706
|
// ---------- Heartbeat Monitor ----------
|
|
363
707
|
setInterval(() => {
|
|
364
708
|
const now = Date.now();
|
|
@@ -419,82 +763,289 @@ if (process.env.NODE_ENV !== 'production' && !process.env.RAILWAY_ENVIRONMENT) {
|
|
|
419
763
|
console.log("Running in production mode - terminal interface disabled");
|
|
420
764
|
}
|
|
421
765
|
|
|
422
|
-
async function generatePlan(userInput, platform) {
|
|
766
|
+
async function generatePlan(userInput, platform, userApiKey = null) {
|
|
423
767
|
const homeDir = os.homedir();
|
|
424
768
|
const desktopPath = path.join(homeDir, "Desktop");
|
|
425
769
|
|
|
770
|
+
// Platform-specific command patterns
|
|
771
|
+
const isWindows = platform === 'win32';
|
|
772
|
+
const isMac = platform === 'darwin';
|
|
773
|
+
const isLinux = platform === 'linux';
|
|
774
|
+
|
|
775
|
+
let platformInstructions = '';
|
|
776
|
+
|
|
777
|
+
if (isWindows) {
|
|
778
|
+
platformInstructions = `
|
|
779
|
+
WINDOWS COMMAND PATTERNS:
|
|
780
|
+
|
|
781
|
+
File Operations:
|
|
782
|
+
- Create text file: echo Hello World > ${desktopPath}\\\\file.txt
|
|
783
|
+
- Create HTML: echo ^<!DOCTYPE html^>^<html^>^<body^>Hello^</body^>^</html^> > ${desktopPath}\\\\page.html
|
|
784
|
+
- Create directory: mkdir ${desktopPath}\\\\newfolder
|
|
785
|
+
- Copy file: copy source.txt ${desktopPath}\\\\destination.txt
|
|
786
|
+
- Delete file: del ${desktopPath}\\\\file.txt
|
|
787
|
+
- List files: dir ${desktopPath}
|
|
788
|
+
|
|
789
|
+
System Operations:
|
|
790
|
+
- Open file: start ${desktopPath}\\\\file.txt
|
|
791
|
+
- Open folder: explorer ${desktopPath}
|
|
792
|
+
- System info: systeminfo
|
|
793
|
+
- Network info: ipconfig
|
|
794
|
+
- Process list: tasklist
|
|
795
|
+
- Lock PC: rundll32.exe user32.dll,LockWorkStation
|
|
796
|
+
- Shutdown: shutdown /s /t 0
|
|
797
|
+
- Restart: shutdown /r /t 0
|
|
798
|
+
|
|
799
|
+
Opening Applications (ALWAYS prefer installed apps over websites):
|
|
800
|
+
- Open Notion app: start notion://
|
|
801
|
+
- Open Spotify app: start spotify:
|
|
802
|
+
- Open Discord app: start discord://
|
|
803
|
+
- Open Slack app: start slack://
|
|
804
|
+
- Open VS Code: code
|
|
805
|
+
- Open Chrome: start chrome
|
|
806
|
+
- Open Firefox: start firefox
|
|
807
|
+
- Open Edge: start msedge
|
|
808
|
+
- Open Notepad: start notepad
|
|
809
|
+
- Open Calculator: start calc
|
|
810
|
+
- Open Paint: start mspaint
|
|
811
|
+
- Open File Explorer: start explorer
|
|
812
|
+
- Generic app: start <appname>
|
|
813
|
+
|
|
814
|
+
IMPORTANT: When user says "open [app name]", ALWAYS try to open the installed application first using "start <appname>" or "start <appname>://". Only open websites if explicitly asked for "website" or "browser".
|
|
815
|
+
|
|
816
|
+
EXAMPLES:
|
|
817
|
+
Request: "open notion"
|
|
818
|
+
Response: {"steps": [{"command": "start notion://"}]}
|
|
819
|
+
|
|
820
|
+
Request: "open notion website"
|
|
821
|
+
Response: {"steps": [{"command": "start https://notion.so"}]}
|
|
822
|
+
|
|
823
|
+
Request: "open spotify"
|
|
824
|
+
Response: {"steps": [{"command": "start spotify:"}]}
|
|
825
|
+
|
|
826
|
+
Request: "create hello.html on desktop"
|
|
827
|
+
Response: {"steps": [{"command": "echo ^<!DOCTYPE html^>^<html^>^<head^>^<title^>Hello^</title^>^</head^>^<body^>^<h1^>Hello World^</h1^>^</body^>^</html^> > ${desktopPath}\\\\hello.html"}]}
|
|
828
|
+
`;
|
|
829
|
+
} else if (isMac || isLinux) {
|
|
830
|
+
platformInstructions = `
|
|
831
|
+
${isMac ? 'macOS' : 'LINUX'} COMMAND PATTERNS:
|
|
832
|
+
|
|
833
|
+
File Operations:
|
|
834
|
+
- Create text file: echo "Hello World" > ${desktopPath}/file.txt
|
|
835
|
+
- Create HTML: echo '<!DOCTYPE html><html><body>Hello</body></html>' > ${desktopPath}/page.html
|
|
836
|
+
- Create directory: mkdir -p ${desktopPath}/newfolder
|
|
837
|
+
- Copy file: cp source.txt ${desktopPath}/destination.txt
|
|
838
|
+
- Delete file: rm ${desktopPath}/file.txt
|
|
839
|
+
- List files: ls -la ${desktopPath}
|
|
840
|
+
|
|
841
|
+
System Operations:
|
|
842
|
+
- Open file: ${isMac ? 'open' : 'xdg-open'} ${desktopPath}/file.txt
|
|
843
|
+
- Open folder: ${isMac ? 'open' : 'xdg-open'} ${desktopPath}
|
|
844
|
+
- System info: ${isMac ? 'system_profiler SPSoftwareDataType' : 'uname -a'}
|
|
845
|
+
- Network info: ifconfig
|
|
846
|
+
- Process list: ps aux
|
|
847
|
+
${isMac ? '- Lock screen: pmset displaysleepnow' : '- Lock screen: gnome-screensaver-command -l'}
|
|
848
|
+
${isMac ? '- Shutdown: sudo shutdown -h now' : '- Shutdown: sudo shutdown -h now'}
|
|
849
|
+
${isMac ? '- Restart: sudo shutdown -r now' : '- Restart: sudo reboot'}
|
|
850
|
+
|
|
851
|
+
Opening Applications (ALWAYS prefer installed apps over websites):
|
|
852
|
+
${isMac ? `- Open Notion app: open -a Notion
|
|
853
|
+
- Open Spotify app: open -a Spotify
|
|
854
|
+
- Open Discord app: open -a Discord
|
|
855
|
+
- Open Slack app: open -a Slack
|
|
856
|
+
- Open VS Code: open -a "Visual Studio Code"
|
|
857
|
+
- Open Chrome: open -a "Google Chrome"
|
|
858
|
+
- Open Safari: open -a Safari
|
|
859
|
+
- Open Firefox: open -a Firefox
|
|
860
|
+
- Generic app: open -a "<AppName>"` : `- Text Editor: gedit (or nano, vim, kate)
|
|
861
|
+
- Open Notion: notion-app (if installed)
|
|
862
|
+
- Open Spotify: spotify (if installed)
|
|
863
|
+
- Open VS Code: code
|
|
864
|
+
- Open Chrome: google-chrome
|
|
865
|
+
- Open Firefox: firefox
|
|
866
|
+
- File Manager: nautilus or dolphin or thunar
|
|
867
|
+
- Terminal: gnome-terminal or konsole or xterm
|
|
868
|
+
- Calculator: gnome-calculator or kcalc
|
|
869
|
+
- Generic app: <appname> (just the command name, no paths)
|
|
870
|
+
|
|
871
|
+
IMPORTANT FOR LINUX:
|
|
872
|
+
- Notepad does NOT exist on Linux. Use: gedit, nano, vim, or kate instead
|
|
873
|
+
- Paint does NOT exist on Linux. Use: gimp, krita, or kolourpaint instead
|
|
874
|
+
- Calculator: use gnome-calculator or kcalc
|
|
875
|
+
- DO NOT use xdg-open with app names, just run the app command directly`}
|
|
876
|
+
|
|
877
|
+
IMPORTANT: When user says "open [app name]", ALWAYS try to open the installed application first. Only open websites if explicitly asked for "website" or "browser".
|
|
878
|
+
|
|
879
|
+
EXAMPLES:
|
|
880
|
+
Request: "open notion"
|
|
881
|
+
Response: {"steps": [{"command": "${isMac ? 'open -a Notion' : 'notion-app'}"}]}
|
|
882
|
+
|
|
883
|
+
Request: "open notepad" (Linux)
|
|
884
|
+
Response: {"steps": [{"command": "gedit"}]}
|
|
885
|
+
|
|
886
|
+
Request: "start text editor" (Linux)
|
|
887
|
+
Response: {"steps": [{"command": "gedit"}]}
|
|
888
|
+
|
|
889
|
+
Request: "open notion website"
|
|
890
|
+
Response: {"steps": [{"command": "${isMac ? 'open' : 'xdg-open'} https://notion.so"}]}
|
|
891
|
+
|
|
892
|
+
Request: "create hello.html on desktop"
|
|
893
|
+
Response: {"steps": [{"command": "echo '<!DOCTYPE html><html><head><title>Hello</title></head><body><h1>Hello World</h1></body></html>' > ${desktopPath}/hello.html"}]}
|
|
894
|
+
`;
|
|
895
|
+
}
|
|
896
|
+
|
|
426
897
|
const prompt = `
|
|
427
|
-
You are an
|
|
898
|
+
You are an expert ${isWindows ? 'Windows' : isMac ? 'macOS' : 'Linux'} command-line assistant. Generate precise, executable ${isWindows ? 'Windows' : isMac ? 'macOS' : 'Linux'} commands for the user's request.
|
|
899
|
+
|
|
900
|
+
PLATFORM: ${isWindows ? 'WINDOWS' : isMac ? 'macOS' : 'LINUX'} - You MUST use ${isWindows ? 'Windows' : isMac ? 'macOS' : 'Linux'} commands ONLY!
|
|
428
901
|
|
|
429
902
|
System Information:
|
|
430
903
|
- Home Directory: ${homeDir}
|
|
431
904
|
- Desktop Path: ${desktopPath}
|
|
432
|
-
- Platform: ${platform}
|
|
905
|
+
- Platform: ${platform} (${isWindows ? 'Windows' : isMac ? 'macOS' : 'Linux'})
|
|
433
906
|
|
|
434
907
|
CRITICAL RULES:
|
|
435
|
-
1. Return ONLY valid JSON
|
|
436
|
-
2. Use ABSOLUTE paths
|
|
437
|
-
3.
|
|
438
|
-
4.
|
|
439
|
-
5.
|
|
440
|
-
6.
|
|
908
|
+
1. Return ONLY valid JSON: {"steps": [{"command": "exact_command_here"}]}
|
|
909
|
+
2. Use ABSOLUTE paths appropriate for ${isWindows ? 'Windows (backslashes)' : 'Unix (forward slashes)'}
|
|
910
|
+
3. For file creation: use appropriate echo syntax for the platform
|
|
911
|
+
4. Break complex tasks into simple, sequential steps
|
|
912
|
+
5. Test each command mentally before including it
|
|
913
|
+
6. IMPORTANT: When user says "open [app name]", ALWAYS open the INSTALLED APPLICATION, NOT the website
|
|
914
|
+
7. Only open websites if user explicitly says "website", "browser", or provides a URL
|
|
915
|
+
8. ${isWindows ? 'USE WINDOWS COMMANDS ONLY! Use "start" command for opening apps!' : isMac ? 'USE macOS COMMANDS ONLY! Use "open -a" for apps!' : 'USE LINUX COMMANDS ONLY!'}
|
|
916
|
+
|
|
917
|
+
APPLICATION OPENING ${isWindows ? '(WINDOWS)' : isMac ? '(macOS)' : '(LINUX)'}:
|
|
918
|
+
${isWindows ? `- "open notion" → start notion://
|
|
919
|
+
- "open spotify" → start spotify:
|
|
920
|
+
- "open vscode" → code
|
|
921
|
+
- "open chrome" → start chrome
|
|
922
|
+
- ANY APP → start <appname> or start <appname>://` : isMac ? `- "open notion" → open -a Notion
|
|
923
|
+
- "open spotify" → open -a Spotify
|
|
924
|
+
- "open vscode" → open -a "Visual Studio Code"
|
|
925
|
+
- ANY APP → open -a "<AppName>"` : `- "open notion" → notion-app (if installed)
|
|
926
|
+
- "open spotify" → spotify
|
|
927
|
+
- "open vscode" → code
|
|
928
|
+
- ANY APP → <appname> or xdg-open <appname>`}
|
|
929
|
+
|
|
930
|
+
${platformInstructions}
|
|
441
931
|
|
|
442
|
-
|
|
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"}]}
|
|
932
|
+
User Request: ${userInput}
|
|
445
933
|
|
|
446
|
-
|
|
447
|
-
|
|
934
|
+
Return ONLY JSON with executable commands:
|
|
935
|
+
`;
|
|
448
936
|
|
|
449
|
-
-
|
|
450
|
-
|
|
937
|
+
// Priority: user-provided API key > server environment variable > Ollama fallback
|
|
938
|
+
const GROQ_API_KEY = userApiKey || process.env.GROQ_API_KEY;
|
|
939
|
+
|
|
940
|
+
if (GROQ_API_KEY) {
|
|
941
|
+
// Use Groq API (free cloud option)
|
|
942
|
+
console.log("Using Groq API for AI planning" + (userApiKey ? " (user-provided key)" : " (server key)"));
|
|
943
|
+
|
|
944
|
+
try {
|
|
945
|
+
const response = await axios.post(
|
|
946
|
+
"https://api.groq.com/openai/v1/chat/completions",
|
|
947
|
+
{
|
|
948
|
+
model: "llama-3.3-70b-versatile",
|
|
949
|
+
messages: [
|
|
950
|
+
{
|
|
951
|
+
role: "user",
|
|
952
|
+
content: prompt
|
|
953
|
+
}
|
|
954
|
+
],
|
|
955
|
+
temperature: 0
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
headers: {
|
|
959
|
+
"Authorization": `Bearer ${GROQ_API_KEY} `,
|
|
960
|
+
"Content-Type": "application/json"
|
|
961
|
+
},
|
|
962
|
+
timeout: 30000
|
|
963
|
+
}
|
|
964
|
+
);
|
|
451
965
|
|
|
452
|
-
|
|
966
|
+
const text = response.data.choices[0].message.content.trim();
|
|
967
|
+
console.log("Groq AI output:", text);
|
|
453
968
|
|
|
454
|
-
|
|
455
|
-
|
|
969
|
+
// Extract JSON safely
|
|
970
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
971
|
+
|
|
972
|
+
if (!jsonMatch) {
|
|
973
|
+
throw new Error("No valid JSON found in AI output");
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return JSON.parse(jsonMatch[0]);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
console.error("Groq API error:", error.message);
|
|
979
|
+
|
|
980
|
+
if (error.response?.status === 401) {
|
|
981
|
+
throw new Error("Invalid API key. Please check your Groq API key in Settings.");
|
|
982
|
+
}
|
|
456
983
|
|
|
457
|
-
|
|
458
|
-
"http://localhost:11434/api/generate",
|
|
459
|
-
{
|
|
460
|
-
model: "qwen2.5:7b",
|
|
461
|
-
prompt: prompt,
|
|
462
|
-
stream: false
|
|
984
|
+
throw new Error("AI planning failed. Please check your Groq API key or use direct commands.");
|
|
463
985
|
}
|
|
464
|
-
|
|
986
|
+
} else {
|
|
987
|
+
// Fallback to Ollama (local only)
|
|
988
|
+
console.log("Using Ollama for AI planning (local only)");
|
|
989
|
+
const ollamaUrl = process.env.OLLAMA_URL || "http://localhost:11434";
|
|
990
|
+
|
|
991
|
+
try {
|
|
992
|
+
const response = await axios.post(
|
|
993
|
+
`${ollamaUrl} /api/generate`,
|
|
994
|
+
{
|
|
995
|
+
model: "qwen2.5:7b",
|
|
996
|
+
prompt: prompt,
|
|
997
|
+
stream: false
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
timeout: 10000
|
|
1001
|
+
}
|
|
1002
|
+
);
|
|
465
1003
|
|
|
466
|
-
|
|
1004
|
+
const text = response.data.response.trim();
|
|
1005
|
+
console.log("Ollama output:", text);
|
|
467
1006
|
|
|
468
|
-
|
|
1007
|
+
// Extract JSON safely
|
|
1008
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
469
1009
|
|
|
470
|
-
|
|
471
|
-
|
|
1010
|
+
if (!jsonMatch) {
|
|
1011
|
+
throw new Error("No valid JSON found in LLM output");
|
|
1012
|
+
}
|
|
472
1013
|
|
|
473
|
-
|
|
474
|
-
|
|
1014
|
+
return JSON.parse(jsonMatch[0]);
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
console.error("Ollama connection error:", error.message);
|
|
1017
|
+
throw new Error("AI planning is not available. Ollama is not running or not accessible. Please use direct commands instead.");
|
|
1018
|
+
}
|
|
475
1019
|
}
|
|
476
|
-
|
|
477
|
-
return JSON.parse(jsonMatch[0]);
|
|
478
1020
|
}
|
|
479
1021
|
|
|
480
1022
|
|
|
481
1023
|
|
|
482
|
-
|
|
1024
|
+
// Helper to broadcast device list to a specific dashboard
|
|
1025
|
+
function broadcastDevicesToDashboard(dashboardWs) {
|
|
483
1026
|
const deviceList = [];
|
|
484
1027
|
|
|
485
|
-
devices.forEach((device) => {
|
|
1028
|
+
devices.forEach((device, deviceId) => {
|
|
1029
|
+
const isAuthenticated = dashboardWs.authenticatedDevices.has(deviceId);
|
|
1030
|
+
console.log(`Device ${deviceId}: authenticated = ${isAuthenticated} `);
|
|
1031
|
+
|
|
486
1032
|
deviceList.push({
|
|
487
1033
|
deviceName: device.deviceName,
|
|
488
1034
|
status: device.status,
|
|
1035
|
+
authenticated: isAuthenticated
|
|
489
1036
|
});
|
|
490
1037
|
});
|
|
491
1038
|
|
|
1039
|
+
console.log('Broadcasting devices to dashboard:', deviceList);
|
|
1040
|
+
|
|
1041
|
+
dashboardWs.send(JSON.stringify({
|
|
1042
|
+
type: "DEVICES",
|
|
1043
|
+
devices: deviceList,
|
|
1044
|
+
}));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function broadcastDevices() {
|
|
492
1048
|
dashboardClients.forEach((client) => {
|
|
493
|
-
client
|
|
494
|
-
JSON.stringify({
|
|
495
|
-
type: "DEVICES",
|
|
496
|
-
devices: deviceList,
|
|
497
|
-
})
|
|
498
|
-
);
|
|
1049
|
+
broadcastDevicesToDashboard(client);
|
|
499
1050
|
});
|
|
500
1051
|
}
|