vortix 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agent/agent.js CHANGED
@@ -4,6 +4,7 @@ const os = require("os");
4
4
  const readline = require("readline");
5
5
  const fs = require("fs");
6
6
  const path = require("path");
7
+ const screenshot = require("screenshot-desktop");
7
8
 
8
9
  process.on("uncaughtException", (err) => {
9
10
  console.error("Uncaught Exception:", err);
@@ -15,6 +16,12 @@ process.on("unhandledRejection", (err) => {
15
16
 
16
17
  const CONFIG_FILE = path.join(os.homedir(), '.vortix-config.json');
17
18
 
19
+ // Detect platform
20
+ const PLATFORM = os.platform(); // 'win32', 'darwin', 'linux'
21
+ const IS_WINDOWS = PLATFORM === 'win32';
22
+ const IS_MAC = PLATFORM === 'darwin';
23
+ const IS_LINUX = PLATFORM === 'linux';
24
+
18
25
  // Load or create config
19
26
  function loadConfig() {
20
27
  try {
@@ -75,9 +82,27 @@ if (command === "start") {
75
82
  return;
76
83
  }
77
84
 
85
+ if (command === "enable-autostart") {
86
+ enableAutoStart();
87
+ return;
88
+ }
89
+
90
+ if (command === "disable-autostart") {
91
+ disableAutoStart();
92
+ return;
93
+ }
94
+
95
+ if (command === "status") {
96
+ checkAutoStartStatus();
97
+ return;
98
+ }
99
+
78
100
  console.log("Available commands:");
79
- console.log(" vortix login - Set device password");
80
- console.log(" vortix start - Start the agent");
101
+ console.log(" vortix login - Set device password");
102
+ console.log(" vortix start - Start the agent");
103
+ console.log(" vortix enable-autostart - Enable auto-start on system boot");
104
+ console.log(" vortix disable-autostart - Disable auto-start");
105
+ console.log(" vortix status - Check auto-start status");
81
106
 
82
107
  function startAgent() {
83
108
  const config = loadConfig();
@@ -92,179 +117,840 @@ function startAgent() {
92
117
  const token = `${deviceToken}:${config.password}`;
93
118
 
94
119
  console.log(`Device: ${deviceName}`);
120
+ console.log(`Platform: ${PLATFORM} (${IS_WINDOWS ? 'Windows' : IS_MAC ? 'macOS' : IS_LINUX ? 'Linux' : 'Unknown'})`);
95
121
  console.log("Connecting to backend...");
96
122
 
97
- // Production backend URL - Render deployment
123
+ // Backend URL - supports both local and production
124
+ // For production: set BACKEND_URL environment variable
125
+ // For local: defaults to ws://localhost:8080
98
126
  const BACKEND_URL = process.env.BACKEND_URL || 'wss://vortix.onrender.com';
127
+ console.log(`Backend URL: ${BACKEND_URL}`);
99
128
 
100
- const ws = new WebSocket(`${BACKEND_URL}?token=${encodeURIComponent(token)}`);
129
+ let ws = null;
130
+ let screenCaptureInterval = null;
131
+ let reconnectTimeout = null;
132
+ let isIntentionalClose = false;
101
133
 
102
- ws.on("open", () => {
103
- console.log("Authenticated and connected to backend");
134
+ function connect() {
135
+ console.log(`Attempting connection to: ${BACKEND_URL}`);
136
+ ws = new WebSocket(`${BACKEND_URL}?token=${encodeURIComponent(token)}`);
104
137
 
105
- setInterval(() => {
106
- if (ws.readyState === WebSocket.OPEN) {
107
- console.log("Sending heartbeat...");
108
- ws.send(
109
- JSON.stringify({
110
- type: "HEARTBEAT"
111
- })
112
- );
113
- }
114
- }, 5000);
115
- });
138
+ ws.on("open", () => {
139
+ console.log("✅ Authenticated and connected to backend successfully!");
140
+ console.log(`Connected to: ${BACKEND_URL}`);
116
141
 
117
- ws.on("close", () => {
118
- console.log("Disconnected from backend");
119
- });
142
+ // Clear any reconnect timeout
143
+ if (reconnectTimeout) {
144
+ clearTimeout(reconnectTimeout);
145
+ reconnectTimeout = null;
146
+ }
120
147
 
121
- ws.on("error", (err) => {
122
- console.error("WebSocket error:", err.message);
123
- });
148
+ // Send platform info immediately on connection
149
+ ws.send(
150
+ JSON.stringify({
151
+ type: "HEARTBEAT",
152
+ platform: PLATFORM
153
+ })
154
+ );
155
+
156
+ // Then send heartbeat every 5 seconds
157
+ setInterval(() => {
158
+ if (ws.readyState === WebSocket.OPEN) {
159
+ console.log("Sending heartbeat...");
160
+ ws.send(
161
+ JSON.stringify({
162
+ type: "HEARTBEAT",
163
+ platform: PLATFORM
164
+ })
165
+ );
166
+ }
167
+ }, 5000);
168
+ });
124
169
 
125
- let commandQueue = [];
126
- let isRunning = false;
127
- let currentCwd = require("os").homedir(); // Track current working directory
170
+ ws.on("close", (code, reason) => {
171
+ console.log("Disconnected from backend");
172
+ console.log(`Close code: ${code}, Reason: ${reason || 'No reason provided'}`);
128
173
 
129
- ws.on("message", (message) => {
130
- try {
131
- const data = JSON.parse(message.toString());
174
+ // Clean up screen capture
175
+ if (screenCaptureInterval) {
176
+ clearInterval(screenCaptureInterval);
177
+ screenCaptureInterval = null;
178
+ }
132
179
 
133
- if (data.type === "EXECUTE") {
134
- console.log("Agent received EXECUTE:", data.command);
135
- commandQueue.push(data.command);
136
- processQueue();
180
+ // Auto-reconnect unless it was intentional
181
+ if (!isIntentionalClose) {
182
+ console.log("Attempting to reconnect in 5 seconds...");
183
+ reconnectTimeout = setTimeout(() => {
184
+ console.log("Reconnecting...");
185
+ connect();
186
+ }, 5000);
137
187
  }
188
+ });
138
189
 
139
- } catch (err) {
140
- console.error("Invalid message received:", message.toString());
141
- }
142
- });
190
+ ws.on("error", (err) => {
191
+ console.error("=".repeat(50));
192
+ console.error("❌ WebSocket Connection Error!");
193
+ console.error("Error:", err.message);
194
+ console.error("Trying to connect to:", BACKEND_URL);
195
+ console.error("=".repeat(50));
196
+ console.error("Troubleshooting:");
197
+ console.error("1. Make sure backend is running: cd backend && npm start");
198
+ console.error("2. Backend should show: 'Backend running on port 8080'");
199
+ console.error("3. Check if port 8080 is open: netstat -an | findstr :8080");
200
+ console.error("4. Verify BACKEND_URL environment variable (if set)");
201
+ console.error("=".repeat(50));
202
+ });
203
+
204
+ let commandQueue = [];
205
+ let isRunning = false;
206
+ let currentCwd = require("os").homedir(); // Track current working directory
207
+
208
+ ws.on("message", (message) => {
209
+ try {
210
+ const data = JSON.parse(message.toString());
211
+
212
+ if (data.type === "EXECUTE") {
213
+ console.log("Agent received EXECUTE:", data.command);
214
+ commandQueue.push(data.command);
215
+ processQueue();
216
+ }
217
+
218
+ if (data.type === "START_SCREEN_CAPTURE") {
219
+ console.log("🎥 Starting screen capture...");
220
+ if (!screenCaptureInterval) {
221
+ screenCaptureInterval = setInterval(async () => {
222
+ try {
223
+ console.log("📸 Capturing frame...");
224
+ const img = await screenshot({ format: 'jpg' });
225
+ const base64 = img.toString('base64');
226
+ console.log(`✅ Frame captured: ${img.length} bytes, base64: ${base64.length} chars`);
227
+
228
+ if (ws.readyState === WebSocket.OPEN) {
229
+ ws.send(JSON.stringify({
230
+ type: "SCREEN_FRAME",
231
+ frame: base64
232
+ }));
233
+ console.log("📤 Frame sent to backend");
234
+ } else {
235
+ console.log("⚠️ WebSocket not open, skipping frame");
236
+ }
237
+ } catch (err) {
238
+ console.error("❌ Screen capture error:", err.message);
239
+ console.error(err.stack);
240
+ }
241
+ }, 1000); // Capture every 1 second
242
+ console.log("✅ Screen capture interval started");
243
+ } else {
244
+ console.log("⚠️ Screen capture already running");
245
+ }
246
+ }
247
+
248
+ if (data.type === "STOP_SCREEN_CAPTURE") {
249
+ console.log("Stopping screen capture...");
250
+ if (screenCaptureInterval) {
251
+ clearInterval(screenCaptureInterval);
252
+ screenCaptureInterval = null;
253
+ }
254
+ }
255
+
256
+ if (data.type === "ENABLE_AUTOSTART") {
257
+ console.log("Received request to enable auto-start...");
258
+ try {
259
+ enableAutoStart();
260
+ ws.send(JSON.stringify({
261
+ type: "AUTOSTART_STATUS",
262
+ enabled: true,
263
+ message: "Auto-start enabled successfully"
264
+ }));
265
+ } catch (err) {
266
+ ws.send(JSON.stringify({
267
+ type: "AUTOSTART_ERROR",
268
+ message: err.message
269
+ }));
270
+ }
271
+ }
272
+
273
+ if (data.type === "DISABLE_AUTOSTART") {
274
+ console.log("Received request to disable auto-start...");
275
+ try {
276
+ disableAutoStart();
277
+ ws.send(JSON.stringify({
278
+ type: "AUTOSTART_STATUS",
279
+ enabled: false,
280
+ message: "Auto-start disabled successfully"
281
+ }));
282
+ } catch (err) {
283
+ ws.send(JSON.stringify({
284
+ type: "AUTOSTART_ERROR",
285
+ message: err.message
286
+ }));
287
+ }
288
+ }
289
+
290
+ if (data.type === "GET_AUTOSTART_STATUS") {
291
+ const config = loadConfig();
292
+ ws.send(JSON.stringify({
293
+ type: "AUTOSTART_STATUS",
294
+ enabled: config.autoStart || false
295
+ }));
296
+ }
143
297
 
144
- function extractDirectoryFromCommand(command) {
145
- // Check for cd commands and extract the target directory
146
- // Handle: cd /d D:, cd /D D:\, cd D:\folder, etc.
147
-
148
- // Match: cd /d D: or cd /D D: or similar
149
- let match = command.match(/^\s*cd\s+\/[dD]\s+([^\s]+)\s*$/i);
150
- if (match) {
151
- let path = match[1].trim();
152
- path = expandPath(path);
153
- // Ensure it ends with backslash if it's just a drive letter
154
- if (/^[A-Za-z]:$/.test(path)) {
155
- path = path + "\\";
298
+ // System stats
299
+ if (data.type === "GET_SYSTEM_STATS") {
300
+ const os = require('os');
301
+ const { exec } = require('child_process');
302
+
303
+ // Get accurate CPU usage (measure over 500ms for faster response)
304
+ const getCPUUsage = () => {
305
+ return new Promise((resolve) => {
306
+ if (IS_WINDOWS) {
307
+ // Windows: Use wmic for more accurate CPU reading
308
+ exec('wmic cpu get loadpercentage', (err, stdout) => {
309
+ if (err) {
310
+ console.error("Error getting CPU usage:", err);
311
+ // Fallback to Node.js method
312
+ fallbackCPUMeasure(resolve);
313
+ return;
314
+ }
315
+
316
+ const lines = stdout.trim().split('\n');
317
+ if (lines.length >= 2) {
318
+ const cpuLoad = parseInt(lines[1].trim());
319
+ if (!isNaN(cpuLoad)) {
320
+ resolve(cpuLoad);
321
+ return;
322
+ }
323
+ }
324
+ fallbackCPUMeasure(resolve);
325
+ });
326
+ } else {
327
+ // Mac/Linux: Use Node.js method
328
+ fallbackCPUMeasure(resolve);
329
+ }
330
+ });
331
+ };
332
+
333
+ const fallbackCPUMeasure = (resolve) => {
334
+ const startMeasure = os.cpus();
335
+
336
+ setTimeout(() => {
337
+ const endMeasure = os.cpus();
338
+
339
+ let totalIdle = 0;
340
+ let totalTick = 0;
341
+
342
+ for (let i = 0; i < startMeasure.length; i++) {
343
+ const start = startMeasure[i].times;
344
+ const end = endMeasure[i].times;
345
+
346
+ const idleDiff = end.idle - start.idle;
347
+ const totalDiff =
348
+ (end.user - start.user) +
349
+ (end.nice - start.nice) +
350
+ (end.sys - start.sys) +
351
+ (end.idle - start.idle) +
352
+ (end.irq - start.irq);
353
+
354
+ totalIdle += idleDiff;
355
+ totalTick += totalDiff;
356
+ }
357
+
358
+ const cpuUsage = 100 - (100 * totalIdle / totalTick);
359
+ resolve(Math.round(cpuUsage));
360
+ }, 500);
361
+ };
362
+
363
+ // Get accurate disk usage
364
+ const getDiskUsage = () => {
365
+ return new Promise((resolve) => {
366
+ if (IS_WINDOWS) {
367
+ // Windows: Get C: drive usage
368
+ exec('wmic logicaldisk where "DeviceID=\'C:\'" get Size,FreeSpace', (err, stdout) => {
369
+ if (err) {
370
+ console.error("Error getting disk usage:", err);
371
+ resolve(0);
372
+ return;
373
+ }
374
+
375
+ const lines = stdout.trim().split('\n');
376
+ if (lines.length >= 2) {
377
+ const values = lines[1].trim().split(/\s+/);
378
+ if (values.length >= 2) {
379
+ const freeSpace = parseInt(values[0]);
380
+ const totalSpace = parseInt(values[1]);
381
+ const usedSpace = totalSpace - freeSpace;
382
+ const diskUsage = (usedSpace / totalSpace) * 100;
383
+ resolve(Math.round(diskUsage));
384
+ return;
385
+ }
386
+ }
387
+ resolve(0);
388
+ });
389
+ } else if (IS_MAC || IS_LINUX) {
390
+ // Mac/Linux: Get root partition usage
391
+ exec('df -h / | tail -1', (err, stdout) => {
392
+ if (err) {
393
+ console.error("Error getting disk usage:", err);
394
+ resolve(0);
395
+ return;
396
+ }
397
+
398
+ const parts = stdout.trim().split(/\s+/);
399
+ if (parts.length >= 5) {
400
+ const usagePercent = parts[4].replace('%', '');
401
+ resolve(parseInt(usagePercent));
402
+ return;
403
+ }
404
+ resolve(0);
405
+ });
406
+ } else {
407
+ resolve(0);
408
+ }
409
+ });
410
+ };
411
+
412
+ // Get all stats
413
+ Promise.all([
414
+ getCPUUsage(),
415
+ getDiskUsage()
416
+ ]).then(([cpuUsage, diskUsage]) => {
417
+ const totalMem = os.totalmem();
418
+ const freeMem = os.freemem();
419
+ const memoryUsage = ((totalMem - freeMem) / totalMem) * 100;
420
+
421
+ const statsData = {
422
+ cpu: cpuUsage,
423
+ memory: Math.round(memoryUsage),
424
+ disk: diskUsage
425
+ };
426
+
427
+ ws.send(JSON.stringify({
428
+ type: "SYSTEM_STATS",
429
+ stats: statsData
430
+ }));
431
+ }).catch(err => {
432
+ console.error("Error collecting stats:", err);
433
+ // Send fallback data
434
+ const totalMem = os.totalmem();
435
+ const freeMem = os.freemem();
436
+ const memoryUsage = ((totalMem - freeMem) / totalMem) * 100;
437
+
438
+ ws.send(JSON.stringify({
439
+ type: "SYSTEM_STATS",
440
+ stats: {
441
+ cpu: 0,
442
+ memory: Math.round(memoryUsage),
443
+ disk: 0
444
+ }
445
+ }));
446
+ });
447
+ }
448
+
449
+ // File browsing
450
+ if (data.type === "BROWSE_FILES") {
451
+ const fs = require('fs');
452
+ const path = require('path');
453
+
454
+ let targetPath = data.path || os.homedir();
455
+
456
+ // Handle special paths
457
+ if (targetPath === "Desktop") {
458
+ targetPath = path.join(os.homedir(), "Desktop");
459
+ } else if (targetPath === "Downloads") {
460
+ targetPath = path.join(os.homedir(), "Downloads");
461
+ } else if (targetPath === "Documents") {
462
+ targetPath = path.join(os.homedir(), "Documents");
463
+ }
464
+
465
+ try {
466
+ const items = fs.readdirSync(targetPath);
467
+ const files = items.map(item => {
468
+ const fullPath = path.join(targetPath, item);
469
+ const stats = fs.statSync(fullPath);
470
+ return {
471
+ name: item,
472
+ type: stats.isDirectory() ? "directory" : "file",
473
+ size: stats.isFile() ? stats.size : undefined,
474
+ path: fullPath
475
+ };
476
+ });
477
+
478
+ console.log(`Agent: Sending FILE_LIST for ${targetPath} with ${files.length} items`);
479
+ ws.send(JSON.stringify({
480
+ type: "FILE_LIST",
481
+ files,
482
+ path: targetPath
483
+ }));
484
+ } catch (err) {
485
+ console.error("Error browsing files:", err.message);
486
+ ws.send(JSON.stringify({
487
+ type: "FILE_LIST",
488
+ files: [],
489
+ path: targetPath,
490
+ error: err.message
491
+ }));
492
+ }
493
+ }
494
+
495
+ // File upload
496
+ if (data.type === "UPLOAD_FILE") {
497
+ const fs = require('fs');
498
+ const path = require('path');
499
+
500
+ let targetPath = data.targetPath || os.homedir();
501
+ if (targetPath === "Desktop") {
502
+ targetPath = path.join(os.homedir(), "Desktop");
503
+ } else if (targetPath === "Downloads") {
504
+ targetPath = path.join(os.homedir(), "Downloads");
505
+ } else if (targetPath === "Documents") {
506
+ targetPath = path.join(os.homedir(), "Documents");
507
+ }
508
+
509
+ const filePath = path.join(targetPath, data.fileName);
510
+ const fileBuffer = Buffer.from(data.fileData, 'base64');
511
+
512
+ try {
513
+ fs.writeFileSync(filePath, fileBuffer);
514
+ console.log(`Agent: File uploaded successfully: ${filePath}`);
515
+ ws.send(JSON.stringify({
516
+ type: "LOG",
517
+ deviceName,
518
+ message: `✓ File uploaded: ${data.fileName} to ${targetPath}`
519
+ }));
520
+ } catch (err) {
521
+ console.error("Agent: Error uploading file:", err.message);
522
+ ws.send(JSON.stringify({
523
+ type: "LOG",
524
+ deviceName,
525
+ message: `✗ Upload failed: ${err.message}`
526
+ }));
527
+ }
528
+ }
529
+
530
+ // File download
531
+ if (data.type === "DOWNLOAD_FILE") {
532
+ const fs = require('fs');
533
+
534
+ try {
535
+ const fileBuffer = fs.readFileSync(data.filePath);
536
+ const base64 = fileBuffer.toString('base64');
537
+ const fileName = require('path').basename(data.filePath);
538
+
539
+ ws.send(JSON.stringify({
540
+ type: "FILE_DATA",
541
+ fileName,
542
+ fileData: base64
543
+ }));
544
+
545
+ console.log(`File downloaded: ${data.filePath}`);
546
+ } catch (err) {
547
+ console.error("Error downloading file:", err.message);
548
+ ws.send(JSON.stringify({
549
+ type: "LOG",
550
+ deviceName,
551
+ message: `✗ Download failed: ${err.message}`
552
+ }));
553
+ }
554
+ }
555
+
556
+ } catch (err) {
557
+ console.error("Invalid message received:", message.toString());
156
558
  }
157
- console.log("CD detected, new path:", path);
158
- return path;
159
- }
559
+ });
560
+
561
+ function extractDirectoryFromCommand(command) {
562
+ // Check for cd commands and extract the target directory
563
+ // Handle: cd /d D:, cd /D D:\, cd D:\folder, etc.
160
564
 
161
- // Match: cd D:\folder or cd folder
162
- match = command.match(/^\s*cd\s+([^\s][^\s]*)\s*$/i);
163
- if (match) {
164
- let path = match[1].trim();
165
- path = expandPath(path);
166
- // Only treat as cd if it looks like a path (has : or starts with \)
167
- if (/[:\\]/.test(path)) {
565
+ // Match: cd /d D: or cd /D D: or similar
566
+ let match = command.match(/^\s*cd\s+\/[dD]\s+([^\s]+)\s*$/i);
567
+ if (match) {
568
+ let path = match[1].trim();
569
+ path = expandPath(path);
570
+ // Ensure it ends with backslash if it's just a drive letter
168
571
  if (/^[A-Za-z]:$/.test(path)) {
169
572
  path = path + "\\";
170
573
  }
171
574
  console.log("CD detected, new path:", path);
172
575
  return path;
173
576
  }
577
+
578
+ // Match: cd D:\folder or cd folder
579
+ match = command.match(/^\s*cd\s+([^\s][^\s]*)\s*$/i);
580
+ if (match) {
581
+ let path = match[1].trim();
582
+ path = expandPath(path);
583
+ // Only treat as cd if it looks like a path (has : or starts with \)
584
+ if (/[:\\]/.test(path)) {
585
+ if (/^[A-Za-z]:$/.test(path)) {
586
+ path = path + "\\";
587
+ }
588
+ console.log("CD detected, new path:", path);
589
+ return path;
590
+ }
591
+ }
592
+
593
+ return null;
594
+ }
595
+
596
+ function expandPath(path) {
597
+ // Expand environment variables
598
+ if (path.includes("%USERPROFILE%")) {
599
+ path = path.replace(/%USERPROFILE%/gi, os.homedir());
600
+ }
601
+ if (path.includes("%HOMEPATH%")) {
602
+ path = path.replace(/%HOMEPATH%/gi, os.homedir());
603
+ }
604
+ if (path.includes("%HOMEDRIVE%")) {
605
+ const homedir = os.homedir();
606
+ const drive = homedir.split("\\")[0];
607
+ path = path.replace(/%HOMEDRIVE%/gi, drive);
608
+ }
609
+ return path;
174
610
  }
175
611
 
176
- return null;
612
+ function processQueue() {
613
+ if (isRunning || commandQueue.length === 0) return;
614
+
615
+ isRunning = true;
616
+ let command = commandQueue.shift();
617
+
618
+ console.log("Executing command:", command);
619
+
620
+ // Update current working directory if this is a cd command
621
+ const newCwd = extractDirectoryFromCommand(command);
622
+ if (newCwd) {
623
+ currentCwd = newCwd;
624
+ ws.send(JSON.stringify({ type: "LOG", deviceName, message: `Directory changed to: ${currentCwd}` }));
625
+ console.log("Directory state updated to:", currentCwd);
626
+ }
627
+
628
+ // report current cwd for diagnostics
629
+ ws.send(JSON.stringify({ type: "LOG", deviceName, message: `Working from: ${currentCwd}` }));
630
+ ws.send(JSON.stringify({ type: "LOG", deviceName, message: `Executing: ${command}` }));
631
+
632
+ // Platform-specific shell
633
+ const shell = IS_WINDOWS ? "cmd.exe" : IS_MAC || IS_LINUX ? "/bin/bash" : "sh";
634
+ const shellFlag = IS_WINDOWS ? "/c" : "-c";
635
+
636
+ // Use platform-appropriate shell
637
+ const process = exec(command, {
638
+ cwd: currentCwd,
639
+ shell: shell,
640
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large outputs
641
+ encoding: "utf8"
642
+ });
643
+
644
+ // Track if we've received any output
645
+ let hasOutput = false;
646
+
647
+ process.stdout.on("data", (data) => {
648
+ hasOutput = true;
649
+ ws.send(JSON.stringify({
650
+ type: "LOG",
651
+ deviceName,
652
+ message: data.toString()
653
+ }));
654
+ });
655
+
656
+ process.stderr.on("data", (data) => {
657
+ hasOutput = true;
658
+ ws.send(JSON.stringify({
659
+ type: "LOG",
660
+ deviceName,
661
+ message: `[ERROR] ${data.toString()}`
662
+ }));
663
+ });
664
+
665
+ process.on("error", (err) => {
666
+ ws.send(JSON.stringify({
667
+ type: "LOG",
668
+ deviceName,
669
+ message: `[EXEC ERROR] ${err.message}`
670
+ }));
671
+ });
672
+
673
+ process.on("close", (code) => {
674
+ ws.send(JSON.stringify({
675
+ type: "LOG",
676
+ deviceName,
677
+ message: `Command execution finished with code: ${code}`
678
+ }));
679
+
680
+ // Send structured result so server can orchestrate sequential steps
681
+ ws.send(JSON.stringify({
682
+ type: "EXECUTE_RESULT",
683
+ command,
684
+ code: typeof code === 'number' ? code : 0
685
+ }));
686
+
687
+ isRunning = false;
688
+ processQueue();
689
+ });
690
+ }
177
691
  }
178
692
 
179
- function expandPath(path) {
180
- // Expand environment variables
181
- if (path.includes("%USERPROFILE%")) {
182
- path = path.replace(/%USERPROFILE%/gi, os.homedir());
693
+ // Start the connection
694
+ connect();
695
+
696
+ // Handle Ctrl+C gracefully
697
+ process.on('SIGINT', () => {
698
+ console.log('\n\nShutting down gracefully...');
699
+ isIntentionalClose = true;
700
+
701
+ if (screenCaptureInterval) {
702
+ clearInterval(screenCaptureInterval);
183
703
  }
184
- if (path.includes("%HOMEPATH%")) {
185
- path = path.replace(/%HOMEPATH%/gi, os.homedir());
704
+
705
+ if (reconnectTimeout) {
706
+ clearTimeout(reconnectTimeout);
707
+ }
708
+
709
+ if (ws && ws.readyState === WebSocket.OPEN) {
710
+ ws.close();
711
+ }
712
+
713
+ console.log('Agent stopped.');
714
+ process.exit(0);
715
+ });
716
+ }
717
+
718
+
719
+ // ========== AUTO-START FUNCTIONALITY ==========
720
+
721
+ function enableAutoStart() {
722
+ console.log("Enabling auto-start on system boot...");
723
+
724
+ const config = loadConfig();
725
+ if (!config.password) {
726
+ console.log("❌ Error: Please set a password first using 'vortix login'");
727
+ return;
728
+ }
729
+
730
+ try {
731
+ if (IS_WINDOWS) {
732
+ enableAutoStartWindows();
733
+ } else if (IS_MAC) {
734
+ enableAutoStartMac();
735
+ } else if (IS_LINUX) {
736
+ enableAutoStartLinux();
737
+ } else {
738
+ console.log("❌ Unsupported platform");
739
+ return;
186
740
  }
187
- if (path.includes("%HOMEDRIVE%")) {
188
- const homedir = os.homedir();
189
- const drive = homedir.split("\\")[0];
190
- path = path.replace(/%HOMEDRIVE%/gi, drive);
741
+
742
+ config.autoStart = true;
743
+ saveConfig(config);
744
+ console.log("✅ Auto-start enabled successfully!");
745
+ console.log(" The agent will start automatically on system boot.");
746
+ } catch (err) {
747
+ console.error("❌ Failed to enable auto-start:", err.message);
748
+ }
749
+ }
750
+
751
+ function disableAutoStart() {
752
+ console.log("Disabling auto-start...");
753
+
754
+ try {
755
+ if (IS_WINDOWS) {
756
+ disableAutoStartWindows();
757
+ } else if (IS_MAC) {
758
+ disableAutoStartMac();
759
+ } else if (IS_LINUX) {
760
+ disableAutoStartLinux();
761
+ } else {
762
+ console.log("❌ Unsupported platform");
763
+ return;
191
764
  }
192
- return path;
765
+
766
+ const config = loadConfig();
767
+ config.autoStart = false;
768
+ saveConfig(config);
769
+ console.log("✅ Auto-start disabled successfully!");
770
+ } catch (err) {
771
+ console.error("❌ Failed to disable auto-start:", err.message);
193
772
  }
773
+ }
774
+
775
+ function checkAutoStartStatus() {
776
+ const config = loadConfig();
777
+ console.log("\n=== Vortix Agent Status ===");
778
+ console.log(`Platform: ${PLATFORM} (${IS_WINDOWS ? 'Windows' : IS_MAC ? 'macOS' : IS_LINUX ? 'Linux' : 'Unknown'})`);
779
+ console.log(`Device: ${os.hostname()}`);
780
+ console.log(`Password Set: ${config.password ? '✅ Yes' : '❌ No'}`);
781
+ console.log(`Auto-start: ${config.autoStart ? '✅ Enabled' : '❌ Disabled'}`);
782
+ console.log("===========================\n");
783
+ }
784
+
785
+ // ========== WINDOWS AUTO-START ==========
786
+
787
+ function enableAutoStartWindows() {
788
+ const { execSync } = require('child_process');
194
789
 
195
- function processQueue() {
196
- if (isRunning || commandQueue.length === 0) return;
790
+ // Get the path to node and the agent script
791
+ const nodePath = process.execPath;
792
+ const agentPath = __filename;
197
793
 
198
- isRunning = true;
199
- let command = commandQueue.shift();
794
+ // Create a VBScript to run the agent silently (no console window)
795
+ const vbsPath = path.join(os.homedir(), 'vortix-agent.vbs');
796
+ const vbsContent = `Set WshShell = CreateObject("WScript.Shell")
797
+ WshShell.Run """${nodePath}"" ""${agentPath}"" start", 0, False`;
200
798
 
201
- console.log("Executing command:", command);
799
+ fs.writeFileSync(vbsPath, vbsContent);
202
800
 
203
- // Update current working directory if this is a cd command
204
- const newCwd = extractDirectoryFromCommand(command);
205
- if (newCwd) {
206
- currentCwd = newCwd;
207
- ws.send(JSON.stringify({ type: "LOG", deviceName, message: `Directory changed to: ${currentCwd}` }));
208
- console.log("Directory state updated to:", currentCwd);
801
+ // Add to Windows Task Scheduler
802
+ const taskName = "VortixAgent";
803
+ const command = `schtasks /create /tn "${taskName}" /tr "${vbsPath}" /sc onlogon /rl highest /f`;
804
+
805
+ try {
806
+ execSync(command, { stdio: 'ignore' });
807
+ console.log(" Created Windows Task Scheduler entry");
808
+ } catch (err) {
809
+ throw new Error("Failed to create scheduled task. Try running as administrator.");
810
+ }
811
+ }
812
+
813
+ function disableAutoStartWindows() {
814
+ const { execSync } = require('child_process');
815
+
816
+ const taskName = "VortixAgent";
817
+ const command = `schtasks /delete /tn "${taskName}" /f`;
818
+
819
+ try {
820
+ execSync(command, { stdio: 'ignore' });
821
+ console.log(" Removed Windows Task Scheduler entry");
822
+ } catch (err) {
823
+ // Task might not exist, that's okay
824
+ }
825
+
826
+ // Remove VBScript file
827
+ const vbsPath = path.join(os.homedir(), 'vortix-agent.vbs');
828
+ if (fs.existsSync(vbsPath)) {
829
+ fs.unlinkSync(vbsPath);
830
+ }
831
+ }
832
+
833
+ // ========== macOS AUTO-START ==========
834
+
835
+ function enableAutoStartMac() {
836
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.vortix.agent.plist');
837
+
838
+ // Get the path to node and the agent script
839
+ const nodePath = process.execPath;
840
+ const agentPath = __filename;
841
+
842
+ const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
843
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
844
+ <plist version="1.0">
845
+ <dict>
846
+ <key>Label</key>
847
+ <string>com.vortix.agent</string>
848
+ <key>ProgramArguments</key>
849
+ <array>
850
+ <string>${nodePath}</string>
851
+ <string>${agentPath}</string>
852
+ <string>start</string>
853
+ </array>
854
+ <key>RunAtLoad</key>
855
+ <true/>
856
+ <key>KeepAlive</key>
857
+ <true/>
858
+ <key>StandardOutPath</key>
859
+ <string>${os.homedir()}/Library/Logs/vortix-agent.log</string>
860
+ <key>StandardErrorPath</key>
861
+ <string>${os.homedir()}/Library/Logs/vortix-agent-error.log</string>
862
+ </dict>
863
+ </plist>`;
864
+
865
+ // Create LaunchAgents directory if it doesn't exist
866
+ const launchAgentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
867
+ if (!fs.existsSync(launchAgentsDir)) {
868
+ fs.mkdirSync(launchAgentsDir, { recursive: true });
869
+ }
870
+
871
+ fs.writeFileSync(plistPath, plistContent);
872
+
873
+ // Load the launch agent
874
+ const { execSync } = require('child_process');
875
+ try {
876
+ execSync(`launchctl load ${plistPath}`, { stdio: 'ignore' });
877
+ console.log(" Created macOS LaunchAgent");
878
+ } catch (err) {
879
+ console.log(" Created LaunchAgent (will load on next login)");
880
+ }
881
+ }
882
+
883
+ function disableAutoStartMac() {
884
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.vortix.agent.plist');
885
+
886
+ if (fs.existsSync(plistPath)) {
887
+ const { execSync } = require('child_process');
888
+ try {
889
+ execSync(`launchctl unload ${plistPath}`, { stdio: 'ignore' });
890
+ } catch (err) {
891
+ // Might not be loaded, that's okay
209
892
  }
210
893
 
211
- // report current cwd for diagnostics
212
- ws.send(JSON.stringify({ type: "LOG", deviceName, message: `Working from: ${currentCwd}` }));
213
- ws.send(JSON.stringify({ type: "LOG", deviceName, message: `Executing: ${command}` }));
894
+ fs.unlinkSync(plistPath);
895
+ console.log(" Removed macOS LaunchAgent");
896
+ }
897
+ }
898
+
899
+ // ========== LINUX AUTO-START ==========
214
900
 
215
- // Use cmd.exe with /c flag to properly execute Windows commands
216
- const process = exec(command, {
217
- cwd: currentCwd,
218
- shell: "cmd.exe",
219
- maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large outputs
220
- encoding: "utf8"
221
- });
901
+ function enableAutoStartLinux() {
902
+ const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'vortix-agent.service');
222
903
 
223
- // Track if we've received any output
224
- let hasOutput = false;
904
+ // Get the path to node and the agent script
905
+ const nodePath = process.execPath;
906
+ const agentPath = __filename;
225
907
 
226
- process.stdout.on("data", (data) => {
227
- hasOutput = true;
228
- ws.send(JSON.stringify({
229
- type: "LOG",
230
- deviceName,
231
- message: data.toString()
232
- }));
233
- });
908
+ const serviceContent = `[Unit]
909
+ Description=Vortix Agent - Remote OS Control
910
+ After=network.target
234
911
 
235
- process.stderr.on("data", (data) => {
236
- hasOutput = true;
237
- ws.send(JSON.stringify({
238
- type: "LOG",
239
- deviceName,
240
- message: `[ERROR] ${data.toString()}`
241
- }));
242
- });
912
+ [Service]
913
+ Type=simple
914
+ ExecStart=${nodePath} ${agentPath} start
915
+ Restart=always
916
+ RestartSec=10
243
917
 
244
- process.on("error", (err) => {
245
- ws.send(JSON.stringify({
246
- type: "LOG",
247
- deviceName,
248
- message: `[EXEC ERROR] ${err.message}`
249
- }));
250
- });
918
+ [Install]
919
+ WantedBy=default.target`;
251
920
 
252
- process.on("close", (code) => {
253
- ws.send(JSON.stringify({
254
- type: "LOG",
255
- deviceName,
256
- message: `Command execution finished with code: ${code}`
257
- }));
258
-
259
- // Send structured result so server can orchestrate sequential steps
260
- ws.send(JSON.stringify({
261
- type: "EXECUTE_RESULT",
262
- command,
263
- code: typeof code === 'number' ? code : 0
264
- }));
265
-
266
- isRunning = false;
267
- processQueue();
268
- });
921
+ // Create systemd user directory if it doesn't exist
922
+ const systemdDir = path.join(os.homedir(), '.config', 'systemd', 'user');
923
+ if (!fs.existsSync(systemdDir)) {
924
+ fs.mkdirSync(systemdDir, { recursive: true });
925
+ }
926
+
927
+ fs.writeFileSync(servicePath, serviceContent);
928
+
929
+ // Enable and start the service
930
+ const { execSync } = require('child_process');
931
+ try {
932
+ execSync('systemctl --user daemon-reload', { stdio: 'ignore' });
933
+ execSync('systemctl --user enable vortix-agent.service', { stdio: 'ignore' });
934
+ execSync('systemctl --user start vortix-agent.service', { stdio: 'ignore' });
935
+ console.log(" Created systemd user service");
936
+ } catch (err) {
937
+ console.log(" Created service file (enable with: systemctl --user enable vortix-agent.service)");
938
+ }
939
+ }
940
+
941
+ function disableAutoStartLinux() {
942
+ const servicePath = path.join(os.homedir(), '.config', 'systemd', 'user', 'vortix-agent.service');
943
+
944
+ if (fs.existsSync(servicePath)) {
945
+ const { execSync } = require('child_process');
946
+ try {
947
+ execSync('systemctl --user stop vortix-agent.service', { stdio: 'ignore' });
948
+ execSync('systemctl --user disable vortix-agent.service', { stdio: 'ignore' });
949
+ } catch (err) {
950
+ // Might not be running, that's okay
951
+ }
952
+
953
+ fs.unlinkSync(servicePath);
954
+ console.log(" Removed systemd user service");
269
955
  }
270
956
  }
@@ -12,6 +12,7 @@
12
12
  "license": "ISC",
13
13
  "type": "commonjs",
14
14
  "dependencies": {
15
- "ws": "^8.19.0"
15
+ "ws": "^8.19.0",
16
+ "screenshot-desktop": "^1.15.0"
16
17
  }
17
18
  }
package/bin/vortix.js CHANGED
@@ -1,6 +1,8 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  const path = require('path');
4
+ const fs = require('fs');
5
+ const os = require('os');
4
6
 
5
7
  const command = process.argv[2];
6
8
 
@@ -9,21 +11,73 @@ const packageRoot = path.join(__dirname, '..');
9
11
  const agentPath = path.join(packageRoot, 'agent', 'agent.js');
10
12
  const backendPath = path.join(packageRoot, 'backend', 'server.js');
11
13
 
14
+ // Check if this is first run
15
+ const configPath = path.join(os.homedir(), '.vortix-config.json');
16
+ const firstRunFlagPath = path.join(os.homedir(), '.vortix-first-run');
17
+
18
+ function showWelcome() {
19
+ const colors = {
20
+ reset: '\x1b[0m',
21
+ bright: '\x1b[1m',
22
+ green: '\x1b[32m',
23
+ cyan: '\x1b[36m',
24
+ yellow: '\x1b[33m',
25
+ magenta: '\x1b[35m',
26
+ };
27
+
28
+ console.log('\n');
29
+ console.log(colors.bright + colors.green + ' ╦ ╦┌─┐┬─┐┌┬┐┬─┐ ┬' + colors.reset);
30
+ console.log(colors.bright + colors.green + ' ╚╗╔╝│ │├┬┘ │ │┌┴┬┘' + colors.reset);
31
+ console.log(colors.bright + colors.green + ' ╚╝ └─┘┴└─ ┴ ┴┴ └─' + colors.reset);
32
+ console.log('\n');
33
+ console.log(colors.bright + colors.cyan + ' 🚀 Welcome to Vortix!' + colors.reset);
34
+ console.log(colors.yellow + ' AI-Powered Remote OS Control' + colors.reset);
35
+ console.log('\n');
36
+ console.log(colors.bright + ' 📖 Quick Start:' + colors.reset);
37
+ console.log('');
38
+ console.log(' ' + colors.cyan + '1.' + colors.reset + ' Set device password:');
39
+ console.log(' ' + colors.green + 'vortix login' + colors.reset);
40
+ console.log('');
41
+ console.log(' ' + colors.cyan + '2.' + colors.reset + ' Start the agent:');
42
+ console.log(' ' + colors.green + 'vortix start' + colors.reset);
43
+ console.log('');
44
+ console.log(' ' + colors.cyan + '3.' + colors.reset + ' Open dashboard:');
45
+ console.log(' ' + colors.magenta + colors.bright + 'https://vortixai.vercel.app' + colors.reset);
46
+ console.log('');
47
+ console.log(colors.yellow + ' ⚡ Pro Tip: ' + colors.reset + 'Use AI commands in the dashboard for natural language control!');
48
+ console.log('');
49
+ }
50
+
12
51
  function showHelp() {
13
- console.log(`
14
- Vortix - AI OS Control CLI
15
-
16
- Usage:
17
- vortix login Login and authenticate device
18
- vortix start Start the agent
19
- vortix backend Start the backend server
20
- vortix help Show this help message
21
-
22
- Examples:
23
- vortix login # Authenticate your device
24
- vortix start # Start agent on this machine
25
- vortix backend # Start backend server
26
- `);
52
+ showWelcome();
53
+ console.log(' 📚 Available Commands:\n');
54
+ console.log(' vortix login Set device password');
55
+ console.log(' vortix start Start the agent');
56
+ console.log(' vortix enable-autostart Enable auto-start on system boot');
57
+ console.log(' vortix disable-autostart Disable auto-start');
58
+ console.log(' vortix status Check agent and auto-start status');
59
+ console.log(' vortix backend Start backend server (dev only)');
60
+ console.log(' vortix help Show this help message');
61
+ console.log('');
62
+ console.log(' 📖 Documentation:');
63
+ console.log(' https://github.com/Vaibhav262610/vortix');
64
+ console.log('');
65
+ }
66
+
67
+ // Show welcome message on first run or if no command provided
68
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
69
+ if (!fs.existsSync(firstRunFlagPath) && !command) {
70
+ // First run without command
71
+ showWelcome();
72
+ fs.writeFileSync(firstRunFlagPath, Date.now().toString());
73
+ process.exit(0);
74
+ } else if (command === 'help' || command === '--help' || command === '-h') {
75
+ showHelp();
76
+ process.exit(0);
77
+ } else if (!command) {
78
+ showHelp();
79
+ process.exit(0);
80
+ }
27
81
  }
28
82
 
29
83
  function runAgent(cmd) {
@@ -66,14 +120,20 @@ switch (command) {
66
120
  runAgent('start');
67
121
  break;
68
122
 
69
- case 'backend':
70
- runBackend();
123
+ case 'enable-autostart':
124
+ runAgent('enable-autostart');
71
125
  break;
72
126
 
73
- case 'help':
74
- case '--help':
75
- case '-h':
76
- showHelp();
127
+ case 'disable-autostart':
128
+ runAgent('disable-autostart');
129
+ break;
130
+
131
+ case 'status':
132
+ runAgent('status');
133
+ break;
134
+
135
+ case 'backend':
136
+ runBackend();
77
137
  break;
78
138
 
79
139
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vortix",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "AI-powered OS control system with remote command execution",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -15,7 +15,17 @@
15
15
  "remote-control",
16
16
  "cli",
17
17
  "os-control",
18
- "agent"
18
+ "agent",
19
+ "nodejs",
20
+ "websocket",
21
+ "command-execution",
22
+ "screen-sharing",
23
+ "multi-platform",
24
+ "windows",
25
+ "macos",
26
+ "linux",
27
+ "typescript",
28
+ "nextjs"
19
29
  ],
20
30
  "author": "Vaibhav Rajpoot <vaibhavrajpoot2626@gmail.com>",
21
31
  "license": "MIT",
@@ -23,6 +33,11 @@
23
33
  "type": "git",
24
34
  "url": "https://github.com/Vaibhav262610/vortix.git"
25
35
  },
36
+ "homepage": "https://vortixai.vercel.app",
37
+ "bugs": {
38
+ "url": "https://github.com/Vaibhav262610/vortix/issues",
39
+ "email": "vaibhavrajpoot2626@gmail.com"
40
+ },
26
41
  "engines": {
27
42
  "node": ">=14.0.0"
28
43
  },
@@ -35,6 +50,7 @@
35
50
  ],
36
51
  "dependencies": {
37
52
  "axios": "^1.6.0",
38
- "ws": "^8.19.0"
53
+ "ws": "^8.19.0",
54
+ "screenshot-desktop": "^1.15.0"
39
55
  }
40
56
  }