samarthya-bot 1.1.1 → 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/README.md +11 -9
- package/backend/bin/samarthya.js +48 -5
- 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
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ SamarthyaBot is built for developers. The best way to start is the **CLI Wizard*
|
|
|
34
34
|
|
|
35
35
|
### 📦 Install (Global)
|
|
36
36
|
|
|
37
|
-
Runtime: **Node
|
|
37
|
+
Runtime: **Node 20 LTS** (Officially Supported/Recommended) and **MongoDB** (Local).
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
npm install -g samarthya-bot
|
|
@@ -42,10 +42,13 @@ npm install -g samarthya-bot
|
|
|
42
42
|
# Start the interactive setup
|
|
43
43
|
samarthya onboard
|
|
44
44
|
|
|
45
|
-
# Launch the engine and dashboard
|
|
45
|
+
# Launch the engine and dashboard (Terminal 1)
|
|
46
46
|
samarthya gateway
|
|
47
|
+
|
|
48
|
+
# Expose to internet & setup Telegram Webhook (Terminal 2)
|
|
49
|
+
samarthya tunnel
|
|
47
50
|
```
|
|
48
|
-
*The wizard guides you through API keys (Gemini/Ollama) and system permissions.*
|
|
51
|
+
*The wizard guides you through API keys (Gemini/Ollama, Telegram Bot Token) and system permissions.*
|
|
49
52
|
|
|
50
53
|
### 🛠️ From Source (Development)
|
|
51
54
|
|
|
@@ -87,7 +90,7 @@ npm run dev
|
|
|
87
90
|
| `samarthya status` | Display the status of background jobs and the engine. |
|
|
88
91
|
| `samarthya stop` | Gracefully shut down all background autonomous agents. |
|
|
89
92
|
| `samarthya model` | Swap between LLM providers (e.g. `ollama`, `gemini`). |
|
|
90
|
-
| `samarthya tunnel` | Expose gateway to internet
|
|
93
|
+
| `samarthya tunnel` | Expose gateway to internet. **(Must run in a separate terminal)** |
|
|
91
94
|
|
|
92
95
|
---
|
|
93
96
|
|
|
@@ -113,11 +116,10 @@ ACTIVE_PROVIDER=gemini # or ollama
|
|
|
113
116
|
### 📱 Telegram Integration
|
|
114
117
|
To connect SamarthyaBot to Telegram:
|
|
115
118
|
1. Get a bot token from [@BotFather](https://t.me/BotFather).
|
|
116
|
-
2.
|
|
117
|
-
3. Run `samarthya
|
|
118
|
-
4.
|
|
119
|
-
|
|
120
|
-
*Note: The `samarthya onboard` command handles these for you automatically!*
|
|
119
|
+
2. The `samarthya onboard` wizard will automatically ask for this token.
|
|
120
|
+
3. Run `samarthya gateway` in your **first terminal**.
|
|
121
|
+
4. Run `samarthya tunnel` in a **new, separate terminal**.
|
|
122
|
+
5. Samarthya will automatically create a secure tunnel and link your bot!
|
|
121
123
|
|
|
122
124
|
---
|
|
123
125
|
|
package/backend/bin/samarthya.js
CHANGED
|
@@ -12,8 +12,15 @@ const backendDir = path.join(__dirname, '..');
|
|
|
12
12
|
// Helper to check if server is already running on port 5000
|
|
13
13
|
const isServerRunning = () => {
|
|
14
14
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if (process.platform === 'win32') {
|
|
16
|
+
const output = execSync('netstat -ano | findstr :5000', { encoding: 'utf-8' });
|
|
17
|
+
return output.includes('LISTENING');
|
|
18
|
+
} else {
|
|
19
|
+
// macOS and Linux
|
|
20
|
+
// Using lsof as it's more standard across unix than fuser
|
|
21
|
+
execSync('lsof -i:5000 -t 2>/dev/null');
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
17
24
|
} catch {
|
|
18
25
|
return false;
|
|
19
26
|
}
|
|
@@ -79,6 +86,7 @@ switch (command) {
|
|
|
79
86
|
const anthropicKey = await question('🔑 Enter Anthropic (Claude) API Key (or press Enter to skip): ');
|
|
80
87
|
const groqKey = await question('🔑 Enter Groq API Key (or press Enter to skip): ');
|
|
81
88
|
const openAiKey = await question('🔑 Enter OpenAI API Key (or press Enter to skip): ');
|
|
89
|
+
const telegramToken = await question('🤖 Enter Telegram Bot Token (or press Enter to skip): ');
|
|
82
90
|
|
|
83
91
|
const envPath = path.join(backendDir, '.env');
|
|
84
92
|
let envVars = {};
|
|
@@ -113,6 +121,7 @@ switch (command) {
|
|
|
113
121
|
if (anthropicKey.trim()) envVars['ANTHROPIC_API_KEY'] = anthropicKey.trim();
|
|
114
122
|
if (groqKey.trim()) envVars['GROQ_API_KEY'] = groqKey.trim();
|
|
115
123
|
if (openAiKey.trim()) envVars['OPENAI_API_KEY'] = openAiKey.trim();
|
|
124
|
+
if (telegramToken.trim()) envVars['TELEGRAM_BOT_TOKEN'] = telegramToken.trim();
|
|
116
125
|
|
|
117
126
|
if (!envVars['GEMINI_API_KEY']) envVars['GEMINI_API_KEY'] = 'dummy';
|
|
118
127
|
|
|
@@ -297,7 +306,11 @@ switch (command) {
|
|
|
297
306
|
|
|
298
307
|
try { require('dotenv').config({ path: path.join(backendDir, '.env') }); } catch (e) { }
|
|
299
308
|
|
|
300
|
-
const
|
|
309
|
+
const isWindows = process.platform === 'win32';
|
|
310
|
+
const tunnelProcess = spawn('npm', ['exec', 'localtunnel', '--', '--port', '5000'], {
|
|
311
|
+
stdio: 'pipe',
|
|
312
|
+
shell: isWindows
|
|
313
|
+
});
|
|
301
314
|
|
|
302
315
|
tunnelProcess.stdout.on('data', async (data) => {
|
|
303
316
|
const output = data.toString();
|
|
@@ -346,7 +359,23 @@ switch (command) {
|
|
|
346
359
|
if (isServerRunning()) {
|
|
347
360
|
console.log('🛑 Stopping SamarthyaBot Gateway...');
|
|
348
361
|
try {
|
|
349
|
-
|
|
362
|
+
if (process.platform === 'win32') {
|
|
363
|
+
// Find PID of process listening on port 5000 and kill it
|
|
364
|
+
const netstatOut = execSync('netstat -ano | findstr :5000', { encoding: 'utf-8' });
|
|
365
|
+
const lines = netstatOut.split('\\n');
|
|
366
|
+
for (const line of lines) {
|
|
367
|
+
if (line.includes('LISTENING')) {
|
|
368
|
+
const parts = line.trim().split(/\\s+/);
|
|
369
|
+
const pid = parts[parts.length - 1];
|
|
370
|
+
if (pid && pid !== '0') {
|
|
371
|
+
execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
// macOS and Linux
|
|
377
|
+
execSync('lsof -t -i:5000 | xargs kill -9 2>/dev/null || fuser -k 5000/tcp 2>/dev/null');
|
|
378
|
+
}
|
|
350
379
|
console.log('✅ Gateway stopped successfully.');
|
|
351
380
|
} catch (e) {
|
|
352
381
|
console.log('❌ Failed to stop gateway gracefully. Process might already be dead.');
|
|
@@ -360,7 +389,21 @@ switch (command) {
|
|
|
360
389
|
console.log('🔄 Restarting SamarthyaBot Gateway...');
|
|
361
390
|
if (isServerRunning()) {
|
|
362
391
|
try {
|
|
363
|
-
|
|
392
|
+
if (process.platform === 'win32') {
|
|
393
|
+
const netstatOut = execSync('netstat -ano | findstr :5000', { encoding: 'utf-8' });
|
|
394
|
+
const lines = netstatOut.split('\\n');
|
|
395
|
+
for (const line of lines) {
|
|
396
|
+
if (line.includes('LISTENING')) {
|
|
397
|
+
const parts = line.trim().split(/\\s+/);
|
|
398
|
+
const pid = parts[parts.length - 1];
|
|
399
|
+
if (pid && pid !== '0') {
|
|
400
|
+
execSync(`taskkill /PID ${pid} /F`, { stdio: 'ignore' });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
execSync('lsof -t -i:5000 | xargs kill -9 2>/dev/null || fuser -k 5000/tcp 2>/dev/null');
|
|
406
|
+
}
|
|
364
407
|
} catch (e) { /* ignore */ }
|
|
365
408
|
}
|
|
366
409
|
// Give it a moment to free the port
|
|
@@ -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": [
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"samarthya-bot",
|
|
38
39
|
"ollama"
|
|
39
40
|
],
|
|
40
|
-
"author": "Bishnu Sahu",
|
|
41
|
+
"author": "Bishnu Prasad Sahu",
|
|
41
42
|
"license": "MIT",
|
|
42
43
|
"engines": {
|
|
43
44
|
"node": ">=20.0.0"
|
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
|