samarthya-bot 2.2.1 → 2.3.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/CHANGELOG.md +25 -0
- package/README.md +340 -353
- package/backend/bin/samarthya.js +29 -10
- package/backend/config/constants.js +4 -4
- package/backend/controllers/chatController.js +72 -0
- package/backend/controllers/telegramController.js +8 -0
- package/backend/package.json +1 -1
- package/backend/public/assets/index-DiMx9ERJ.css +1 -0
- package/backend/public/assets/index-Dn5WYZTH.js +32 -0
- package/backend/public/index.html +186 -16
- package/backend/public/robots.txt +32 -0
- package/backend/public/sitemap.xml +39 -0
- package/backend/services/agent/commandService.js +168 -0
- package/backend/services/llm/llmService.js +8 -0
- package/backend/services/planner/plannerService.js +17 -0
- package/backend/services/security/sandboxService.js +11 -4
- package/backend/services/system/platform.js +150 -0
- package/backend/services/tools/toolRegistry.js +521 -36
- package/backend/services/worker/workerClient.js +141 -29
- package/package.json +1 -1
- package/backend/public/assets/index-6PCzI3K2.js +0 -40
- package/backend/public/assets/index-6TF5jVRQ.js +0 -149
- package/backend/public/assets/index-B0U7rt6f.js +0 -46
- package/backend/public/assets/index-BF0RZh9i.js +0 -149
- package/backend/public/assets/index-BFRAq8Y1.js +0 -149
- package/backend/public/assets/index-CGw8cc8z.js +0 -149
- package/backend/public/assets/index-Ckf0GO1B.css +0 -1
- package/backend/public/assets/index-Cx0Ei-z7.js +0 -149
- package/backend/public/assets/index-DIPdcLv-.js +0 -25
- package/backend/public/assets/index-Da1E-MYB.js +0 -53
- package/backend/public/assets/index-DdCKkq38.js +0 -149
- package/backend/public/assets/index-Do4jNsZS.js +0 -19
- package/backend/public/assets/index-DyjpBYmS.js +0 -51
- package/backend/public/assets/index-DzlXcaXT.js +0 -149
- package/backend/public/assets/index-J7XSVHCz.css +0 -1
- package/backend/public/assets/index-Ui-pyZvK.js +0 -25
- package/backend/public/assets/index-kzffNwzo.js +0 -149
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
const { TOOL_PACKS } = require('../../config/constants');
|
|
2
2
|
const fs = require('fs').promises;
|
|
3
|
+
const fsSync = require('fs');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
const { exec } = require('child_process');
|
|
6
7
|
const nodemailer = require('nodemailer');
|
|
7
8
|
const { Client } = require('ssh2');
|
|
8
9
|
const workerClient = require('../worker/workerClient');
|
|
10
|
+
const sandbox = require('../security/sandboxService');
|
|
11
|
+
const platform = require('../system/platform');
|
|
9
12
|
|
|
10
13
|
// ────────────────────────────────────────────────────────────
|
|
11
14
|
// REAL Tool Definitions — No more simulations!
|
|
@@ -13,9 +16,68 @@ const workerClient = require('../worker/workerClient');
|
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Security: Command Blacklist Map
|
|
16
|
-
* Prevent the AI (or user injection) from autonomously executing critically
|
|
19
|
+
* Prevent the AI (or user injection) from autonomously executing critically
|
|
20
|
+
* destructive host/remote commands. Covers both *nix and Windows.
|
|
21
|
+
* NOTE: anchored to the start of each (trimmed) command SEGMENT — see
|
|
22
|
+
* assertCommandSafe(), which splits chained commands so that something like
|
|
23
|
+
* `echo hi && rm -rf /` cannot slip past the blacklist.
|
|
17
24
|
*/
|
|
18
|
-
const BLOCKED_COMMANDS = /^(rm\s+-rf|rmdir|mkfs|dd|fdisk|shutdown|reboot|poweroff|halt|init|killall\s+-9|wget
|
|
25
|
+
const BLOCKED_COMMANDS = /^(sudo\s+)?(rm\s+-[a-z]*[rf]|rmdir|mkfs|dd\b|fdisk|shutdown|reboot|poweroff|halt|init\s+[06]|killall\s+-9|wget\s+.*\.sh|curl\s+.*\.sh|chmod\s+-R\s+777|chown\s+-R.*:.*\/|del\s+\/|rd\s+\/s|format\s+[a-z]:|diskpart|:\(\)\s*\{)/im;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Split a raw command into individually-checkable segments so chained /
|
|
29
|
+
* piped / substituted commands are all validated, not just the first one.
|
|
30
|
+
*/
|
|
31
|
+
function splitCommandSegments(command) {
|
|
32
|
+
return String(command)
|
|
33
|
+
// Split on every shell separator/substitution: && || ; | & newline $( `
|
|
34
|
+
.split(/&&|\|\||[;\n\r&|]|\$\(|`/)
|
|
35
|
+
.map(s => s.trim())
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Unified command safety gate. Returns { ok: boolean, reason?: string }.
|
|
41
|
+
* Runs every segment through the blacklist AND the workspace sandbox.
|
|
42
|
+
*/
|
|
43
|
+
function assertCommandSafe(command, { checkWorkspace = true } = {}) {
|
|
44
|
+
if (!command || typeof command !== 'string') {
|
|
45
|
+
return { ok: false, reason: '❌ No command provided.' };
|
|
46
|
+
}
|
|
47
|
+
for (const segment of splitCommandSegments(command)) {
|
|
48
|
+
if (BLOCKED_COMMANDS.test(segment)) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
reason: `❌ Security Block: "${segment}" matches a restricted pattern (recursive delete, disk format, reboot, fork-bomb, remote shell download, etc.).`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Workspace + dangerous-pattern checks (fork bombs, /dev/sd*, etc.)
|
|
56
|
+
// Skipped for remote (SSH) targets, whose filesystem layout differs.
|
|
57
|
+
if (checkWorkspace) {
|
|
58
|
+
const sandboxCheck = sandbox.validateCommand(command);
|
|
59
|
+
if (!sandboxCheck.allowed) {
|
|
60
|
+
return { ok: false, reason: sandboxCheck.reason };
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// Still apply the always-on dangerous-pattern guard for remote hosts.
|
|
64
|
+
for (const pattern of sandbox.blockedPatterns) {
|
|
65
|
+
if (pattern.test(command)) {
|
|
66
|
+
return { ok: false, reason: `🛡️ Safety Guard: remote command blocked — dangerous pattern detected.` };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { ok: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Workspace path safety gate for file operations. Honours
|
|
75
|
+
* RESTRICT_TO_WORKSPACE (disabled => always allowed).
|
|
76
|
+
*/
|
|
77
|
+
function assertPathSafe(filePath) {
|
|
78
|
+
const check = sandbox.validatePath(filePath);
|
|
79
|
+
return { ok: check.allowed, reason: check.reason };
|
|
80
|
+
}
|
|
19
81
|
|
|
20
82
|
/**
|
|
21
83
|
* Safe directory — tools can only operate within this sandbox
|
|
@@ -205,8 +267,12 @@ const toolDefinitions = {
|
|
|
205
267
|
// Normalize: LLM may use file_path, filename, file_name, etc.
|
|
206
268
|
const argPath = args.path || args.file_path || args.filename || args.file_name || args.file;
|
|
207
269
|
if (!argPath) return { success: false, result: '❌ Please provide a file path/name.' };
|
|
208
|
-
//
|
|
209
|
-
let filePath =
|
|
270
|
+
// Resolve absolute (cross-platform) vs. workspace-relative paths
|
|
271
|
+
let filePath = path.isAbsolute(argPath) ? path.resolve(argPath) : path.resolve(SAFE_DIR, argPath);
|
|
272
|
+
|
|
273
|
+
// Enforce workspace sandbox (no-op when RESTRICT_TO_WORKSPACE=false)
|
|
274
|
+
const guard = assertPathSafe(filePath);
|
|
275
|
+
if (!guard.ok) return { success: false, result: guard.reason };
|
|
210
276
|
|
|
211
277
|
const stat = await fs.stat(filePath);
|
|
212
278
|
if (stat.isDirectory()) {
|
|
@@ -264,8 +330,12 @@ const toolDefinitions = {
|
|
|
264
330
|
// Normalize: LLM may use file_path, filename, etc.
|
|
265
331
|
const argPath = args.path || args.file_path || args.filename || args.file_name || args.file;
|
|
266
332
|
if (!argPath) return { success: false, result: '❌ Please provide a file path/name.' };
|
|
267
|
-
//
|
|
268
|
-
let filePath =
|
|
333
|
+
// Resolve absolute (cross-platform) vs. workspace-relative paths
|
|
334
|
+
let filePath = path.isAbsolute(argPath) ? path.resolve(argPath) : path.resolve(SAFE_DIR, argPath);
|
|
335
|
+
|
|
336
|
+
// Enforce workspace sandbox (no-op when RESTRICT_TO_WORKSPACE=false)
|
|
337
|
+
const guard = assertPathSafe(filePath);
|
|
338
|
+
if (!guard.ok) return { success: false, result: guard.reason };
|
|
269
339
|
|
|
270
340
|
// Create subdirectories if needed
|
|
271
341
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
@@ -297,7 +367,13 @@ const toolDefinitions = {
|
|
|
297
367
|
execute: async (args, userContext) => {
|
|
298
368
|
try {
|
|
299
369
|
const SAFE_DIR = await getSafeDir(userContext);
|
|
300
|
-
const targetDir = args.path
|
|
370
|
+
const targetDir = args.path
|
|
371
|
+
? (path.isAbsolute(args.path) ? path.resolve(args.path) : path.resolve(SAFE_DIR, args.path))
|
|
372
|
+
: SAFE_DIR;
|
|
373
|
+
|
|
374
|
+
// Enforce workspace sandbox (no-op when RESTRICT_TO_WORKSPACE=false)
|
|
375
|
+
const guard = assertPathSafe(targetDir);
|
|
376
|
+
if (!guard.ok) return { success: false, result: guard.reason };
|
|
301
377
|
|
|
302
378
|
const files = await fs.readdir(targetDir, { withFileTypes: true });
|
|
303
379
|
if (files.length === 0) {
|
|
@@ -503,7 +579,7 @@ const toolDefinitions = {
|
|
|
503
579
|
// ─────────── DEVOPS / STREAMING EXECUTION (Go Worker Integration) ───────────
|
|
504
580
|
devops_execute_stream: {
|
|
505
581
|
name: 'devops_execute_stream',
|
|
506
|
-
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
|
|
582
|
+
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 Go micro-worker when available, with an automatic native fallback on every OS. Host OS: ${platform.describe()}. Use commands valid for this OS.`,
|
|
507
583
|
descriptionHi: 'लाइव शेल कमांड चलाएं',
|
|
508
584
|
riskLevel: 'critical',
|
|
509
585
|
category: 'system',
|
|
@@ -512,9 +588,8 @@ const toolDefinitions = {
|
|
|
512
588
|
dir: { type: 'string', required: false, description: 'Working directory for the command' }
|
|
513
589
|
},
|
|
514
590
|
execute: async (args, userContext) => {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
591
|
+
const safe = assertCommandSafe(args.command);
|
|
592
|
+
if (!safe.ok) return { success: false, result: safe.reason };
|
|
518
593
|
|
|
519
594
|
return new Promise((resolve) => {
|
|
520
595
|
let liveLog = '';
|
|
@@ -549,20 +624,20 @@ const toolDefinitions = {
|
|
|
549
624
|
// ─────────── LEGACY RUN COMMAND (Kept for instant small commands) ───────────
|
|
550
625
|
run_command: {
|
|
551
626
|
name: 'run_command',
|
|
552
|
-
description: `Run basic, instant shell commands natively. Host OS: ${
|
|
627
|
+
description: `Run basic, instant shell commands natively. Host OS: ${platform.describe()}. IMPORTANT — use commands valid for this OS: on Windows use 'dir', 'type', 'copy'; on macOS/Linux use 'ls', 'cat', 'cp'. To open a URL/file/app prefer the 'open_path' tool (it auto-picks start/open/xdg-open).`,
|
|
553
628
|
descriptionHi: 'शेल कमांड चलाएं',
|
|
554
629
|
riskLevel: 'critical',
|
|
555
630
|
category: 'system',
|
|
556
631
|
parameters: {
|
|
557
|
-
command: { type: 'string', required: true, description: 'Shell command to execute' }
|
|
632
|
+
command: { type: 'string', required: true, description: 'Shell command to execute (must match the host OS)' }
|
|
558
633
|
},
|
|
559
634
|
execute: async (args, userContext) => {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
635
|
+
const safe = assertCommandSafe(args.command);
|
|
636
|
+
if (!safe.ok) return { success: false, result: safe.reason };
|
|
563
637
|
|
|
638
|
+
const { shell } = platform.getShell();
|
|
564
639
|
return new Promise((resolve) => {
|
|
565
|
-
exec(args.command, { timeout: 15000, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
|
|
640
|
+
exec(args.command, { timeout: 15000, maxBuffer: 1024 * 1024, shell, windowsHide: true }, (error, stdout, stderr) => {
|
|
566
641
|
if (error) {
|
|
567
642
|
resolve({
|
|
568
643
|
success: false,
|
|
@@ -594,9 +669,8 @@ const toolDefinitions = {
|
|
|
594
669
|
command: { type: 'string', required: true, description: 'Command string to run on the remote VPS (e.g., "cd /var/www && git pull")' }
|
|
595
670
|
},
|
|
596
671
|
execute: async (args, userContext) => {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
672
|
+
const safe = assertCommandSafe(args.command, { checkWorkspace: false });
|
|
673
|
+
if (!safe.ok) return { success: false, result: safe.reason };
|
|
600
674
|
|
|
601
675
|
return new Promise(async (resolve) => {
|
|
602
676
|
const conn = new Client();
|
|
@@ -845,29 +919,21 @@ const toolDefinitions = {
|
|
|
845
919
|
let log = `🌐 **Browser Automation Started:** ${args.url}\n`;
|
|
846
920
|
|
|
847
921
|
try {
|
|
848
|
-
//
|
|
849
|
-
|
|
850
|
-
let execPaths = [];
|
|
851
|
-
if (os.platform() === 'win32') {
|
|
852
|
-
execPaths = [
|
|
853
|
-
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
854
|
-
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'
|
|
855
|
-
];
|
|
856
|
-
} else if (os.platform() === 'darwin') {
|
|
857
|
-
execPaths = ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'];
|
|
858
|
-
} else {
|
|
859
|
-
execPaths = ['/usr/bin/google-chrome', '/usr/bin/chromium-browser'];
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
let validPath = execPaths.find(p => require('fs').existsSync(p));
|
|
922
|
+
// Discover an installed Chromium-based browser across Windows/macOS/Linux.
|
|
923
|
+
const validPath = platform.findBrowser();
|
|
863
924
|
if (!validPath) {
|
|
864
|
-
return resolve({
|
|
925
|
+
return resolve({
|
|
926
|
+
success: false,
|
|
927
|
+
result: `❌ No Chrome/Edge/Brave/Chromium browser found for Puppeteer on this ${platform.OS} machine.\n\n💡 Install Google Chrome, or set CHROME_PATH in your .env to the browser executable path.`
|
|
928
|
+
});
|
|
865
929
|
}
|
|
866
930
|
|
|
867
931
|
browser = await puppeteer.launch({
|
|
868
932
|
executablePath: validPath,
|
|
869
933
|
headless: false, // Make it visible to the user!
|
|
870
934
|
defaultViewport: null,
|
|
935
|
+
// --no-sandbox is required on many Linux servers / containers.
|
|
936
|
+
args: platform.isLinux ? ['--no-sandbox', '--disable-setuid-sandbox', '--start-maximized'] : ['--start-maximized'],
|
|
871
937
|
});
|
|
872
938
|
|
|
873
939
|
const page = await browser.newPage();
|
|
@@ -986,6 +1052,425 @@ const toolDefinitions = {
|
|
|
986
1052
|
result: `🧪 **TASK SIMULATION MODE ACTIVE**\n\nI have analyzed the request: "${args.taskDecription}"\n\nIf executed, the following sequence would occur:\n1. 🔍 Validation step\n2. 📄 File operations (Safe mode preview)\n3. 📤 Network request (Dry run)\n\n_No actual system state was changed._`
|
|
987
1053
|
};
|
|
988
1054
|
}
|
|
1055
|
+
},
|
|
1056
|
+
|
|
1057
|
+
// ─────────── OPEN PATH/URL/APP (Cross-platform: start | open | xdg-open) ───────────
|
|
1058
|
+
open_path: {
|
|
1059
|
+
name: 'open_path',
|
|
1060
|
+
description: 'Open a URL, file, folder or application with the system default handler. Auto-selects the right command for the OS (Windows: start, macOS: open, Linux: xdg-open). Use this instead of guessing OS-specific open commands.',
|
|
1061
|
+
descriptionHi: 'URL/फ़ाइल/ऐप खोलें',
|
|
1062
|
+
riskLevel: 'medium',
|
|
1063
|
+
category: 'system',
|
|
1064
|
+
parameters: {
|
|
1065
|
+
target: { type: 'string', required: true, description: 'A URL (https://...), an absolute file/folder path, or an application name' }
|
|
1066
|
+
},
|
|
1067
|
+
execute: async (args, userContext) => {
|
|
1068
|
+
const target = String(args.target || args.url || args.path || args.app || '');
|
|
1069
|
+
if (!target) return { success: false, result: '❌ Please provide a URL, file path or app to open.' };
|
|
1070
|
+
|
|
1071
|
+
// SECURITY: reject shell metacharacters. We spawn WITHOUT a shell
|
|
1072
|
+
// (args array), but this is defense-in-depth against any handler
|
|
1073
|
+
// that re-parses (e.g. cmd.exe `start`) and command substitution.
|
|
1074
|
+
if (/[`$;&|<>(){}\n\r"'\\]/.test(target)) {
|
|
1075
|
+
return { success: false, result: '❌ Refused: the target contains characters that are not allowed for safety.' };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const { spawn } = require('child_process');
|
|
1079
|
+
// Build [binary, ...args] with NO shell interpolation.
|
|
1080
|
+
let bin, spawnArgs;
|
|
1081
|
+
if (platform.isWindows) {
|
|
1082
|
+
bin = 'cmd';
|
|
1083
|
+
spawnArgs = ['/c', 'start', '', target]; // empty title arg required by start
|
|
1084
|
+
} else if (platform.isMac) {
|
|
1085
|
+
bin = 'open';
|
|
1086
|
+
spawnArgs = [target];
|
|
1087
|
+
} else {
|
|
1088
|
+
bin = 'xdg-open';
|
|
1089
|
+
spawnArgs = [target];
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return new Promise((resolve) => {
|
|
1093
|
+
let child;
|
|
1094
|
+
try {
|
|
1095
|
+
child = spawn(bin, spawnArgs, { windowsHide: true, stdio: 'ignore' });
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
return resolve({ success: false, result: `❌ Could not open "${target}" on ${platform.OS}: ${err.message}` });
|
|
1098
|
+
}
|
|
1099
|
+
const timer = setTimeout(() => { try { child.kill(); } catch (_) {} resolve({ success: true, result: `🚀 Opened **${target}** (${platform.OS}).` }); }, 8000);
|
|
1100
|
+
child.on('error', (err) => { clearTimeout(timer); resolve({ success: false, result: `❌ Could not open "${target}" on ${platform.OS}: ${err.message}` }); });
|
|
1101
|
+
child.on('spawn', () => { clearTimeout(timer); resolve({ success: true, result: `🚀 Opened **${target}** using the system default handler (${platform.OS}).` }); });
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
},
|
|
1105
|
+
|
|
1106
|
+
// ─────────── HTTP REQUEST (Generic REST/API caller) ───────────
|
|
1107
|
+
http_request: {
|
|
1108
|
+
name: 'http_request',
|
|
1109
|
+
description: 'Make an HTTP request to any REST API or webhook and return the response. Supports GET/POST/PUT/PATCH/DELETE, custom headers and JSON body.',
|
|
1110
|
+
descriptionHi: 'API रिक्वेस्ट भेजें',
|
|
1111
|
+
riskLevel: 'medium',
|
|
1112
|
+
category: 'tool',
|
|
1113
|
+
parameters: {
|
|
1114
|
+
url: { type: 'string', required: true, description: 'Full URL (must start with http:// or https://)' },
|
|
1115
|
+
method: { type: 'string', required: false, description: 'HTTP method (GET default)' },
|
|
1116
|
+
headers: { type: 'string', required: false, description: 'JSON object of request headers' },
|
|
1117
|
+
body: { type: 'string', required: false, description: 'Request body (JSON string or raw text)' }
|
|
1118
|
+
},
|
|
1119
|
+
execute: async (args, userContext) => {
|
|
1120
|
+
try {
|
|
1121
|
+
const url = args.url;
|
|
1122
|
+
if (!/^https?:\/\//i.test(url || '')) {
|
|
1123
|
+
return { success: false, result: '❌ URL must start with http:// or https://' };
|
|
1124
|
+
}
|
|
1125
|
+
const method = (args.method || 'GET').toUpperCase();
|
|
1126
|
+
let headers = {};
|
|
1127
|
+
if (args.headers) {
|
|
1128
|
+
try { headers = typeof args.headers === 'string' ? JSON.parse(args.headers) : args.headers; } catch (e) { /* ignore bad headers */ }
|
|
1129
|
+
}
|
|
1130
|
+
const opts = { method, headers };
|
|
1131
|
+
if (args.body && method !== 'GET' && method !== 'HEAD') {
|
|
1132
|
+
opts.body = typeof args.body === 'string' ? args.body : JSON.stringify(args.body);
|
|
1133
|
+
if (!Object.keys(headers).some(h => h.toLowerCase() === 'content-type')) {
|
|
1134
|
+
headers['Content-Type'] = 'application/json';
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const controller = new AbortController();
|
|
1139
|
+
const timer = setTimeout(() => controller.abort(), 20000);
|
|
1140
|
+
const res = await fetch(url, { ...opts, signal: controller.signal });
|
|
1141
|
+
clearTimeout(timer);
|
|
1142
|
+
|
|
1143
|
+
const text = await res.text();
|
|
1144
|
+
const preview = text.length > 3000 ? text.slice(0, 3000) + '\n…(truncated)' : text;
|
|
1145
|
+
return {
|
|
1146
|
+
success: res.ok,
|
|
1147
|
+
result: `🌐 **${method} ${url}**\nStatus: ${res.status} ${res.statusText}\n\n\`\`\`\n${preview || '(empty body)'}\n\`\`\``
|
|
1148
|
+
};
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
return { success: false, result: `❌ HTTP request failed: ${error.message}` };
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
},
|
|
1154
|
+
|
|
1155
|
+
// ─────────── SECURE PASSWORD / TOKEN GENERATOR ───────────
|
|
1156
|
+
password_generate: {
|
|
1157
|
+
name: 'password_generate',
|
|
1158
|
+
description: 'Generate a cryptographically secure random password, API token or secret key.',
|
|
1159
|
+
descriptionHi: 'सुरक्षित पासवर्ड बनाएं',
|
|
1160
|
+
riskLevel: 'low',
|
|
1161
|
+
category: 'tool',
|
|
1162
|
+
parameters: {
|
|
1163
|
+
length: { type: 'number', required: false, description: 'Length of the password (default 20, max 128)' },
|
|
1164
|
+
symbols: { type: 'boolean', required: false, description: 'Include symbols (default true)' }
|
|
1165
|
+
},
|
|
1166
|
+
execute: async (args, userContext) => {
|
|
1167
|
+
try {
|
|
1168
|
+
const crypto = require('crypto');
|
|
1169
|
+
const length = Math.min(Math.max(parseInt(args.length) || 20, 8), 128);
|
|
1170
|
+
const useSymbols = args.symbols !== false && args.symbols !== 'false';
|
|
1171
|
+
const lower = 'abcdefghijklmnopqrstuvwxyz';
|
|
1172
|
+
const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
1173
|
+
const digits = '0123456789';
|
|
1174
|
+
const symbols = '!@#$%^&*()-_=+[]{}<>?';
|
|
1175
|
+
const charset = lower + upper + digits + (useSymbols ? symbols : '');
|
|
1176
|
+
|
|
1177
|
+
const bytes = crypto.randomBytes(length);
|
|
1178
|
+
let pwd = '';
|
|
1179
|
+
for (let i = 0; i < length; i++) pwd += charset[bytes[i] % charset.length];
|
|
1180
|
+
|
|
1181
|
+
return {
|
|
1182
|
+
success: true,
|
|
1183
|
+
result: `🔐 **Secure ${length}-char password generated:**\n\n\`${pwd}\`\n\n_Generated with crypto.randomBytes — never stored, never logged._`
|
|
1184
|
+
};
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
return { success: false, result: `❌ Password generation failed: ${error.message}` };
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
},
|
|
1190
|
+
|
|
1191
|
+
// ─────────── QR CODE GENERATOR (great for UPI links) ───────────
|
|
1192
|
+
qr_generate: {
|
|
1193
|
+
name: 'qr_generate',
|
|
1194
|
+
description: 'Generate a QR code image URL for any text, link or UPI payment string. Returns a ready-to-share PNG link.',
|
|
1195
|
+
descriptionHi: 'QR कोड बनाएं',
|
|
1196
|
+
riskLevel: 'low',
|
|
1197
|
+
category: 'tool',
|
|
1198
|
+
parameters: {
|
|
1199
|
+
data: { type: 'string', required: true, description: 'Text/URL/UPI string to encode' },
|
|
1200
|
+
size: { type: 'number', required: false, description: 'Image size in px (default 300)' }
|
|
1201
|
+
},
|
|
1202
|
+
execute: async (args) => {
|
|
1203
|
+
const data = args.data || args.text || args.url;
|
|
1204
|
+
if (!data) return { success: false, result: '❌ Provide data to encode.' };
|
|
1205
|
+
const size = Math.min(Math.max(parseInt(args.size) || 300, 100), 1000);
|
|
1206
|
+
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodeURIComponent(data)}`;
|
|
1207
|
+
return {
|
|
1208
|
+
success: true,
|
|
1209
|
+
result: `📱 **QR Code generated!**\n\nEncodes: \`${data}\`\n\n🔗 Image: ${qrUrl}\n\n_Open the link to view/download the QR._`
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
|
|
1214
|
+
// ─────────── CURRENCY CONVERTER (free, no key) ───────────
|
|
1215
|
+
currency_convert: {
|
|
1216
|
+
name: 'currency_convert',
|
|
1217
|
+
description: 'Convert an amount between currencies using live exchange rates (e.g. USD to INR).',
|
|
1218
|
+
descriptionHi: 'करेंसी कन्वर्ट करें',
|
|
1219
|
+
riskLevel: 'low',
|
|
1220
|
+
category: 'search',
|
|
1221
|
+
parameters: {
|
|
1222
|
+
amount: { type: 'number', required: true, description: 'Amount to convert' },
|
|
1223
|
+
from: { type: 'string', required: true, description: 'Source currency code (e.g. USD)' },
|
|
1224
|
+
to: { type: 'string', required: true, description: 'Target currency code (e.g. INR)' }
|
|
1225
|
+
},
|
|
1226
|
+
execute: async (args) => {
|
|
1227
|
+
try {
|
|
1228
|
+
const from = String(args.from || 'USD').toUpperCase();
|
|
1229
|
+
const to = String(args.to || 'INR').toUpperCase();
|
|
1230
|
+
const amount = Number(args.amount) || 0;
|
|
1231
|
+
const res = await fetch(`https://open.er-api.com/v6/latest/${from}`);
|
|
1232
|
+
const data = await res.json();
|
|
1233
|
+
if (data.result !== 'success' || !data.rates?.[to]) {
|
|
1234
|
+
return { success: false, result: `❌ Could not get rate for ${from}→${to}.` };
|
|
1235
|
+
}
|
|
1236
|
+
const rate = data.rates[to];
|
|
1237
|
+
const converted = (amount * rate).toFixed(2);
|
|
1238
|
+
return {
|
|
1239
|
+
success: true,
|
|
1240
|
+
result: `💱 **${amount} ${from} = ${converted} ${to}**\n\n📈 Rate: 1 ${from} = ${rate} ${to}\n🕐 As of ${data.time_last_update_utc || 'now'}`
|
|
1241
|
+
};
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
return { success: false, result: `❌ Currency conversion failed: ${error.message}` };
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
},
|
|
1247
|
+
|
|
1248
|
+
// ─────────── CRYPTO PRICE (CoinGecko, free) ───────────
|
|
1249
|
+
crypto_price: {
|
|
1250
|
+
name: 'crypto_price',
|
|
1251
|
+
description: 'Get the current price of a cryptocurrency (e.g. bitcoin, ethereum) in INR/USD.',
|
|
1252
|
+
descriptionHi: 'क्रिप्टो कीमत जानें',
|
|
1253
|
+
riskLevel: 'low',
|
|
1254
|
+
category: 'search',
|
|
1255
|
+
parameters: {
|
|
1256
|
+
coin: { type: 'string', required: true, description: 'Coin id (e.g. bitcoin, ethereum, dogecoin)' },
|
|
1257
|
+
currency: { type: 'string', required: false, description: 'Fiat currency (default inr)' }
|
|
1258
|
+
},
|
|
1259
|
+
execute: async (args) => {
|
|
1260
|
+
try {
|
|
1261
|
+
const coin = String(args.coin || 'bitcoin').toLowerCase().replace(/\s+/g, '-');
|
|
1262
|
+
const vs = String(args.currency || 'inr').toLowerCase();
|
|
1263
|
+
const res = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(coin)}&vs_currencies=${encodeURIComponent(vs)}&include_24hr_change=true`);
|
|
1264
|
+
const data = await res.json();
|
|
1265
|
+
if (!data[coin] || data[coin][vs] === undefined) {
|
|
1266
|
+
return { success: false, result: `❌ Coin "${coin}" not found. Use ids like bitcoin, ethereum, solana.` };
|
|
1267
|
+
}
|
|
1268
|
+
const price = data[coin][vs];
|
|
1269
|
+
const change = data[coin][`${vs}_24h_change`];
|
|
1270
|
+
const arrow = change >= 0 ? '🟢▲' : '🔴▼';
|
|
1271
|
+
return {
|
|
1272
|
+
success: true,
|
|
1273
|
+
result: `🪙 **${coin.toUpperCase()}**\n\n💰 Price: **${price.toLocaleString()} ${vs.toUpperCase()}**\n${arrow} 24h: ${change?.toFixed(2)}%`
|
|
1274
|
+
};
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
return { success: false, result: `❌ Crypto price failed: ${error.message}` };
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
},
|
|
1280
|
+
|
|
1281
|
+
// ─────────── URL SHORTENER (is.gd, free) ───────────
|
|
1282
|
+
url_shorten: {
|
|
1283
|
+
name: 'url_shorten',
|
|
1284
|
+
description: 'Shorten a long URL into a compact shareable link.',
|
|
1285
|
+
descriptionHi: 'URL छोटा करें',
|
|
1286
|
+
riskLevel: 'low',
|
|
1287
|
+
category: 'tool',
|
|
1288
|
+
parameters: {
|
|
1289
|
+
url: { type: 'string', required: true, description: 'Long URL to shorten' }
|
|
1290
|
+
},
|
|
1291
|
+
execute: async (args) => {
|
|
1292
|
+
try {
|
|
1293
|
+
if (!/^https?:\/\//i.test(args.url || '')) {
|
|
1294
|
+
return { success: false, result: '❌ Provide a valid http(s) URL.' };
|
|
1295
|
+
}
|
|
1296
|
+
const res = await fetch(`https://is.gd/create.php?format=simple&url=${encodeURIComponent(args.url)}`);
|
|
1297
|
+
const short = (await res.text()).trim();
|
|
1298
|
+
if (!short.startsWith('http')) {
|
|
1299
|
+
return { success: false, result: `❌ Shortening failed: ${short}` };
|
|
1300
|
+
}
|
|
1301
|
+
return { success: true, result: `🔗 **Short URL:** ${short}\n\nOriginal: ${args.url}` };
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
return { success: false, result: `❌ URL shorten failed: ${error.message}` };
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
},
|
|
1307
|
+
|
|
1308
|
+
// ─────────── IP GEOLOCATION (ip-api, free) ───────────
|
|
1309
|
+
ip_geolocate: {
|
|
1310
|
+
name: 'ip_geolocate',
|
|
1311
|
+
description: 'Look up geolocation, ISP and org details for an IP address (or the host public IP if none given).',
|
|
1312
|
+
descriptionHi: 'IP की लोकेशन पता करें',
|
|
1313
|
+
riskLevel: 'low',
|
|
1314
|
+
category: 'search',
|
|
1315
|
+
parameters: {
|
|
1316
|
+
ip: { type: 'string', required: false, description: 'IP address (leave empty for this machine\'s public IP)' }
|
|
1317
|
+
},
|
|
1318
|
+
execute: async (args) => {
|
|
1319
|
+
try {
|
|
1320
|
+
const ip = (args.ip || '').trim();
|
|
1321
|
+
const res = await fetch(`http://ip-api.com/json/${encodeURIComponent(ip)}`);
|
|
1322
|
+
const d = await res.json();
|
|
1323
|
+
if (d.status !== 'success') {
|
|
1324
|
+
return { success: false, result: `❌ Lookup failed: ${d.message || 'unknown'}` };
|
|
1325
|
+
}
|
|
1326
|
+
return {
|
|
1327
|
+
success: true,
|
|
1328
|
+
result: `🌍 **IP ${d.query}**\n\n📍 ${d.city}, ${d.regionName}, ${d.country}\n🛰️ ISP: ${d.isp}\n🏢 Org: ${d.org || 'N/A'}\n🕐 Timezone: ${d.timezone}\n🗺️ Lat/Lon: ${d.lat}, ${d.lon}`
|
|
1329
|
+
};
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
return { success: false, result: `❌ IP geolocation failed: ${error.message}` };
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
},
|
|
1335
|
+
|
|
1336
|
+
// ─────────── TRANSLATE TEXT (MyMemory, free — great for Hindi) ───────────
|
|
1337
|
+
translate_text: {
|
|
1338
|
+
name: 'translate_text',
|
|
1339
|
+
description: 'Translate text between languages (e.g. English to Hindi). Uses ISO codes like en, hi, ta, bn, mr.',
|
|
1340
|
+
descriptionHi: 'टेक्स्ट का अनुवाद करें',
|
|
1341
|
+
riskLevel: 'low',
|
|
1342
|
+
category: 'tool',
|
|
1343
|
+
parameters: {
|
|
1344
|
+
text: { type: 'string', required: true, description: 'Text to translate' },
|
|
1345
|
+
to: { type: 'string', required: true, description: 'Target language code (e.g. hi for Hindi)' },
|
|
1346
|
+
from: { type: 'string', required: false, description: 'Source language code (default auto/en)' }
|
|
1347
|
+
},
|
|
1348
|
+
execute: async (args) => {
|
|
1349
|
+
try {
|
|
1350
|
+
const text = args.text;
|
|
1351
|
+
if (!text) return { success: false, result: '❌ Provide text to translate.' };
|
|
1352
|
+
const from = (args.from || 'en').toLowerCase();
|
|
1353
|
+
const to = (args.to || 'hi').toLowerCase();
|
|
1354
|
+
const res = await fetch(`https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${from}|${to}`);
|
|
1355
|
+
const data = await res.json();
|
|
1356
|
+
const translated = data?.responseData?.translatedText;
|
|
1357
|
+
if (!translated) return { success: false, result: '❌ Translation failed.' };
|
|
1358
|
+
return {
|
|
1359
|
+
success: true,
|
|
1360
|
+
result: `🌐 **Translation (${from}→${to}):**\n\n${translated}`
|
|
1361
|
+
};
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
return { success: false, result: `❌ Translation failed: ${error.message}` };
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
},
|
|
1367
|
+
|
|
1368
|
+
// ─────────── CLIPBOARD COPY (cross-platform) ───────────
|
|
1369
|
+
clipboard_copy: {
|
|
1370
|
+
name: 'clipboard_copy',
|
|
1371
|
+
description: 'Copy text to the system clipboard. Works on Windows (clip), macOS (pbcopy) and Linux (xclip/xsel/wl-copy).',
|
|
1372
|
+
descriptionHi: 'क्लिपबोर्ड पर कॉपी करें',
|
|
1373
|
+
riskLevel: 'low',
|
|
1374
|
+
category: 'system',
|
|
1375
|
+
parameters: {
|
|
1376
|
+
text: { type: 'string', required: true, description: 'Text to copy to clipboard' }
|
|
1377
|
+
},
|
|
1378
|
+
execute: async (args) => {
|
|
1379
|
+
const text = args.text ?? '';
|
|
1380
|
+
return new Promise((resolve) => {
|
|
1381
|
+
let cmd;
|
|
1382
|
+
if (platform.isWindows) cmd = 'clip';
|
|
1383
|
+
else if (platform.isMac) cmd = 'pbcopy';
|
|
1384
|
+
else cmd = 'xclip -selection clipboard 2>/dev/null || xsel --clipboard --input 2>/dev/null || wl-copy';
|
|
1385
|
+
|
|
1386
|
+
const { shell } = platform.getShell();
|
|
1387
|
+
const child = exec(cmd, { shell, windowsHide: true }, (error) => {
|
|
1388
|
+
if (error) {
|
|
1389
|
+
return resolve({ success: false, result: `❌ Clipboard copy failed (${platform.OS}). On Linux install xclip/xsel/wl-clipboard.` });
|
|
1390
|
+
}
|
|
1391
|
+
resolve({ success: true, result: `📋 Copied ${String(text).length} characters to clipboard.` });
|
|
1392
|
+
});
|
|
1393
|
+
child.stdin.write(String(text));
|
|
1394
|
+
child.stdin.end();
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
|
|
1399
|
+
// ─────────── HASH / CHECKSUM ───────────
|
|
1400
|
+
hash_text: {
|
|
1401
|
+
name: 'hash_text',
|
|
1402
|
+
description: 'Compute a cryptographic hash (md5, sha1, sha256, sha512) of a text string.',
|
|
1403
|
+
descriptionHi: 'टेक्स्ट का हैश निकालें',
|
|
1404
|
+
riskLevel: 'low',
|
|
1405
|
+
category: 'tool',
|
|
1406
|
+
parameters: {
|
|
1407
|
+
text: { type: 'string', required: true, description: 'Text to hash' },
|
|
1408
|
+
algorithm: { type: 'string', required: false, description: 'md5 | sha1 | sha256 (default) | sha512' }
|
|
1409
|
+
},
|
|
1410
|
+
execute: async (args) => {
|
|
1411
|
+
try {
|
|
1412
|
+
const crypto = require('crypto');
|
|
1413
|
+
const algo = (args.algorithm || 'sha256').toLowerCase();
|
|
1414
|
+
const allowed = ['md5', 'sha1', 'sha256', 'sha512'];
|
|
1415
|
+
if (!allowed.includes(algo)) {
|
|
1416
|
+
return { success: false, result: `❌ Algorithm must be one of: ${allowed.join(', ')}` };
|
|
1417
|
+
}
|
|
1418
|
+
const digest = crypto.createHash(algo).update(String(args.text ?? '')).digest('hex');
|
|
1419
|
+
return { success: true, result: `🔑 **${algo.toUpperCase()}**\n\n\`${digest}\`` };
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
return { success: false, result: `❌ Hashing failed: ${error.message}` };
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
},
|
|
1425
|
+
|
|
1426
|
+
// ─────────── BASE64 ENCODE / DECODE ───────────
|
|
1427
|
+
base64_tool: {
|
|
1428
|
+
name: 'base64_tool',
|
|
1429
|
+
description: 'Encode text to Base64 or decode Base64 back to text.',
|
|
1430
|
+
descriptionHi: 'Base64 एनकोड/डिकोड',
|
|
1431
|
+
riskLevel: 'low',
|
|
1432
|
+
category: 'tool',
|
|
1433
|
+
parameters: {
|
|
1434
|
+
text: { type: 'string', required: true, description: 'Input text or base64 string' },
|
|
1435
|
+
mode: { type: 'string', required: false, description: 'encode (default) | decode' }
|
|
1436
|
+
},
|
|
1437
|
+
execute: async (args) => {
|
|
1438
|
+
try {
|
|
1439
|
+
const mode = (args.mode || 'encode').toLowerCase();
|
|
1440
|
+
const input = String(args.text ?? '');
|
|
1441
|
+
if (mode === 'decode') {
|
|
1442
|
+
const out = Buffer.from(input, 'base64').toString('utf-8');
|
|
1443
|
+
return { success: true, result: `🔓 **Decoded:**\n\n${out}` };
|
|
1444
|
+
}
|
|
1445
|
+
const out = Buffer.from(input, 'utf-8').toString('base64');
|
|
1446
|
+
return { success: true, result: `🔒 **Base64:**\n\n\`${out}\`` };
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
return { success: false, result: `❌ Base64 ${args.mode || 'encode'} failed: ${error.message}` };
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
|
|
1453
|
+
// ─────────── WORLD CLOCK / TIMEZONE ───────────
|
|
1454
|
+
timezone_now: {
|
|
1455
|
+
name: 'timezone_now',
|
|
1456
|
+
description: 'Get the current date and time in any timezone (e.g. Asia/Kolkata, America/New_York, UTC).',
|
|
1457
|
+
descriptionHi: 'किसी टाइमज़ोन का समय जानें',
|
|
1458
|
+
riskLevel: 'low',
|
|
1459
|
+
category: 'tool',
|
|
1460
|
+
parameters: {
|
|
1461
|
+
timezone: { type: 'string', required: false, description: 'IANA timezone (default Asia/Kolkata)' }
|
|
1462
|
+
},
|
|
1463
|
+
execute: async (args) => {
|
|
1464
|
+
try {
|
|
1465
|
+
const tz = args.timezone || 'Asia/Kolkata';
|
|
1466
|
+
const now = new Date().toLocaleString('en-US', {
|
|
1467
|
+
timeZone: tz, dateStyle: 'full', timeStyle: 'long'
|
|
1468
|
+
});
|
|
1469
|
+
return { success: true, result: `🕐 **${tz}**\n\n${now}` };
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
return { success: false, result: `❌ Invalid timezone "${args.timezone}". Use IANA names like Asia/Kolkata.` };
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
989
1474
|
}
|
|
990
1475
|
};
|
|
991
1476
|
|