samarthya-bot 2.2.1 → 2.3.1

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +340 -353
  3. package/backend/bin/samarthya.js +29 -10
  4. package/backend/config/constants.js +4 -4
  5. package/backend/controllers/chatController.js +72 -0
  6. package/backend/controllers/telegramController.js +8 -0
  7. package/backend/package.json +1 -1
  8. package/backend/public/assets/index-DiMx9ERJ.css +1 -0
  9. package/backend/public/assets/index-Dn5WYZTH.js +32 -0
  10. package/backend/public/index.html +186 -16
  11. package/backend/public/robots.txt +32 -0
  12. package/backend/public/sitemap.xml +39 -0
  13. package/backend/services/agent/commandService.js +168 -0
  14. package/backend/services/llm/llmService.js +8 -0
  15. package/backend/services/planner/plannerService.js +17 -0
  16. package/backend/services/security/sandboxService.js +11 -4
  17. package/backend/services/system/platform.js +150 -0
  18. package/backend/services/tools/toolRegistry.js +521 -36
  19. package/backend/services/worker/workerClient.js +141 -29
  20. package/package.json +1 -1
  21. package/backend/public/assets/index-6PCzI3K2.js +0 -40
  22. package/backend/public/assets/index-6TF5jVRQ.js +0 -149
  23. package/backend/public/assets/index-B0U7rt6f.js +0 -46
  24. package/backend/public/assets/index-BF0RZh9i.js +0 -149
  25. package/backend/public/assets/index-BFRAq8Y1.js +0 -149
  26. package/backend/public/assets/index-CGw8cc8z.js +0 -149
  27. package/backend/public/assets/index-Ckf0GO1B.css +0 -1
  28. package/backend/public/assets/index-Cx0Ei-z7.js +0 -149
  29. package/backend/public/assets/index-DIPdcLv-.js +0 -25
  30. package/backend/public/assets/index-Da1E-MYB.js +0 -53
  31. package/backend/public/assets/index-DdCKkq38.js +0 -149
  32. package/backend/public/assets/index-Do4jNsZS.js +0 -19
  33. package/backend/public/assets/index-DyjpBYmS.js +0 -51
  34. package/backend/public/assets/index-DzlXcaXT.js +0 -149
  35. package/backend/public/assets/index-J7XSVHCz.css +0 -1
  36. package/backend/public/assets/index-Ui-pyZvK.js +0 -25
  37. 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 destructive host/remote commands.
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.*\.sh|curl.*\.sh|chmod\s+-R\s+777|chown\s+-R.*:.*\/)/im;
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
- // No path restrictions for full OS access!
209
- let filePath = argPath.startsWith('/') ? argPath : path.resolve(SAFE_DIR, argPath);
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
- // No path restrictions for full OS access!
268
- let filePath = argPath.startsWith('/') ? argPath : path.resolve(SAFE_DIR, argPath);
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 ? (args.path.startsWith('/') ? args.path : path.resolve(SAFE_DIR, args.path)) : SAFE_DIR;
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 ultra-fast Go micro-worker. Host OS: ${os.type()} ${os.release()}`,
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
- if (BLOCKED_COMMANDS.test(args.command)) {
516
- return { success: false, result: `❌ Security Block: The command '${args.command}' contains patterns that are restricted (e.g., recursive deletes, system reboots, disk formats).` };
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: ${os.type()} ${os.release()}. (Note: To open URLs/files, use 'start' for Windows, 'open' for macOS, 'xdg-open' for Linux).`,
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
- if (BLOCKED_COMMANDS.test(args.command)) {
561
- return { success: false, result: `❌ Security Block: The command '${args.command}' contains patterns that are restricted (e.g., recursive deletes, system reboots, disk formats).` };
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
- if (BLOCKED_COMMANDS.test(args.command)) {
598
- return { success: false, result: `❌ Security Block: The command '${args.command}' contains patterns that are restricted on remote SSH systems.` };
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
- // Try to connect to existing local Chrome, fallback to a downloaded edge/chrome if needed.
849
- // For local system testing, we simulate launching a default visible chrome.
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({ success: false, result: `❌ Chrome/Edge executable not found for Puppeteer.` });
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