samarthya-bot 1.1.2 → 1.1.3
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/services/tools/toolRegistry.js +211 -25
- package/backend/services/worker/workerClient.js +143 -0
- package/package.json +3 -2
- package/worker/go.mod +3 -0
- package/worker/main.go +156 -0
- package/worker/samarthya-worker +0 -0
|
@@ -4,11 +4,19 @@ const path = require('path');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const { exec } = require('child_process');
|
|
6
6
|
const nodemailer = require('nodemailer');
|
|
7
|
+
const { Client } = require('ssh2');
|
|
8
|
+
const workerClient = require('../worker/workerClient');
|
|
7
9
|
|
|
8
10
|
// ────────────────────────────────────────────────────────────
|
|
9
11
|
// REAL Tool Definitions — No more simulations!
|
|
10
12
|
// ────────────────────────────────────────────────────────────
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Security: Command Blacklist Map
|
|
16
|
+
* Prevent the AI (or user injection) from autonomously executing critically destructive host/remote commands.
|
|
17
|
+
*/
|
|
18
|
+
const BLOCKED_COMMANDS = /^(rm\s+-rf|rmdir|mkfs|dd|fdisk|shutdown|reboot|poweroff|halt|init|killall\s+-9|wget.*\.sh|curl.*\.sh|chmod\s+-R\s+777|chown\s+-R.*:.*\/)/im;
|
|
19
|
+
|
|
12
20
|
/**
|
|
13
21
|
* Safe directory — tools can only operate within this sandbox
|
|
14
22
|
* Change this to allow broader access (at your own risk)
|
|
@@ -491,10 +499,56 @@ const toolDefinitions = {
|
|
|
491
499
|
}
|
|
492
500
|
},
|
|
493
501
|
|
|
494
|
-
// ───────────
|
|
502
|
+
// ─────────── DEVOPS / STREAMING EXECUTION (Go Worker Integration) ───────────
|
|
503
|
+
devops_execute_stream: {
|
|
504
|
+
name: 'devops_execute_stream',
|
|
505
|
+
description: 'Run long or complex shell commands (like npm install, git push, vercel deploy) and stream the output back. Required for any heavy DevOps/Auto-Coder tasks. Uses the ultra-fast Go micro-worker.',
|
|
506
|
+
descriptionHi: 'लाइव शेल कमांड चलाएं',
|
|
507
|
+
riskLevel: 'critical',
|
|
508
|
+
category: 'system',
|
|
509
|
+
parameters: {
|
|
510
|
+
command: { type: 'string', required: true, description: 'Command to execute safely via Go' },
|
|
511
|
+
dir: { type: 'string', required: false, description: 'Working directory for the command' }
|
|
512
|
+
},
|
|
513
|
+
execute: async (args, userContext) => {
|
|
514
|
+
if (BLOCKED_COMMANDS.test(args.command)) {
|
|
515
|
+
return { success: false, result: `❌ Security Block: The command '${args.command}' contains patterns that are restricted (e.g., recursive deletes, system reboots, disk formats).` };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return new Promise((resolve) => {
|
|
519
|
+
let liveLog = '';
|
|
520
|
+
|
|
521
|
+
// Let the Go worker handle the heavy lifting!
|
|
522
|
+
workerClient.executeCommand(args.command, args.dir || '', (data, type) => {
|
|
523
|
+
// Collect live output
|
|
524
|
+
liveLog += data + '\\n';
|
|
525
|
+
// Optional: If we had a websocket to the frontend dashboard, we'd emit it here!
|
|
526
|
+
}).then(({ success, exitCode, output, elapsed }) => {
|
|
527
|
+
// Combine whatever was streamed
|
|
528
|
+
const finalOutput = output || liveLog;
|
|
529
|
+
|
|
530
|
+
if (!success) {
|
|
531
|
+
resolve({
|
|
532
|
+
success: false,
|
|
533
|
+
result: `❌ Execution Failed (Code ${exitCode}):\n\`\`\`\n${finalOutput.substring(finalOutput.length > 3000 ? finalOutput.length - 3000 : 0)}\n\`\`\``
|
|
534
|
+
});
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
resolve({
|
|
538
|
+
success: true,
|
|
539
|
+
result: `✅ **Success:** \`${args.command}\`\n⏱️ Elapsed: ${elapsed}ms\n\n\`\`\`\n${finalOutput.substring(0, 3000)}\n\`\`\``
|
|
540
|
+
});
|
|
541
|
+
}).catch(err => {
|
|
542
|
+
resolve({ success: false, result: `❌ Worker Error: ${err.message}` });
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
// ─────────── LEGACY RUN COMMAND (Kept for instant small commands) ───────────
|
|
495
549
|
run_command: {
|
|
496
550
|
name: 'run_command',
|
|
497
|
-
description: 'Run
|
|
551
|
+
description: 'Run basic, instant shell commands natively. For long-running deploying/building, use devops_execute_stream instead.',
|
|
498
552
|
descriptionHi: 'शेल कमांड चलाएं',
|
|
499
553
|
riskLevel: 'critical',
|
|
500
554
|
category: 'system',
|
|
@@ -502,6 +556,10 @@ const toolDefinitions = {
|
|
|
502
556
|
command: { type: 'string', required: true, description: 'Shell command to execute' }
|
|
503
557
|
},
|
|
504
558
|
execute: async (args, userContext) => {
|
|
559
|
+
if (BLOCKED_COMMANDS.test(args.command)) {
|
|
560
|
+
return { success: false, result: `❌ Security Block: The command '${args.command}' contains patterns that are restricted (e.g., recursive deletes, system reboots, disk formats).` };
|
|
561
|
+
}
|
|
562
|
+
|
|
505
563
|
return new Promise((resolve) => {
|
|
506
564
|
exec(args.command, { timeout: 15000, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
|
|
507
565
|
if (error) {
|
|
@@ -520,6 +578,85 @@ const toolDefinitions = {
|
|
|
520
578
|
}
|
|
521
579
|
},
|
|
522
580
|
|
|
581
|
+
// ─────────── REMOTE VPS DEPLOYMENT (SSH Stream) ───────────
|
|
582
|
+
ssh_deploy: {
|
|
583
|
+
name: 'ssh_deploy',
|
|
584
|
+
description: 'Login to a remote VPS server via SSH and execute commands autonomously (e.g., git pull, npm run build, pm2 restart).',
|
|
585
|
+
descriptionHi: 'रिमोट सर्वर पर कमांड चलाएं',
|
|
586
|
+
riskLevel: 'critical',
|
|
587
|
+
category: 'system',
|
|
588
|
+
parameters: {
|
|
589
|
+
host: { type: 'string', required: true, description: 'Server IP or Hostname' },
|
|
590
|
+
username: { type: 'string', required: true, description: 'SSH Username (e.g., root)' },
|
|
591
|
+
password: { type: 'string', required: false, description: 'SSH Password (if key is not used)' },
|
|
592
|
+
privateKeyPath: { type: 'string', required: false, description: 'Absolute path to local SSH private key file (if password is not used)' },
|
|
593
|
+
command: { type: 'string', required: true, description: 'Command string to run on the remote VPS (e.g., "cd /var/www && git pull")' }
|
|
594
|
+
},
|
|
595
|
+
execute: async (args, userContext) => {
|
|
596
|
+
if (BLOCKED_COMMANDS.test(args.command)) {
|
|
597
|
+
return { success: false, result: `❌ Security Block: The command '${args.command}' contains patterns that are restricted on remote SSH systems.` };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return new Promise(async (resolve) => {
|
|
601
|
+
const conn = new Client();
|
|
602
|
+
let outputLog = '';
|
|
603
|
+
|
|
604
|
+
// Build SSH config
|
|
605
|
+
const config = {
|
|
606
|
+
host: args.host,
|
|
607
|
+
port: 22,
|
|
608
|
+
username: args.username,
|
|
609
|
+
readyTimeout: 10000 // 10s connection timeout
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// Auth strategy
|
|
613
|
+
if (args.password) {
|
|
614
|
+
config.password = args.password;
|
|
615
|
+
} else if (args.privateKeyPath) {
|
|
616
|
+
try {
|
|
617
|
+
config.privateKey = await fs.readFile(args.privateKeyPath, 'utf8');
|
|
618
|
+
} catch (e) {
|
|
619
|
+
return resolve({ success: false, result: `❌ Failed to read private key at ${args.privateKeyPath}: ${e.message}` });
|
|
620
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
return resolve({ success: false, result: `❌ Missing Authentication. Provide either 'password' or 'privateKeyPath'.` });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
conn.on('ready', () => {
|
|
626
|
+
outputLog += `[SSH] Connected to ${args.username}@${args.host} successfully.\n`;
|
|
627
|
+
outputLog += `[SSH] Executing: ${args.command}\n\n`;
|
|
628
|
+
|
|
629
|
+
conn.exec(args.command, (err, stream) => {
|
|
630
|
+
if (err) {
|
|
631
|
+
conn.end();
|
|
632
|
+
return resolve({ success: false, result: `❌ Command Execution Error:\n\`\`\`\n${err.message}\n\`\`\`` });
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
stream.on('close', (code, signal) => {
|
|
636
|
+
conn.end();
|
|
637
|
+
const success = code === 0;
|
|
638
|
+
const statusIcon = success ? '✅' : '❌';
|
|
639
|
+
|
|
640
|
+
resolve({
|
|
641
|
+
success: success,
|
|
642
|
+
result: `${statusIcon} **SSH Execution Complete** (Exit Code: ${code})\n\n\`\`\`\n${outputLog.substring(outputLog.length > 3000 ? outputLog.length - 3000 : 0)}\n\`\`\``
|
|
643
|
+
});
|
|
644
|
+
}).on('data', (data) => {
|
|
645
|
+
outputLog += data.toString();
|
|
646
|
+
}).stderr.on('data', (data) => {
|
|
647
|
+
outputLog += data.toString();
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
}).on('error', (err) => {
|
|
651
|
+
resolve({ success: false, result: `❌ SSH Connection Error to ${args.host}: ${err.message}` });
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Start connection
|
|
655
|
+
conn.connect(config);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
|
|
523
660
|
// ─────────── GST REMINDER (Real — saves reminder files) ───────────
|
|
524
661
|
gst_reminder: {
|
|
525
662
|
name: 'gst_reminder',
|
|
@@ -685,32 +822,84 @@ const toolDefinitions = {
|
|
|
685
822
|
}
|
|
686
823
|
},
|
|
687
824
|
|
|
688
|
-
// ───────────
|
|
689
|
-
|
|
690
|
-
name: '
|
|
691
|
-
description: '
|
|
692
|
-
descriptionHi: 'ब्राउज़र
|
|
693
|
-
riskLevel: '
|
|
825
|
+
// ─────────── ADVANCED BROWSER AUTOMATION (Puppeteer) ───────────
|
|
826
|
+
browser_action: {
|
|
827
|
+
name: 'browser_action',
|
|
828
|
+
description: 'Auto-control a chromium browser. It can navigate, click, type, and read the screen autonomously. Use this to create Github repos, write docs, or scrape web UI.',
|
|
829
|
+
descriptionHi: 'ब्राउज़र पे ऑटोमैटिक काम करें',
|
|
830
|
+
riskLevel: 'high',
|
|
694
831
|
category: 'browser',
|
|
695
832
|
parameters: {
|
|
696
|
-
url: { type: 'string', required: true, description: 'URL to
|
|
833
|
+
url: { type: 'string', required: true, description: 'URL to navigate to or interact with.' },
|
|
834
|
+
actions: {
|
|
835
|
+
type: 'string',
|
|
836
|
+
required: false,
|
|
837
|
+
description: 'JSON array of actions. Format: [{"type":"click","selector":"#submit"},{"type":"type","selector":"#email","text":"my@email.com"},{"type":"wait","ms":2000},{"type":"extract","selector":"body"}]'
|
|
838
|
+
}
|
|
697
839
|
},
|
|
698
840
|
execute: async (args, userContext) => {
|
|
699
|
-
return new Promise((resolve) => {
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
841
|
+
return new Promise(async (resolve) => {
|
|
842
|
+
const puppeteer = require('puppeteer-core');
|
|
843
|
+
let browser;
|
|
844
|
+
let log = `🌐 **Browser Automation Started:** ${args.url}\n`;
|
|
845
|
+
|
|
846
|
+
try {
|
|
847
|
+
// Try to connect to existing local Chrome, fallback to a downloaded edge/chrome if needed.
|
|
848
|
+
// For local system testing, we simulate launching a default visible chrome.
|
|
849
|
+
let execPaths = [];
|
|
850
|
+
if (os.platform() === 'win32') {
|
|
851
|
+
execPaths = [
|
|
852
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
853
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'
|
|
854
|
+
];
|
|
855
|
+
} else if (os.platform() === 'darwin') {
|
|
856
|
+
execPaths = ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'];
|
|
707
857
|
} else {
|
|
708
|
-
|
|
709
|
-
success: true,
|
|
710
|
-
result: `🌐 **Opened in browser:** ${args.url}`
|
|
711
|
-
});
|
|
858
|
+
execPaths = ['/usr/bin/google-chrome', '/usr/bin/chromium-browser'];
|
|
712
859
|
}
|
|
713
|
-
|
|
860
|
+
|
|
861
|
+
let validPath = execPaths.find(p => require('fs').existsSync(p));
|
|
862
|
+
if (!validPath) {
|
|
863
|
+
return resolve({ success: false, result: `❌ Chrome/Edge executable not found for Puppeteer.` });
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
browser = await puppeteer.launch({
|
|
867
|
+
executablePath: validPath,
|
|
868
|
+
headless: false, // Make it visible to the user!
|
|
869
|
+
defaultViewport: null,
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
const page = await browser.newPage();
|
|
873
|
+
await page.goto(args.url, { waitUntil: 'networkidle2' });
|
|
874
|
+
log += `✅ Loaded: ${args.url}\n`;
|
|
875
|
+
|
|
876
|
+
if (args.actions) {
|
|
877
|
+
const actionsList = JSON.parse(args.actions);
|
|
878
|
+
for (let act of actionsList) {
|
|
879
|
+
if (act.type === 'click') {
|
|
880
|
+
await page.click(act.selector);
|
|
881
|
+
log += `🖱️ Clicked: ${act.selector}\n`;
|
|
882
|
+
} else if (act.type === 'type') {
|
|
883
|
+
await page.type(act.selector, act.text);
|
|
884
|
+
log += `⌨️ Typed "${act.text}" into ${act.selector}\n`;
|
|
885
|
+
} else if (act.type === 'wait') {
|
|
886
|
+
await new Promise(r => setTimeout(r, act.ms));
|
|
887
|
+
log += `⏳ Waited ${act.ms}ms\n`;
|
|
888
|
+
} else if (act.type === 'extract') {
|
|
889
|
+
const text = await page.$eval(act.selector, el => el.innerText);
|
|
890
|
+
log += `📄 Extracted text from ${act.selector}:\n\`\`\`\n${text.substring(0, 500)}...\n\`\`\`\n`;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Leave browser open for the user if it's a visual task
|
|
896
|
+
setTimeout(() => browser.close(), 60000);
|
|
897
|
+
|
|
898
|
+
resolve({ success: true, result: log + '\n✨ Browser task completed successfully.' });
|
|
899
|
+
} catch (err) {
|
|
900
|
+
if (browser) await browser.close();
|
|
901
|
+
resolve({ success: false, result: `❌ Browser Action Failed:\n${err.message}\n\nLogs:\n${log}` });
|
|
902
|
+
}
|
|
714
903
|
});
|
|
715
904
|
}
|
|
716
905
|
},
|
|
@@ -728,13 +917,10 @@ const toolDefinitions = {
|
|
|
728
917
|
const screenshot = require('screenshot-desktop');
|
|
729
918
|
screenshot({ format: 'png' })
|
|
730
919
|
.then((imgRaw) => {
|
|
731
|
-
// Return the base64 to the LLM
|
|
732
920
|
const base64 = imgRaw.toString('base64');
|
|
733
921
|
resolve({
|
|
734
922
|
success: true,
|
|
735
923
|
result: `📸 **Screenshot Captured successfully!**\nUse the analyze_screen tool or your vision capabilities to see what is on screen.\n[IMAGE_DATA_BASE64_READY_INTERNAL_USE]`
|
|
736
|
-
// Note: We don't feed the raw 5MB string back into the prompt buffer directly here,
|
|
737
|
-
// but the agent now knows it took a shot. In a full system, you'd integrate this with llmService vision api directly.
|
|
738
924
|
});
|
|
739
925
|
})
|
|
740
926
|
.catch((err) => {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { v4: uuidv4 } = require('uuid');
|
|
5
|
+
|
|
6
|
+
class WorkerClient {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.workerProcess = null;
|
|
9
|
+
this.pendingRequests = new Map();
|
|
10
|
+
|
|
11
|
+
// Find the worker binary.
|
|
12
|
+
// In dev, it's inside `worker/samarthya-worker`
|
|
13
|
+
// In prod (npm module), it might be inside the module root.
|
|
14
|
+
|
|
15
|
+
// Let's resolve it more robustly:
|
|
16
|
+
const projectRoot = path.resolve(__dirname, '../../../'); // Gives /home/bishnups/Documents/PROJECT_DEB
|
|
17
|
+
this.workerPath = path.join(projectRoot, 'worker', 'samarthya-worker' + (os.platform() === 'win32' ? '.exe' : ''));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
start() {
|
|
21
|
+
if (this.workerProcess) return;
|
|
22
|
+
|
|
23
|
+
console.log(`[Worker] Starting Go Micro-Worker from: ${this.workerPath}`);
|
|
24
|
+
|
|
25
|
+
this.workerProcess = spawn(this.workerPath, [], {
|
|
26
|
+
stdio: ['pipe', 'pipe', 'inherit'] // We write to stdin, read from stdout
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
this.workerProcess.on('error', (err) => {
|
|
30
|
+
console.error('[Worker] Failed to start Go worker:', err.message);
|
|
31
|
+
console.error('[Worker] Make sure you have run `cd worker && go build -o samarthya-worker`');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
this.workerProcess.on('exit', (code) => {
|
|
35
|
+
console.log(`[Worker] Go worker exited with code ${code}. Restarting in 5s...`);
|
|
36
|
+
this.workerProcess = null;
|
|
37
|
+
setTimeout(() => this.start(), 5000);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Listen to JSON stream line by line
|
|
41
|
+
let buffer = '';
|
|
42
|
+
this.workerProcess.stdout.on('data', (chunk) => {
|
|
43
|
+
buffer += chunk.toString();
|
|
44
|
+
let newlineIdx;
|
|
45
|
+
while ((newlineIdx = buffer.indexOf('\n')) >= 0) {
|
|
46
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
47
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
48
|
+
|
|
49
|
+
if (line) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(line);
|
|
52
|
+
this._handleResponse(parsed);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error('[Worker] JSON Parse Error from Go line:', line);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_handleResponse(res) {
|
|
62
|
+
if (!res.id || !this.pendingRequests.has(res.id)) {
|
|
63
|
+
// Unbound message or logging
|
|
64
|
+
if (res.type === 'error') {
|
|
65
|
+
console.error(`[Worker Msg] ${res.data}`);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handlers = this.pendingRequests.get(res.id);
|
|
71
|
+
|
|
72
|
+
if (res.type === 'stdout' || res.type === 'stderr') {
|
|
73
|
+
if (handlers.onStream) {
|
|
74
|
+
handlers.onStream(res.data, res.type);
|
|
75
|
+
} else {
|
|
76
|
+
// If the user didn't provide a stream handler, buffer it internally
|
|
77
|
+
if (!handlers.buffer) handlers.buffer = '';
|
|
78
|
+
handlers.buffer += res.data + '\n';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (res.type === 'end') {
|
|
82
|
+
const finalData = handlers.buffer || res.data;
|
|
83
|
+
handlers.resolve({
|
|
84
|
+
success: res.exitCode === 0,
|
|
85
|
+
exitCode: res.exitCode,
|
|
86
|
+
output: finalData,
|
|
87
|
+
elapsed: res.elapsedTimeMs
|
|
88
|
+
});
|
|
89
|
+
this.pendingRequests.delete(res.id);
|
|
90
|
+
}
|
|
91
|
+
else if (res.type === 'error') {
|
|
92
|
+
handlers.resolve({
|
|
93
|
+
success: false,
|
|
94
|
+
exitCode: -1,
|
|
95
|
+
output: res.data,
|
|
96
|
+
elapsed: 0
|
|
97
|
+
});
|
|
98
|
+
this.pendingRequests.delete(res.id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
executeCommand(command, dir = '', streamCallback = null) {
|
|
103
|
+
if (!this.workerProcess) {
|
|
104
|
+
this.start();
|
|
105
|
+
if (!this.workerProcess) {
|
|
106
|
+
return Promise.resolve({ success: false, output: 'Worker process unavailable. Is the Go binary built?' });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
const reqId = uuidv4();
|
|
112
|
+
|
|
113
|
+
// Register handlers
|
|
114
|
+
this.pendingRequests.set(reqId, {
|
|
115
|
+
resolve,
|
|
116
|
+
onStream: streamCallback,
|
|
117
|
+
buffer: ''
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const req = {
|
|
121
|
+
id: reqId,
|
|
122
|
+
command: command,
|
|
123
|
+
dir: dir,
|
|
124
|
+
stream: true,
|
|
125
|
+
timeoutMs: 0
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Send to Go via stdin
|
|
129
|
+
this.workerProcess.stdin.write(JSON.stringify(req) + '\n');
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
stop() {
|
|
134
|
+
if (this.workerProcess) {
|
|
135
|
+
this.workerProcess.kill();
|
|
136
|
+
this.workerProcess = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Export singleton instance
|
|
142
|
+
const workerClient = new WorkerClient();
|
|
143
|
+
module.exports = workerClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "samarthya-bot",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Privacy-First Local Agentic OS & Command Center",
|
|
5
5
|
"main": "backend/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"puppeteer-core": "^24.37.5",
|
|
28
28
|
"screenshot-desktop": "^1.15.3",
|
|
29
29
|
"socket.io": "^4.8.3",
|
|
30
|
+
"ssh2": "^1.17.0",
|
|
30
31
|
"uuid": "^13.0.0"
|
|
31
32
|
},
|
|
32
33
|
"keywords": [
|
|
@@ -46,4 +47,4 @@
|
|
|
46
47
|
"type": "git",
|
|
47
48
|
"url": "https://github.com/mebishnusahu0595/SamarthyaBot.git"
|
|
48
49
|
}
|
|
49
|
-
}
|
|
50
|
+
}
|
package/worker/go.mod
ADDED
package/worker/main.go
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"os"
|
|
9
|
+
"os/exec"
|
|
10
|
+
"strings"
|
|
11
|
+
"time"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// Request defines the incoming JSON command from Node.js
|
|
15
|
+
type Request struct {
|
|
16
|
+
ID string `json:"id"`
|
|
17
|
+
Command string `json:"command"`
|
|
18
|
+
Dir string `json:"dir"`
|
|
19
|
+
Stream bool `json:"stream"` // If true, stream output live
|
|
20
|
+
TimeoutMs int `json:"timeoutMs"` // 0 for infinite
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Response defines the payload sent back to Node.js
|
|
24
|
+
type Response struct {
|
|
25
|
+
ID string `json:"id"`
|
|
26
|
+
Type string `json:"type"` // "start", "stdout", "stderr", "end", "error"
|
|
27
|
+
Data string `json:"data"`
|
|
28
|
+
ExitCode int `json:"exitCode"`
|
|
29
|
+
ElapsedTime int64 `json:"elapsedTimeMs"`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func main() {
|
|
33
|
+
// The Node.js parent process communicates via process.stdin and process.stdout.
|
|
34
|
+
reader := bufio.NewReader(os.Stdin)
|
|
35
|
+
|
|
36
|
+
for {
|
|
37
|
+
line, err := reader.ReadString('\n')
|
|
38
|
+
if err != nil {
|
|
39
|
+
if err == io.EOF {
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
line = strings.TrimSpace(line)
|
|
46
|
+
if line == "" {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var req Request
|
|
51
|
+
if err := json.Unmarshal([]byte(line), &req); err != nil {
|
|
52
|
+
sendError("", fmt.Sprintf("Failed to parse request: %v", err))
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Execute command in a goroutine so the worker can process multiple requests
|
|
57
|
+
go handleCommand(req)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func handleCommand(req Request) {
|
|
62
|
+
startTime := time.Now()
|
|
63
|
+
|
|
64
|
+
// Notify Node.js that the command has started
|
|
65
|
+
sendResponse(req.ID, "start", fmt.Sprintf("Executing: %s", req.Command), 0, 0)
|
|
66
|
+
|
|
67
|
+
// Since we want to pass a raw command string like `npm run dev` or `vercel --prod`,
|
|
68
|
+
// we use a shell to interpret it cleanly. Cross-platform awareness:
|
|
69
|
+
// For Windows, we might use "cmd" "/c". For Unix, "sh" "-c".
|
|
70
|
+
var cmd *exec.Cmd
|
|
71
|
+
if isWindows() {
|
|
72
|
+
cmd = exec.Command("cmd", "/C", req.Command)
|
|
73
|
+
} else {
|
|
74
|
+
cmd = exec.Command("sh", "-c", req.Command)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if req.Dir != "" {
|
|
78
|
+
cmd.Dir = req.Dir
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Capture stdout and stderr
|
|
82
|
+
stdoutPipe, err := cmd.StdoutPipe()
|
|
83
|
+
if err != nil {
|
|
84
|
+
sendError(req.ID, fmt.Sprintf("Failed to create stdout pipe: %v", err))
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
stderrPipe, err := cmd.StderrPipe()
|
|
88
|
+
if err != nil {
|
|
89
|
+
sendError(req.ID, fmt.Sprintf("Failed to create stderr pipe: %v", err))
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if err := cmd.Start(); err != nil {
|
|
94
|
+
sendError(req.ID, fmt.Sprintf("Failed to start command: %v", err))
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Start streamers
|
|
99
|
+
go streamToNode(req.ID, "stdout", stdoutPipe, req.Stream)
|
|
100
|
+
go streamToNode(req.ID, "stderr", stderrPipe, req.Stream)
|
|
101
|
+
|
|
102
|
+
// In the future for a production app, we would add the timeout context here.
|
|
103
|
+
err = cmd.Wait()
|
|
104
|
+
elapsed := time.Since(startTime).Milliseconds()
|
|
105
|
+
|
|
106
|
+
exitCode := 0
|
|
107
|
+
if err != nil {
|
|
108
|
+
if exitError, ok := err.(*exec.ExitError); ok {
|
|
109
|
+
exitCode = exitError.ExitCode()
|
|
110
|
+
} else {
|
|
111
|
+
exitCode = 1
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
sendResponse(req.ID, "end", "Command completed.", exitCode, elapsed)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func streamToNode(reqID string, streamType string, pipe io.Reader, allowStream bool) {
|
|
119
|
+
scanner := bufio.NewScanner(pipe)
|
|
120
|
+
|
|
121
|
+
// Create a large buffer (bufio.MaxScanTokenSize is usually 64kb)
|
|
122
|
+
buf := make([]byte, 0, 64*1024)
|
|
123
|
+
scanner.Buffer(buf, 1024*1024) // Allow up to 1MB per line/token
|
|
124
|
+
|
|
125
|
+
for scanner.Scan() {
|
|
126
|
+
text := scanner.Text()
|
|
127
|
+
if allowStream {
|
|
128
|
+
sendResponse(reqID, streamType, text, -1, 0)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func sendResponse(reqID string, resType string, data string, exitCode int, elapsed int64) {
|
|
134
|
+
// Base64 encode or properly escape JSON if necessary in complex outputs,
|
|
135
|
+
// but Go's json.Marshal smoothly handles quotes inside strings.
|
|
136
|
+
res := Response{
|
|
137
|
+
ID: reqID,
|
|
138
|
+
Type: resType,
|
|
139
|
+
Data: data,
|
|
140
|
+
ExitCode: exitCode,
|
|
141
|
+
ElapsedTime: elapsed,
|
|
142
|
+
}
|
|
143
|
+
encoded, _ := json.Marshal(res)
|
|
144
|
+
|
|
145
|
+
// Write to os.Stdout (this is picked up by Node.js).
|
|
146
|
+
// Make sure we end with a newline so Node can parse it line-by-line.
|
|
147
|
+
fmt.Println(string(encoded))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func sendError(reqID string, msg string) {
|
|
151
|
+
sendResponse(reqID, "error", msg, -1, 0)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func isWindows() bool {
|
|
155
|
+
return os.PathSeparator == '\\'
|
|
156
|
+
}
|
|
Binary file
|