samarthya-bot 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/backend/.env.example +23 -0
- package/backend/bin/samarthya.js +384 -0
- package/backend/config/constants.js +71 -0
- package/backend/config/db.js +13 -0
- package/backend/controllers/auditController.js +86 -0
- package/backend/controllers/authController.js +154 -0
- package/backend/controllers/chatController.js +158 -0
- package/backend/controllers/fileController.js +268 -0
- package/backend/controllers/platformController.js +54 -0
- package/backend/controllers/screenController.js +91 -0
- package/backend/controllers/telegramController.js +120 -0
- package/backend/controllers/toolsController.js +56 -0
- package/backend/controllers/whatsappController.js +214 -0
- package/backend/fix_toolRegistry.js +25 -0
- package/backend/middleware/auth.js +28 -0
- package/backend/models/AuditLog.js +28 -0
- package/backend/models/BackgroundJob.js +13 -0
- package/backend/models/Conversation.js +40 -0
- package/backend/models/Memory.js +17 -0
- package/backend/models/User.js +24 -0
- package/backend/package-lock.json +3766 -0
- package/backend/package.json +41 -0
- package/backend/public/assets/index-Ckf0GO1B.css +1 -0
- package/backend/public/assets/index-Do4jNsZS.js +19 -0
- package/backend/public/assets/index-Ui-pyZvK.js +25 -0
- package/backend/public/favicon.svg +17 -0
- package/backend/public/index.html +18 -0
- package/backend/public/manifest.json +16 -0
- package/backend/routes/audit.js +9 -0
- package/backend/routes/auth.js +11 -0
- package/backend/routes/chat.js +11 -0
- package/backend/routes/files.js +14 -0
- package/backend/routes/platform.js +18 -0
- package/backend/routes/screen.js +10 -0
- package/backend/routes/telegram.js +8 -0
- package/backend/routes/tools.js +9 -0
- package/backend/routes/whatsapp.js +11 -0
- package/backend/server.js +134 -0
- package/backend/services/background/backgroundService.js +81 -0
- package/backend/services/llm/llmService.js +444 -0
- package/backend/services/memory/memoryService.js +159 -0
- package/backend/services/planner/plannerService.js +182 -0
- package/backend/services/security/securityService.js +166 -0
- package/backend/services/telegram/telegramService.js +49 -0
- package/backend/services/tools/toolRegistry.js +879 -0
- package/backend/services/whatsapp/whatsappService.js +254 -0
- package/backend/test_email.js +29 -0
- package/backend/test_parser.js +10 -0
- package/package.json +49 -0
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
const { TOOL_PACKS } = require('../../config/constants');
|
|
2
|
+
const fs = require('fs').promises;
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { exec } = require('child_process');
|
|
6
|
+
const nodemailer = require('nodemailer');
|
|
7
|
+
|
|
8
|
+
// ────────────────────────────────────────────────────────────
|
|
9
|
+
// REAL Tool Definitions — No more simulations!
|
|
10
|
+
// ────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Safe directory — tools can only operate within this sandbox
|
|
14
|
+
* Change this to allow broader access (at your own risk)
|
|
15
|
+
*/
|
|
16
|
+
const BASE_DIR = path.join(os.homedir(), 'SamarthyaBot_Files');
|
|
17
|
+
|
|
18
|
+
async function getSafeDir(user) {
|
|
19
|
+
let dir = BASE_DIR;
|
|
20
|
+
if (user && (user._id || user.id)) {
|
|
21
|
+
dir = path.join(BASE_DIR, String(user._id || user.id));
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
await fs.mkdir(dir, { recursive: true });
|
|
25
|
+
} catch (e) { }
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const toolDefinitions = {
|
|
30
|
+
|
|
31
|
+
// ─────────── WEB SEARCH (Real via DuckDuckGo) ───────────
|
|
32
|
+
web_search: {
|
|
33
|
+
name: 'web_search',
|
|
34
|
+
description: 'Search the web for information using DuckDuckGo',
|
|
35
|
+
descriptionHi: 'इंटरनेट पर जानकारी खोजें',
|
|
36
|
+
riskLevel: 'low',
|
|
37
|
+
category: 'search',
|
|
38
|
+
parameters: {
|
|
39
|
+
query: { type: 'string', required: true, description: 'Search query' }
|
|
40
|
+
},
|
|
41
|
+
execute: async (args, userContext) => {
|
|
42
|
+
try {
|
|
43
|
+
const SAFE_DIR = await getSafeDir(userContext);
|
|
44
|
+
const query = encodeURIComponent(args.query);
|
|
45
|
+
|
|
46
|
+
// Use html.duckduckgo.com for much better real-time snippets than their JSON API
|
|
47
|
+
const response = await fetch(`https://html.duckduckgo.com/html/?q=${query}`, {
|
|
48
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' }
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new Error(`HTTP error ${response.status}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const html = await response.text();
|
|
56
|
+
|
|
57
|
+
// Extract snippets using regex
|
|
58
|
+
const snippetRegex = /<a class="result__snippet[^>]*>([\s\S]*?)<\/a>/g;
|
|
59
|
+
let match;
|
|
60
|
+
let snippets = [];
|
|
61
|
+
|
|
62
|
+
while ((match = snippetRegex.exec(html)) !== null && snippets.length < 4) {
|
|
63
|
+
// Strip inner HTML tags (like <b> tags for bolding)
|
|
64
|
+
const cleanText = match[1].replace(/<[^>]*>?/gm, '').replace(/&/g, '&').replace(/"/g, '"').trim();
|
|
65
|
+
if (cleanText) snippets.push(cleanText);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let results = '';
|
|
69
|
+
if (snippets.length > 0) {
|
|
70
|
+
results += `🔍 **Top Real-time Results:**\n\n`;
|
|
71
|
+
snippets.forEach((snippet, i) => {
|
|
72
|
+
results += `${i + 1}. ${snippet}\n`;
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
results = `🔍 No instant snippets found for "${args.query}". Try Google: https://www.google.com/search?q=${query}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Try to extract some URLs as well
|
|
79
|
+
const urlRegex = /<a class="result__url" href="([^"]+)">/g;
|
|
80
|
+
const urls = [];
|
|
81
|
+
while ((match = urlRegex.exec(html)) !== null && urls.length < 3) {
|
|
82
|
+
// Un-escape duckduckgo redirect wrapper if present
|
|
83
|
+
let extractedUrl = match[1];
|
|
84
|
+
if (extractedUrl.includes('?q=')) {
|
|
85
|
+
const qMatch = extractedUrl.match(/\?q=([^&]+)/);
|
|
86
|
+
if (qMatch) extractedUrl = decodeURIComponent(qMatch[1]);
|
|
87
|
+
}
|
|
88
|
+
if (extractedUrl && !urls.includes(extractedUrl) && extractedUrl.startsWith('http')) urls.push(extractedUrl);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (urls.length > 0) {
|
|
92
|
+
results += `\n🔗 **Sources:**\n${urls.map(u => `• ${u}`).join('\n')}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { success: true, result: results };
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return { success: false, result: `❌ Search failed: ${error.message}` };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
// ─────────── CALCULATOR (Real, safe eval) ───────────
|
|
103
|
+
calculate: {
|
|
104
|
+
name: 'calculate',
|
|
105
|
+
description: 'Perform mathematical calculations',
|
|
106
|
+
descriptionHi: 'गणितीय गणना करें',
|
|
107
|
+
riskLevel: 'low',
|
|
108
|
+
category: 'tool',
|
|
109
|
+
parameters: {
|
|
110
|
+
expression: { type: 'string', required: true, description: 'Math expression to evaluate' }
|
|
111
|
+
},
|
|
112
|
+
execute: async (args, userContext) => {
|
|
113
|
+
try {
|
|
114
|
+
const SAFE_DIR = await getSafeDir(userContext);
|
|
115
|
+
const sanitized = args.expression.replace(/[^0-9+\-*/().%\s]/g, '');
|
|
116
|
+
const result = Function('"use strict"; return (' + sanitized + ')')();
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
result: `🧮 **Calculation:**\n${args.expression} = **${result}**\n\n💡 GST tip: For 18% GST on ₹${result}, total = ₹${(result * 1.18).toFixed(2)}`
|
|
120
|
+
};
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return { success: false, result: `❌ Calculation error: ${e.message}` };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// ─────────── WEATHER (Real via Open-Meteo — Free, No API key!) ───────────
|
|
128
|
+
weather_info: {
|
|
129
|
+
name: 'weather_info',
|
|
130
|
+
description: 'Get real-time weather for any city',
|
|
131
|
+
descriptionHi: 'शहर का रियल-टाइम मौसम जानें',
|
|
132
|
+
riskLevel: 'low',
|
|
133
|
+
category: 'search',
|
|
134
|
+
parameters: {
|
|
135
|
+
city: { type: 'string', required: true, description: 'City name' }
|
|
136
|
+
},
|
|
137
|
+
execute: async (args, userContext) => {
|
|
138
|
+
try {
|
|
139
|
+
const SAFE_DIR = await getSafeDir(userContext);
|
|
140
|
+
// Step 1: Geocode city name
|
|
141
|
+
const geoRes = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(args.city)}&count=1`);
|
|
142
|
+
const geoData = await geoRes.json();
|
|
143
|
+
|
|
144
|
+
if (!geoData.results || geoData.results.length === 0) {
|
|
145
|
+
return { success: false, result: `❌ City "${args.city}" not found. Try English name.` };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { latitude, longitude, name, country } = geoData.results[0];
|
|
149
|
+
|
|
150
|
+
// Step 2: Get weather
|
|
151
|
+
const weatherRes = await fetch(
|
|
152
|
+
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code&timezone=Asia/Kolkata`
|
|
153
|
+
);
|
|
154
|
+
const weather = await weatherRes.json();
|
|
155
|
+
const c = weather.current;
|
|
156
|
+
|
|
157
|
+
const weatherCodes = {
|
|
158
|
+
0: '☀️ Clear sky', 1: '🌤️ Mainly clear', 2: '⛅ Partly cloudy',
|
|
159
|
+
3: '☁️ Overcast', 45: '🌫️ Fog', 48: '🌫️ Rime fog',
|
|
160
|
+
51: '🌦️ Light drizzle', 53: '🌦️ Moderate drizzle', 55: '🌧️ Dense drizzle',
|
|
161
|
+
61: '🌧️ Slight rain', 63: '🌧️ Moderate rain', 65: '🌧️ Heavy rain',
|
|
162
|
+
71: '🌨️ Slight snow', 73: '🌨️ Moderate snow', 75: '❄️ Heavy snow',
|
|
163
|
+
80: '🌦️ Rain showers', 81: '🌧️ Moderate showers', 82: '⛈️ Violent showers',
|
|
164
|
+
95: '⛈️ Thunderstorm', 96: '⛈️ Thunderstorm with hail'
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const desc = weatherCodes[c.weather_code] || '🌡️ Unknown';
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
result: `🌤️ **Weather for ${name}, ${country}**\n\n` +
|
|
172
|
+
`${desc}\n` +
|
|
173
|
+
`🌡️ Temperature: **${c.temperature_2m}°C**\n` +
|
|
174
|
+
`💧 Humidity: **${c.relative_humidity_2m}%**\n` +
|
|
175
|
+
`💨 Wind: **${c.wind_speed_10m} km/h**\n\n` +
|
|
176
|
+
`🕐 Updated: ${new Date().toLocaleTimeString('en-IN', { timeZone: 'Asia/Kolkata' })} IST`
|
|
177
|
+
};
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return { success: false, result: `❌ Weather fetch failed: ${error.message}` };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// ─────────── FILE READ (Real — FULL OS ACCESS) ───────────
|
|
185
|
+
file_read: {
|
|
186
|
+
name: 'file_read',
|
|
187
|
+
description: 'Read contents of any file on the system. Provide absolute path or filename (defaults to user home directory).',
|
|
188
|
+
descriptionHi: 'फ़ाइल की सामग्री पढ़ें',
|
|
189
|
+
riskLevel: 'medium',
|
|
190
|
+
category: 'file',
|
|
191
|
+
parameters: {
|
|
192
|
+
path: { type: 'string', required: true, description: 'Absolute path or filename' }
|
|
193
|
+
},
|
|
194
|
+
execute: async (args, userContext) => {
|
|
195
|
+
try {
|
|
196
|
+
const SAFE_DIR = await getSafeDir(userContext);
|
|
197
|
+
// Normalize: LLM may use file_path, filename, file_name, etc.
|
|
198
|
+
const argPath = args.path || args.file_path || args.filename || args.file_name || args.file;
|
|
199
|
+
if (!argPath) return { success: false, result: '❌ Please provide a file path/name.' };
|
|
200
|
+
// No path restrictions for full OS access!
|
|
201
|
+
let filePath = argPath.startsWith('/') ? argPath : path.resolve(SAFE_DIR, argPath);
|
|
202
|
+
|
|
203
|
+
const stat = await fs.stat(filePath);
|
|
204
|
+
if (stat.isDirectory()) {
|
|
205
|
+
const files = await fs.readdir(filePath);
|
|
206
|
+
return {
|
|
207
|
+
success: true,
|
|
208
|
+
result: `📁 **Directory listing: ${argPath}**\n\n${files.map(f => `• ${f}`).join('\n') || '(empty folder)'}`
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Don't read files over 1MB
|
|
213
|
+
if (stat.size > 1024 * 1024) {
|
|
214
|
+
return { success: false, result: `❌ File too large (${(stat.size / 1024).toFixed(1)}KB). Max 1MB.` };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
218
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
success: true,
|
|
222
|
+
result: `📄 **File: ${argPath}** (${stat.size} bytes)\n\n\`\`\`\n${content}\n\`\`\``
|
|
223
|
+
};
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error.code === 'ENOENT') {
|
|
226
|
+
// List available files instead
|
|
227
|
+
try {
|
|
228
|
+
const files = await fs.readdir(SAFE_DIR);
|
|
229
|
+
return {
|
|
230
|
+
success: false,
|
|
231
|
+
result: `❌ File not found: "${argPath}"\n\n📁 Available files in SamarthyaBot_Files:\n${files.map(f => `• ${f}`).join('\n') || '(empty — write a file first!)'}`
|
|
232
|
+
};
|
|
233
|
+
} catch (e) {
|
|
234
|
+
return { success: false, result: `❌ File not found: "${argPath}"` };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { success: false, result: `❌ File read error: ${error.message}` };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
// ─────────── FILE WRITE (Real — FULL OS ACCESS) ───────────
|
|
243
|
+
file_write: {
|
|
244
|
+
name: 'file_write',
|
|
245
|
+
description: 'Write/create any file on the system. Provide absolute path or filename (defaults to user home directory).',
|
|
246
|
+
descriptionHi: 'फ़ाइल बनाएं या लिखें',
|
|
247
|
+
riskLevel: 'medium',
|
|
248
|
+
category: 'file',
|
|
249
|
+
parameters: {
|
|
250
|
+
path: { type: 'string', required: true, description: 'Absolute path or filename' },
|
|
251
|
+
content: { type: 'string', required: true, description: 'Content to write' }
|
|
252
|
+
},
|
|
253
|
+
execute: async (args, userContext) => {
|
|
254
|
+
try {
|
|
255
|
+
const SAFE_DIR = await getSafeDir(userContext);
|
|
256
|
+
// Normalize: LLM may use file_path, filename, etc.
|
|
257
|
+
const argPath = args.path || args.file_path || args.filename || args.file_name || args.file;
|
|
258
|
+
if (!argPath) return { success: false, result: '❌ Please provide a file path/name.' };
|
|
259
|
+
// No path restrictions for full OS access!
|
|
260
|
+
let filePath = argPath.startsWith('/') ? argPath : path.resolve(SAFE_DIR, argPath);
|
|
261
|
+
|
|
262
|
+
// Create subdirectories if needed
|
|
263
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
264
|
+
|
|
265
|
+
await fs.writeFile(filePath, args.content, 'utf-8');
|
|
266
|
+
const stat = await fs.stat(filePath);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
success: true,
|
|
270
|
+
result: `✅ **File written successfully!**\n\n📄 Path: ~/SamarthyaBot_Files/${argPath}\n📏 Size: ${stat.size} bytes\n🕐 Time: ${new Date().toLocaleTimeString('en-IN', { timeZone: 'Asia/Kolkata' })} IST`
|
|
271
|
+
};
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return { success: false, result: `❌ File write error: ${error.message}` };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
// ─────────── DIRECTORY LIST (Real — FULL OS ACCESS) ───────────
|
|
279
|
+
file_list: {
|
|
280
|
+
name: 'file_list',
|
|
281
|
+
description: 'List all files in a directory. Provide absolute path (defaults to user home directory).',
|
|
282
|
+
descriptionHi: 'फ़ोल्डर की सभी फाइलें दिखाएं',
|
|
283
|
+
riskLevel: 'low',
|
|
284
|
+
category: 'file',
|
|
285
|
+
parameters: {
|
|
286
|
+
path: { type: 'string', required: false, description: 'Absolute directory path to list' }
|
|
287
|
+
},
|
|
288
|
+
execute: async (args, userContext) => {
|
|
289
|
+
try {
|
|
290
|
+
const SAFE_DIR = await getSafeDir(userContext);
|
|
291
|
+
const targetDir = args.path ? (args.path.startsWith('/') ? args.path : path.resolve(SAFE_DIR, args.path)) : SAFE_DIR;
|
|
292
|
+
|
|
293
|
+
const files = await fs.readdir(targetDir, { withFileTypes: true });
|
|
294
|
+
if (files.length === 0) {
|
|
295
|
+
return { success: true, result: `📁 ${targetDir} is empty.` };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let listing = `📁 **${targetDir}**\n\n`;
|
|
299
|
+
for (const f of files) {
|
|
300
|
+
const icon = f.isDirectory() ? '📂' : '📄';
|
|
301
|
+
try {
|
|
302
|
+
const stat = await fs.stat(path.join(targetDir, f.name));
|
|
303
|
+
const size = f.isDirectory() ? 'folder' : `${(stat.size / 1024).toFixed(1)}KB`;
|
|
304
|
+
listing += `${icon} ${f.name} (${size})\n`;
|
|
305
|
+
} catch (e) {
|
|
306
|
+
listing += `${icon} ${f.name}\n`;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { success: true, result: listing };
|
|
310
|
+
} catch (error) {
|
|
311
|
+
return { success: false, result: `❌ List error: ${error.message}` };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
// ─────────── SEND EMAIL (Real via Nodemailer + Gmail) ───────────
|
|
317
|
+
send_email: {
|
|
318
|
+
name: 'send_email',
|
|
319
|
+
description: 'Send a real email via Gmail SMTP',
|
|
320
|
+
descriptionHi: 'ईमेल भेजें (Gmail)',
|
|
321
|
+
riskLevel: 'high',
|
|
322
|
+
category: 'email',
|
|
323
|
+
parameters: {
|
|
324
|
+
to: { type: 'string', required: true, description: 'Recipient email' },
|
|
325
|
+
subject: { type: 'string', required: true, description: 'Email subject' },
|
|
326
|
+
body: { type: 'string', required: true, description: 'Email body. CAUTION: You MUST use \\n for newlines instead of actual linebreaks inside this string.' }
|
|
327
|
+
},
|
|
328
|
+
execute: async (args, userContext) => {
|
|
329
|
+
try {
|
|
330
|
+
const SAFE_DIR = await getSafeDir(userContext);
|
|
331
|
+
const emailUser = process.env.SMTP_EMAIL;
|
|
332
|
+
const emailPass = process.env.SMTP_PASSWORD;
|
|
333
|
+
|
|
334
|
+
if (!emailUser || !emailPass) {
|
|
335
|
+
return {
|
|
336
|
+
success: false,
|
|
337
|
+
result: `❌ **Email not configured!**\n\n🔧 Setup steps:\n1. Go to Google Account → Security → App Passwords\n2. Generate an App Password for "Mail"\n3. Add to .env:\n\`\`\`\nSMTP_EMAIL=your_gmail@gmail.com\nSMTP_PASSWORD=your_16_char_app_password\n\`\`\`\n4. Restart server`
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const transporter = nodemailer.createTransport({
|
|
342
|
+
service: 'gmail',
|
|
343
|
+
auth: {
|
|
344
|
+
user: emailUser,
|
|
345
|
+
pass: emailPass
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const isHtml = args.body.includes('<') && args.body.includes('>');
|
|
350
|
+
|
|
351
|
+
const info = await transporter.sendMail({
|
|
352
|
+
from: `"SamarthyaBot" <${emailUser}>`,
|
|
353
|
+
to: args.to,
|
|
354
|
+
subject: args.subject,
|
|
355
|
+
[isHtml ? 'html' : 'text']: args.body
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
success: true,
|
|
360
|
+
result: `✅ **Email sent successfully!**\n\n📧 To: ${args.to}\n📋 Subject: ${args.subject}\n🆔 Message ID: ${info.messageId}\n🕐 Sent at: ${new Date().toLocaleTimeString('en-IN', { timeZone: 'Asia/Kolkata' })} IST`
|
|
361
|
+
};
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return { success: false, result: `❌ Email failed: ${error.message}` };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
// ─────────── NOTE/REMINDER (Real — saves to filesystem) ───────────
|
|
369
|
+
note_take: {
|
|
370
|
+
name: 'note_take',
|
|
371
|
+
description: 'Save a note to ~/SamarthyaBot_Files/notes/',
|
|
372
|
+
descriptionHi: 'नोट सेव करें',
|
|
373
|
+
riskLevel: 'low',
|
|
374
|
+
category: 'tool',
|
|
375
|
+
parameters: {
|
|
376
|
+
title: { type: 'string', required: true, description: 'Note title' },
|
|
377
|
+
content: { type: 'string', required: true, description: 'Note content' }
|
|
378
|
+
},
|
|
379
|
+
execute: async (args, userContext) => {
|
|
380
|
+
try {
|
|
381
|
+
const SAFE_DIR = await getSafeDir(userContext);
|
|
382
|
+
const notesDir = path.join(SAFE_DIR, 'notes');
|
|
383
|
+
await fs.mkdir(notesDir, { recursive: true });
|
|
384
|
+
|
|
385
|
+
const filename = `${args.title.replace(/[^a-zA-Z0-9_\- ]/g, '').replace(/\s+/g, '_')}_${Date.now()}.md`;
|
|
386
|
+
const filePath = path.join(notesDir, filename);
|
|
387
|
+
|
|
388
|
+
const noteContent = `# ${args.title}\n\n_Created: ${new Date().toLocaleString('en-IN', { timeZone: 'Asia/Kolkata' })}_\n\n${args.content}`;
|
|
389
|
+
await fs.writeFile(filePath, noteContent, 'utf-8');
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
success: true,
|
|
393
|
+
result: `📝 **Note saved!**\n\n📄 File: ~/SamarthyaBot_Files/notes/${filename}\n📋 Title: ${args.title}\n🕐 Time: ${new Date().toLocaleTimeString('en-IN', { timeZone: 'Asia/Kolkata' })} IST`
|
|
394
|
+
};
|
|
395
|
+
} catch (error) {
|
|
396
|
+
return { success: false, result: `❌ Note save error: ${error.message}` };
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
// ─────────── REMINDER (Real — writes to file + shows time) ───────────
|
|
402
|
+
reminder_set: {
|
|
403
|
+
name: 'reminder_set',
|
|
404
|
+
description: 'Set a reminder (saved to file)',
|
|
405
|
+
descriptionHi: 'रिमाइंडर सेट करें',
|
|
406
|
+
riskLevel: 'low',
|
|
407
|
+
category: 'tool',
|
|
408
|
+
parameters: {
|
|
409
|
+
message: { type: 'string', required: true, description: 'Reminder message' },
|
|
410
|
+
time: { type: 'string', required: false, description: 'When to remind (IST)' },
|
|
411
|
+
isoDate: { type: 'string', required: false, description: 'When to remind exactly, specified as ISO-8601 UTC date string (e.g. 2026-03-02T10:00:00Z)' },
|
|
412
|
+
delayInSeconds: { type: 'number', required: false, description: 'If the requested time is relative (like "in 20 seconds" or "after 5 minutes"), specify total seconds here (e.g. 20, 300) for high precision.' }
|
|
413
|
+
},
|
|
414
|
+
execute: async (args, userContext) => {
|
|
415
|
+
try {
|
|
416
|
+
// Fallbacks if LLM hallucinated keys
|
|
417
|
+
const message = args.message || args.description || 'Reminder';
|
|
418
|
+
const delayInput = args.delayInSeconds || args.time_in_seconds || args.seconds || 0;
|
|
419
|
+
|
|
420
|
+
let delayMs = 0;
|
|
421
|
+
let gcalLink = '';
|
|
422
|
+
const gcalTitle = encodeURIComponent(message);
|
|
423
|
+
|
|
424
|
+
if (delayInput > 0) {
|
|
425
|
+
delayMs = delayInput * 1000;
|
|
426
|
+
const d = new Date(Date.now() + delayMs);
|
|
427
|
+
const dEnd = new Date(d.getTime() + 30 * 60000);
|
|
428
|
+
const formatIso = (dateObj) => dateObj.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
|
|
429
|
+
gcalLink = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${gcalTitle}&dates=${formatIso(d)}/${formatIso(dEnd)}`;
|
|
430
|
+
} else if (args.isoDate) {
|
|
431
|
+
try {
|
|
432
|
+
const targetTime = new Date(args.isoDate);
|
|
433
|
+
if (!isNaN(targetTime)) {
|
|
434
|
+
delayMs = Math.max(0, targetTime.getTime() - Date.now());
|
|
435
|
+
const dEnd = new Date(targetTime.getTime() + 30 * 60000); // +30 mins
|
|
436
|
+
const formatIso = (dateObj) => dateObj.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
|
|
437
|
+
gcalLink = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${gcalTitle}&dates=${formatIso(targetTime)}/${formatIso(dEnd)}`;
|
|
438
|
+
}
|
|
439
|
+
} catch (e) { }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!gcalLink) {
|
|
443
|
+
gcalLink = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${gcalTitle}`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
success: true,
|
|
448
|
+
notificationParams: {
|
|
449
|
+
message: message,
|
|
450
|
+
delayMs: delayMs || 5000 // default 5s if can't parse
|
|
451
|
+
},
|
|
452
|
+
result: `⏰ **Live Reminder set!**\n\n💬 ${message}\n🕐 Time: ${args.time || 'No specific time'}\n\n_Browser notification scheduled! Please keep this tab open._\n\n✨ **[Add to Google Calendar](${gcalLink})** (Gets you email & web notifications!)`
|
|
453
|
+
};
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return { success: false, result: `❌ Reminder error: ${error.message}` };
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
// ─────────── SYSTEM INFO (Real OS data) ───────────
|
|
461
|
+
system_info: {
|
|
462
|
+
name: 'system_info',
|
|
463
|
+
description: 'Get system information (CPU, RAM, OS)',
|
|
464
|
+
descriptionHi: 'सिस्टम की जानकारी प्राप्त करें',
|
|
465
|
+
riskLevel: 'low',
|
|
466
|
+
category: 'system',
|
|
467
|
+
parameters: {},
|
|
468
|
+
execute: async (args, userContext) => {
|
|
469
|
+
const totalMem = os.totalmem();
|
|
470
|
+
const freeMem = os.freemem();
|
|
471
|
+
const usedMem = totalMem - freeMem;
|
|
472
|
+
const cpus = os.cpus();
|
|
473
|
+
const uptime = os.uptime();
|
|
474
|
+
|
|
475
|
+
const hours = Math.floor(uptime / 3600);
|
|
476
|
+
const mins = Math.floor((uptime % 3600) / 60);
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
success: true,
|
|
480
|
+
result: `🖥️ **System Information**\n\n` +
|
|
481
|
+
`💻 OS: ${os.type()} ${os.release()} (${os.arch()})\n` +
|
|
482
|
+
`🏠 Hostname: ${os.hostname()}\n` +
|
|
483
|
+
`👤 User: ${os.userInfo().username}\n` +
|
|
484
|
+
`🧠 CPU: ${cpus[0]?.model || 'Unknown'} (${cpus.length} cores)\n` +
|
|
485
|
+
`💾 RAM: ${(usedMem / 1073741824).toFixed(1)}GB / ${(totalMem / 1073741824).toFixed(1)}GB (${((usedMem / totalMem) * 100).toFixed(0)}% used)\n` +
|
|
486
|
+
`💿 Free RAM: ${(freeMem / 1073741824).toFixed(1)}GB\n` +
|
|
487
|
+
`⏱️ Uptime: ${hours}h ${mins}m\n` +
|
|
488
|
+
`📂 Home: ${os.homedir()}\n` +
|
|
489
|
+
`🕐 Time: ${new Date().toLocaleString('en-IN', { timeZone: 'Asia/Kolkata' })} IST`
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
// ─────────── RUN COMMAND (Real — FULL ROOT/OS EXECUTION) ───────────
|
|
495
|
+
run_command: {
|
|
496
|
+
name: 'run_command',
|
|
497
|
+
description: 'Run ANY shell command on the host OS. Warning: Full access provided. Use carefully.',
|
|
498
|
+
descriptionHi: 'शेल कमांड चलाएं',
|
|
499
|
+
riskLevel: 'critical',
|
|
500
|
+
category: 'system',
|
|
501
|
+
parameters: {
|
|
502
|
+
command: { type: 'string', required: true, description: 'Shell command to execute' }
|
|
503
|
+
},
|
|
504
|
+
execute: async (args, userContext) => {
|
|
505
|
+
return new Promise((resolve) => {
|
|
506
|
+
exec(args.command, { timeout: 15000, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
|
|
507
|
+
if (error) {
|
|
508
|
+
resolve({
|
|
509
|
+
success: false,
|
|
510
|
+
result: `❌ Command failed:\n\`\`\`\n${stderr || error.message}\n\`\`\``
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
resolve({
|
|
515
|
+
success: true,
|
|
516
|
+
result: `💻 **Command:** \`${args.command}\`\n\n\`\`\`\n${(stdout || '(no output)').substring(0, 3000)}\n\`\`\``
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
// ─────────── GST REMINDER (Real — saves reminder files) ───────────
|
|
524
|
+
gst_reminder: {
|
|
525
|
+
name: 'gst_reminder',
|
|
526
|
+
description: 'Set GST filing reminder with real deadlines',
|
|
527
|
+
descriptionHi: 'GST फाइलिंग रिमाइंडर',
|
|
528
|
+
riskLevel: 'low',
|
|
529
|
+
category: 'tool',
|
|
530
|
+
parameters: {
|
|
531
|
+
gstType: { type: 'string', required: true, description: 'GST return type (GSTR-1, GSTR-3B, etc.)' },
|
|
532
|
+
dueDate: { type: 'string', required: false, description: 'Due date' }
|
|
533
|
+
},
|
|
534
|
+
execute: async (args, userContext) => {
|
|
535
|
+
const gstDates = {
|
|
536
|
+
'GSTR-1': { day: 11, desc: '11th of next month', late: '₹50/day (₹20 for nil)' },
|
|
537
|
+
'GSTR-3B': { day: 20, desc: '20th of next month', late: '₹50/day (₹20 for nil) + 18% interest' },
|
|
538
|
+
'GSTR-9': { day: 31, desc: 'December 31st', late: '₹200/day (max ₹5000)' },
|
|
539
|
+
'GSTR-4': { day: 30, desc: 'April 30th (annual)', late: '₹50/day (₹20 for nil)' },
|
|
540
|
+
'GSTR-2A': { day: 0, desc: 'Auto-populated', late: 'N/A' }
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const info = gstDates[args.gstType?.toUpperCase()] || null;
|
|
544
|
+
|
|
545
|
+
// Save reminder to file
|
|
546
|
+
try {
|
|
547
|
+
const SAFE_DIR = await getSafeDir(userContext);
|
|
548
|
+
const remindersDir = path.join(SAFE_DIR, 'reminders');
|
|
549
|
+
await fs.mkdir(remindersDir, { recursive: true });
|
|
550
|
+
const reminder = {
|
|
551
|
+
type: 'gst',
|
|
552
|
+
gstType: args.gstType,
|
|
553
|
+
dueDate: info?.desc || args.dueDate || 'Unknown',
|
|
554
|
+
createdAt: new Date().toISOString()
|
|
555
|
+
};
|
|
556
|
+
await fs.writeFile(
|
|
557
|
+
path.join(remindersDir, `gst_${args.gstType}_${Date.now()}.json`),
|
|
558
|
+
JSON.stringify(reminder, null, 2)
|
|
559
|
+
);
|
|
560
|
+
} catch (e) { /* non-critical */ }
|
|
561
|
+
|
|
562
|
+
if (info) {
|
|
563
|
+
return {
|
|
564
|
+
success: true,
|
|
565
|
+
result: `🏢 **GST Reminder Set!**\n\n📋 Return: **${args.gstType.toUpperCase()}**\n📅 Due: **${info.desc}**\n💸 Late Fee: ${info.late}\n\n⚠️ Late filing se penalty + interest lagti hai!\n📄 Reminder saved to ~/SamarthyaBot_Files/reminders/`
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
success: true,
|
|
571
|
+
result: `🏢 GST Reminder Set for ${args.gstType}\nDue: ${args.dueDate || 'Check GST portal'}\n\n📄 Reminder saved.`
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
// ─────────── UPI LINK GENERATOR (Real deep link) ───────────
|
|
577
|
+
upi_generate: {
|
|
578
|
+
name: 'upi_generate',
|
|
579
|
+
description: 'Generate a real UPI payment link',
|
|
580
|
+
descriptionHi: 'UPI पेमेंट लिंक बनाएं',
|
|
581
|
+
riskLevel: 'medium',
|
|
582
|
+
category: 'tool',
|
|
583
|
+
parameters: {
|
|
584
|
+
amount: { type: 'number', required: true, description: 'Amount in INR' },
|
|
585
|
+
upiId: { type: 'string', required: true, description: 'UPI ID to receive payment' },
|
|
586
|
+
name: { type: 'string', required: false, description: 'Payee name' },
|
|
587
|
+
note: { type: 'string', required: false, description: 'Payment note' }
|
|
588
|
+
},
|
|
589
|
+
execute: async (args, userContext) => {
|
|
590
|
+
const params = new URLSearchParams({
|
|
591
|
+
pa: args.upiId,
|
|
592
|
+
am: String(args.amount),
|
|
593
|
+
cu: 'INR',
|
|
594
|
+
tn: args.note || 'Payment'
|
|
595
|
+
});
|
|
596
|
+
if (args.name) params.set('pn', args.name);
|
|
597
|
+
|
|
598
|
+
const upiLink = `upi://pay?${params.toString()}`;
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
success: true,
|
|
602
|
+
result: `💳 **UPI Payment Link Generated!**\n\n` +
|
|
603
|
+
`• **UPI ID:** ${args.upiId}\n` +
|
|
604
|
+
`• **Amount:** ₹${args.amount}\n` +
|
|
605
|
+
`• **Payee:** ${args.name || 'N/A'}\n` +
|
|
606
|
+
`• **Note:** ${args.note || 'Payment'}\n\n` +
|
|
607
|
+
`🔗 Link: \`${upiLink}\`\n\n` +
|
|
608
|
+
`📱 Click on phone to open GPay/PhonePe/Paytm with pre-filled details!`
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
// ─────────── SUMMARIZE TEXT (via Gemini API) ───────────
|
|
614
|
+
summarize_text: {
|
|
615
|
+
name: 'summarize_text',
|
|
616
|
+
description: 'Summarize a long text or document',
|
|
617
|
+
descriptionHi: 'लंबे टेक्स्ट का सारांश',
|
|
618
|
+
riskLevel: 'low',
|
|
619
|
+
category: 'tool',
|
|
620
|
+
parameters: {
|
|
621
|
+
text: { type: 'string', required: true, description: 'Text to summarize' }
|
|
622
|
+
},
|
|
623
|
+
execute: async (args, userContext) => {
|
|
624
|
+
const words = args.text.split(/\s+/).length;
|
|
625
|
+
// The LLM itself will summarize this — tool just provides metadata
|
|
626
|
+
return {
|
|
627
|
+
success: true,
|
|
628
|
+
result: `📄 **Text to Summarize** (${words} words):\n\n${args.text.substring(0, 500)}${args.text.length > 500 ? '...' : ''}\n\n_The AI will now provide a concise summary._`
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
|
|
633
|
+
// ─────────── CALENDAR (saves events to file) ───────────
|
|
634
|
+
calendar_schedule: {
|
|
635
|
+
name: 'calendar_schedule',
|
|
636
|
+
description: 'Schedule a calendar event (saved locally)',
|
|
637
|
+
descriptionHi: 'कैलेंडर में इवेंट शेड्यूल करें',
|
|
638
|
+
riskLevel: 'medium',
|
|
639
|
+
category: 'calendar',
|
|
640
|
+
parameters: {
|
|
641
|
+
title: { type: 'string', required: true, description: 'Event title' },
|
|
642
|
+
date: { type: 'string', required: true, description: 'Event date' },
|
|
643
|
+
time: { type: 'string', required: false, description: 'Event time (IST)' },
|
|
644
|
+
isoDate: { type: 'string', required: false, description: 'Event exact datetime as ISO-8601 UTC string (e.g. 2026-03-02T10:00:00Z)' }
|
|
645
|
+
},
|
|
646
|
+
execute: async (args, userContext) => {
|
|
647
|
+
try {
|
|
648
|
+
const SAFE_DIR = await getSafeDir(userContext);
|
|
649
|
+
const calDir = path.join(SAFE_DIR, 'calendar');
|
|
650
|
+
await fs.mkdir(calDir, { recursive: true });
|
|
651
|
+
|
|
652
|
+
const event = {
|
|
653
|
+
title: args.title,
|
|
654
|
+
date: args.date,
|
|
655
|
+
time: args.time || 'All day',
|
|
656
|
+
createdAt: new Date().toISOString()
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const filename = `event_${Date.now()}.json`;
|
|
660
|
+
await fs.writeFile(path.join(calDir, filename), JSON.stringify(event, null, 2));
|
|
661
|
+
|
|
662
|
+
let gcalLink = '';
|
|
663
|
+
const gcalTitle = encodeURIComponent(args.title);
|
|
664
|
+
if (args.isoDate) {
|
|
665
|
+
try {
|
|
666
|
+
const d = new Date(args.isoDate);
|
|
667
|
+
if (!isNaN(d)) {
|
|
668
|
+
const dEnd = new Date(d.getTime() + 60 * 60000); // 1 hour duration
|
|
669
|
+
const formatIso = (dateObj) => dateObj.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
|
|
670
|
+
gcalLink = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${gcalTitle}&dates=${formatIso(d)}/${formatIso(dEnd)}`;
|
|
671
|
+
}
|
|
672
|
+
} catch (e) { }
|
|
673
|
+
}
|
|
674
|
+
if (!gcalLink) {
|
|
675
|
+
gcalLink = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${gcalTitle}`;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
success: true,
|
|
680
|
+
result: `📅 **Event Scheduled!**\n\n📋 Title: ${args.title}\n📆 Date: ${args.date}\n🕐 Time: ${args.time || 'All day'}\n📄 Saved file: \`calendar/${filename}\`\n\n✨ **[Add to Google Calendar](${gcalLink})** (Gets you email & web notifications!)`
|
|
681
|
+
};
|
|
682
|
+
} catch (error) {
|
|
683
|
+
return { success: false, result: `❌ Calendar error: ${error.message}` };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
|
|
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',
|
|
694
|
+
category: 'browser',
|
|
695
|
+
parameters: {
|
|
696
|
+
url: { type: 'string', required: true, description: 'URL to open' }
|
|
697
|
+
},
|
|
698
|
+
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
|
+
});
|
|
707
|
+
} else {
|
|
708
|
+
resolve({
|
|
709
|
+
success: true,
|
|
710
|
+
result: `🌐 **Opened in browser:** ${args.url}`
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
// ─────────── CAPTURE DESKTOP SCREENSHOT (Real OS screenshot) ───────────
|
|
719
|
+
capture_desktop_screenshot: {
|
|
720
|
+
name: 'capture_desktop_screenshot',
|
|
721
|
+
description: 'Capture a screenshot of the host desktop for screen understanding.',
|
|
722
|
+
descriptionHi: 'स्क्रीनशॉट लें',
|
|
723
|
+
riskLevel: 'medium',
|
|
724
|
+
category: 'system',
|
|
725
|
+
parameters: {},
|
|
726
|
+
execute: async (args, userContext) => {
|
|
727
|
+
return new Promise((resolve) => {
|
|
728
|
+
const screenshot = require('screenshot-desktop');
|
|
729
|
+
screenshot({ format: 'png' })
|
|
730
|
+
.then((imgRaw) => {
|
|
731
|
+
// Return the base64 to the LLM
|
|
732
|
+
const base64 = imgRaw.toString('base64');
|
|
733
|
+
resolve({
|
|
734
|
+
success: true,
|
|
735
|
+
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
|
+
});
|
|
739
|
+
})
|
|
740
|
+
.catch((err) => {
|
|
741
|
+
resolve({ success: false, result: `❌ Screenshot failed: ${err.message}` });
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
// ─────────── SCHEDULE BACKGROUND TASK (Autonomous Mode) ───────────
|
|
748
|
+
schedule_background_task: {
|
|
749
|
+
name: 'schedule_background_task',
|
|
750
|
+
description: 'Schedule a recurring or delayed task for the AI to execute automatically in the background without user intervention.',
|
|
751
|
+
descriptionHi: 'ऑटोमैटिक टास्क सेट करें',
|
|
752
|
+
riskLevel: 'high',
|
|
753
|
+
category: 'system',
|
|
754
|
+
parameters: {
|
|
755
|
+
taskName: { type: 'string', required: true, description: 'Short name for the background task' },
|
|
756
|
+
prompt: { type: 'string', required: true, description: 'Detailed prompt/instructions of what the AI should do when the task runs' },
|
|
757
|
+
intervalMinutes: { type: 'number', required: true, description: 'How often to run in minutes. 0 means run exactly once.' },
|
|
758
|
+
startDelayMinutes: { type: 'number', required: false, description: 'Delay before first run in minutes. Default 0.' }
|
|
759
|
+
},
|
|
760
|
+
execute: async (args, userContext) => {
|
|
761
|
+
try {
|
|
762
|
+
const BackgroundJob = require('../../models/BackgroundJob');
|
|
763
|
+
const startDelay = args.startDelayMinutes || 0;
|
|
764
|
+
const nextRunAt = new Date(Date.now() + startDelay * 60000);
|
|
765
|
+
|
|
766
|
+
const job = new BackgroundJob({
|
|
767
|
+
userId: userContext._id || userContext.id,
|
|
768
|
+
taskName: args.taskName,
|
|
769
|
+
prompt: args.prompt,
|
|
770
|
+
intervalMinutes: args.intervalMinutes,
|
|
771
|
+
nextRunAt: nextRunAt
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
await job.save();
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
success: true,
|
|
778
|
+
result: `⏳ **Background Task Scheduled!**\n\n🎯 Task: ${args.taskName}\n🔄 Interval: ${args.intervalMinutes === 0 ? 'Run once' : `Every ${args.intervalMinutes}m`}\n🕒 Next run: ${nextRunAt.toLocaleString('en-IN', { timeZone: 'Asia/Kolkata' })} IST\n\n_The AI will autonomously execute this task in the background._`
|
|
779
|
+
};
|
|
780
|
+
} catch (err) {
|
|
781
|
+
return { success: false, result: `❌ Schedule failed: ${err.message}` };
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
|
|
786
|
+
// ─────────── SIMULATE TASK (Task Replay Mode) ───────────
|
|
787
|
+
simulate_task: {
|
|
788
|
+
name: 'simulate_task',
|
|
789
|
+
description: 'Simulate a complex action without actually executing it. Explain what would happen, files touched, and APIs called.',
|
|
790
|
+
descriptionHi: 'टास्क का सिमुलेशन करें',
|
|
791
|
+
riskLevel: 'low',
|
|
792
|
+
category: 'tool',
|
|
793
|
+
parameters: {
|
|
794
|
+
taskDecription: { type: 'string', required: true, description: 'Description of the task to simulate' }
|
|
795
|
+
},
|
|
796
|
+
execute: async (args, userContext) => {
|
|
797
|
+
return {
|
|
798
|
+
success: true,
|
|
799
|
+
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._`
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
// ────────────────────────────────────────────────────────────
|
|
806
|
+
// Tool Registry Class
|
|
807
|
+
// ────────────────────────────────────────────────────────────
|
|
808
|
+
|
|
809
|
+
class ToolRegistry {
|
|
810
|
+
constructor() {
|
|
811
|
+
this.tools = toolDefinitions;
|
|
812
|
+
this.loadPlugins();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async loadPlugins() {
|
|
816
|
+
try {
|
|
817
|
+
const pluginDir = path.join(BASE_DIR, 'plugins');
|
|
818
|
+
await fs.mkdir(pluginDir, { recursive: true });
|
|
819
|
+
const files = await fs.readdir(pluginDir);
|
|
820
|
+
let loaded = 0;
|
|
821
|
+
|
|
822
|
+
for (const file of files) {
|
|
823
|
+
if (file.endsWith('.js')) {
|
|
824
|
+
const pluginPath = path.join(pluginDir, file);
|
|
825
|
+
try {
|
|
826
|
+
const plugin = require(pluginPath);
|
|
827
|
+
if (plugin && plugin.name && typeof plugin.execute === 'function') {
|
|
828
|
+
this.registerTool(plugin.name, plugin);
|
|
829
|
+
loaded++;
|
|
830
|
+
}
|
|
831
|
+
} catch (e) {
|
|
832
|
+
console.error(`Failed to load plugin ${file}:`, e.message);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (loaded > 0) console.log(`🔌 Loaded ${loaded} custom plugins from ${pluginDir}`);
|
|
837
|
+
} catch (e) {
|
|
838
|
+
// Ignore if directory doesn't exist yet
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
getAllTools() {
|
|
843
|
+
return Object.values(this.tools);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
getToolsForPack(packName) {
|
|
847
|
+
const pack = TOOL_PACKS[packName];
|
|
848
|
+
if (!pack) return this.getAllTools();
|
|
849
|
+
return pack.tools
|
|
850
|
+
.map(toolName => this.tools[toolName])
|
|
851
|
+
.filter(Boolean);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
getTool(name) {
|
|
855
|
+
return this.tools[name] || null;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async executeTool(name, args, user) {
|
|
859
|
+
const tool = this.getTool(name);
|
|
860
|
+
if (!tool) {
|
|
861
|
+
return { success: false, result: `Tool "${name}" not found` };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
try {
|
|
865
|
+
const startTime = Date.now();
|
|
866
|
+
const result = await tool.execute(args, user);
|
|
867
|
+
result.executionTime = Date.now() - startTime;
|
|
868
|
+
return result;
|
|
869
|
+
} catch (error) {
|
|
870
|
+
return { success: false, result: `Tool execution error: ${error.message}` };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
registerTool(name, definition) {
|
|
875
|
+
this.tools[name] = definition;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
module.exports = new ToolRegistry();
|