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/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
- // pendingResults holds awaiting resolvers for EXECUTE_RESULT from agents
13
- const pendingResults = new Map(); // deviceName -> [{ command, resolve, reject, timer }]
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
- const wss = new WebSocket.Server({ port: PORT });
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
- console.log(`Backend running on port ${PORT}`);
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 targetDevice = [...devices.values()].find(
56
- (d) =>
57
- d.deviceName === data.deviceName &&
58
- d.status === "online"
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
- if (!targetDevice) return;
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 targetDevice = [...devices.values()].find(
110
- (d) =>
111
- d.deviceName === data.deviceName &&
112
- d.status === "online"
113
- );
139
+ const deviceId = `device-${data.deviceName.toLowerCase()}`;
114
140
 
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
- });
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
- console.log("APPROVE_PLAN received:", data.steps);
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 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
- });
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: wait for agent to report EXECUTE_RESULT for each step
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
- 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
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}: ${commandToExecute}`);
188
+ console.log(`Step exited with code ${result.code}`);
175
189
  }
176
190
  } 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
- });
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
- dashboardClients.forEach((client) => {
203
- client.send(
204
- JSON.stringify({
205
- type: "EXECUTION_FINISHED",
206
- deviceName: data.deviceName,
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
- 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");
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 || "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
- })
235
+ targetDevice.platform && targetDevice.platform !== "unknown" ? targetDevice.platform : "win32",
236
+ userApiKey
242
237
  );
243
238
 
244
- console.log("AI Plan sent for approval:", plan);
239
+ ws.send(JSON.stringify({
240
+ type: "PLAN_PREVIEW",
241
+ deviceName: data.deviceName,
242
+ steps: plan.steps
243
+ }));
245
244
  } 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
- });
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 || !devices.has(token)) {
273
- console.log("Unauthorized connection attempt");
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
- const device = devices.get(token);
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(`Authenticated device: ${device.deviceName}`);
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
- console.log(`Heartbeat received from ${device.deviceName}`);
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
- console.log(`[${device.deviceName}] ${data.message}`);
298
-
299
- // 🔥 send logs to dashboard
592
+ // Send logs only to authenticated dashboards
300
593
  dashboardClients.forEach((client) => {
301
- client.send(
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(); // 🔥 notify dashboard
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 AI OS command planner. Generate EXACT Windows commands for the user's request.
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 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
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
- 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"}]}
932
+ User Request: ${userInput}
445
933
 
446
- - "create test file with hello world"
447
- Response: {"steps": [{"command": "echo hello world > ${homeDir}\\Desktop\\test.txt"}]}
934
+ Return ONLY JSON with executable commands:
935
+ `;
448
936
 
449
- - "list desktop files"
450
- Response: {"steps": [{"command": "dir ${desktopPath}"}]}
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
- User Request: ${userInput}
966
+ const text = response.data.choices[0].message.content.trim();
967
+ console.log("Groq AI output:", text);
453
968
 
454
- Return ONLY JSON:
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
- const response = await axios.post(
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
- const text = response.data.response.trim();
1004
+ const text = response.data.response.trim();
1005
+ console.log("Ollama output:", text);
467
1006
 
468
- console.log("LLM raw output:", text);
1007
+ // Extract JSON safely
1008
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
469
1009
 
470
- // Extract JSON safely
471
- const jsonMatch = text.match(/\{[\s\S]*\}/);
1010
+ if (!jsonMatch) {
1011
+ throw new Error("No valid JSON found in LLM output");
1012
+ }
472
1013
 
473
- if (!jsonMatch) {
474
- throw new Error("No valid JSON found in LLM output");
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
- function broadcastDevices() {
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.send(
494
- JSON.stringify({
495
- type: "DEVICES",
496
- devices: deviceList,
497
- })
498
- );
1049
+ broadcastDevicesToDashboard(client);
499
1050
  });
500
1051
  }