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.
@@ -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
- // ─────────── RUN COMMAND (Real FULL ROOT/OS EXECUTION) ───────────
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 ANY shell command on the host OS. Warning: Full access provided. Use carefully.',
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
- // ─────────── OPEN URL (Real browser open) ───────────
689
- open_url: {
690
- name: 'open_url',
691
- description: 'Open a URL in the default browser',
692
- descriptionHi: 'ब्राउज़र में URL खोलें',
693
- riskLevel: 'medium',
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 open' }
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 cmd = `xdg-open "${args.url}" 2>/dev/null || open "${args.url}" 2>/dev/null`;
701
- exec(cmd, { timeout: 5000 }, (error) => {
702
- if (error) {
703
- resolve({
704
- success: false,
705
- result: `❌ Could not open browser: ${error.message}\n\n🔗 URL: ${args.url}`
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
- resolve({
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.2",
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
@@ -0,0 +1,3 @@
1
+ module samarthya-worker
2
+
3
+ go 1.22.2
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