samarthya-bot 1.1.4 → 2.0.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.
@@ -118,7 +118,13 @@ NEVER SAY YOU PERFORMED AN ACTION (like sending an email or saving a file) WITHO
118
118
  } else if (provider === 'mistral') {
119
119
  return this.chatOpenAICompatible(messages, systemPrompt, user, 'https://api.mistral.ai/v1/chat/completions', process.env.MISTRAL_API_KEY, customModel || 'mistral-large-latest');
120
120
  } else if (provider === 'anthropic') {
121
- return this.chatGemini(messages, systemPrompt, user, customModel || 'gemini-2.5-flash'); // fallback to Gemini for Anthropic until SDK is added
121
+ return this.chatAnthropic(messages, systemPrompt, user, customModel || 'claude-sonnet-4-20250514');
122
+ } else if (provider === 'deepseek') {
123
+ return this.chatOpenAICompatible(messages, systemPrompt, user, 'https://api.deepseek.com/v1/chat/completions', process.env.DEEPSEEK_API_KEY, customModel || 'deepseek-chat');
124
+ } else if (provider === 'qwen') {
125
+ return this.chatOpenAICompatible(messages, systemPrompt, user, 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', process.env.QWEN_API_KEY, customModel || 'qwen-plus');
126
+ } else if (provider === 'openrouter') {
127
+ return this.chatOpenRouter(messages, systemPrompt, user, customModel || 'google/gemini-2.5-flash');
122
128
  }
123
129
 
124
130
  return this.chatGemini(messages, systemPrompt, user, customModel || 'gemini-2.5-flash');
@@ -292,6 +298,117 @@ NEVER SAY YOU PERFORMED AN ACTION (like sending an email or saving a file) WITHO
292
298
  }
293
299
  }
294
300
 
301
+ /**
302
+ * Anthropic Claude API (Native Messages API)
303
+ * Claude uses a different API format than OpenAI
304
+ */
305
+ async chatAnthropic(messages, systemPrompt, user = null, modelName = 'claude-sonnet-4-20250514') {
306
+ try {
307
+ const apiKey = process.env.ANTHROPIC_API_KEY;
308
+ if (!apiKey || apiKey === 'dummy') {
309
+ return this.getFallbackResponse(messages[messages.length - 1]?.content, user);
310
+ }
311
+
312
+ // Claude uses a different message format
313
+ const claudeMessages = messages
314
+ .filter(m => m.role !== 'system')
315
+ .map(m => ({
316
+ role: m.role === 'assistant' ? 'assistant' : 'user',
317
+ content: m.content
318
+ }));
319
+
320
+ // Ensure first message is from user
321
+ if (claudeMessages.length === 0 || claudeMessages[0].role !== 'user') {
322
+ claudeMessages.unshift({ role: 'user', content: 'Hello' });
323
+ }
324
+
325
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
326
+ method: 'POST',
327
+ headers: {
328
+ 'Content-Type': 'application/json',
329
+ 'x-api-key': apiKey,
330
+ 'anthropic-version': '2023-06-01'
331
+ },
332
+ body: JSON.stringify({
333
+ model: modelName,
334
+ max_tokens: 8192,
335
+ system: systemPrompt,
336
+ messages: claudeMessages,
337
+ temperature: 0.7
338
+ })
339
+ });
340
+
341
+ if (!response.ok) {
342
+ const errText = await response.text();
343
+ console.error('Anthropic API Error:', errText);
344
+ return this.getFallbackResponse(messages[messages.length - 1]?.content, user);
345
+ }
346
+
347
+ const data = await response.json();
348
+ const textContent = data.content?.find(c => c.type === 'text');
349
+ return {
350
+ content: textContent?.text || 'Empty response from Claude',
351
+ tokensUsed: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0),
352
+ model: modelName
353
+ };
354
+ } catch (error) {
355
+ console.error('Anthropic Error:', error.message);
356
+ return this.getFallbackResponse(messages[messages.length - 1]?.content, user);
357
+ }
358
+ }
359
+
360
+ /**
361
+ * OpenRouter API — access 100+ models through a single API key
362
+ * Uses OpenAI-compatible format with extra headers
363
+ */
364
+ async chatOpenRouter(messages, systemPrompt, user = null, modelName = 'google/gemini-2.5-flash') {
365
+ try {
366
+ const apiKey = process.env.OPENROUTER_API_KEY;
367
+ if (!apiKey || apiKey === 'dummy') {
368
+ return this.getFallbackResponse(messages[messages.length - 1]?.content, user);
369
+ }
370
+
371
+ const apiMessages = [
372
+ { role: 'system', content: systemPrompt },
373
+ ...messages.map(m => ({
374
+ role: m.role === 'assistant' ? 'assistant' : 'user',
375
+ content: m.content
376
+ }))
377
+ ];
378
+
379
+ const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
380
+ method: 'POST',
381
+ headers: {
382
+ 'Content-Type': 'application/json',
383
+ 'Authorization': `Bearer ${apiKey}`,
384
+ 'HTTP-Referer': 'https://github.com/mebishnusahu0595/SamarthyaBot',
385
+ 'X-Title': 'SamarthyaBot'
386
+ },
387
+ body: JSON.stringify({
388
+ model: modelName,
389
+ messages: apiMessages,
390
+ temperature: 0.7
391
+ })
392
+ });
393
+
394
+ if (!response.ok) {
395
+ const errText = await response.text();
396
+ console.error('OpenRouter API Error:', errText);
397
+ return this.getFallbackResponse(messages[messages.length - 1]?.content, user);
398
+ }
399
+
400
+ const data = await response.json();
401
+ return {
402
+ content: data.choices?.[0]?.message?.content || 'Empty response from OpenRouter',
403
+ tokensUsed: data.usage?.total_tokens || 0,
404
+ model: `openrouter:${modelName}`
405
+ };
406
+ } catch (error) {
407
+ console.error('OpenRouter Error:', error.message);
408
+ return this.getFallbackResponse(messages[messages.length - 1]?.content, user);
409
+ }
410
+ }
411
+
295
412
  /**
296
413
  * Screen Understanding via Gemini Vision
297
414
  * Takes a base64 screenshot and analyzes it
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Workspace Security Sandbox for SamarthyaBot
3
+ * Restricts file and command operations to the configured workspace
4
+ * Inspired by PicoClaw's restrict_to_workspace feature
5
+ */
6
+
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ class SandboxService {
11
+ constructor() {
12
+ this.enabled = process.env.RESTRICT_TO_WORKSPACE !== 'false'; // default: true
13
+ this.workspace = process.env.WORKSPACE_PATH || path.join(os.homedir(), 'SamarthyaBot_Files');
14
+
15
+ // Dangerous command patterns (always blocked, even if sandbox is off)
16
+ this.blockedPatterns = [
17
+ /rm\s+(-rf|-fr|--no-preserve-root)\s/i,
18
+ /del\s+\/f/i,
19
+ /rmdir\s+\/s/i,
20
+ /format\s+/i,
21
+ /mkfs\s/i,
22
+ /diskpart/i,
23
+ /dd\s+if=/i,
24
+ /\/dev\/sd[a-z]/i,
25
+ /shutdown/i,
26
+ /reboot/i,
27
+ /poweroff/i,
28
+ /init\s+[06]/i,
29
+ /:()\{\s*:\|:&\s*\};:/, // Fork bomb
30
+ />\s*\/dev\/sda/i,
31
+ /mv\s+\/\s/i, // mv / (moving root)
32
+ /chmod\s+-R\s+777\s+\//i, // chmod 777 on root
33
+ ];
34
+ }
35
+
36
+ /**
37
+ * Validate that a file path is within the allowed workspace
38
+ * @param {string} filePath - The file path to validate
39
+ * @returns {{ allowed: boolean, reason: string }}
40
+ */
41
+ validatePath(filePath) {
42
+ if (!this.enabled) return { allowed: true, reason: 'Sandbox disabled' };
43
+
44
+ const resolvedPath = path.resolve(filePath);
45
+ const resolvedWorkspace = path.resolve(this.workspace);
46
+
47
+ if (resolvedPath.startsWith(resolvedWorkspace)) {
48
+ return { allowed: true, reason: 'Path is within workspace' };
49
+ }
50
+
51
+ // Allow /tmp for scratch files
52
+ if (resolvedPath.startsWith('/tmp') || resolvedPath.startsWith(os.tmpdir())) {
53
+ return { allowed: true, reason: 'Path is in temp directory' };
54
+ }
55
+
56
+ return {
57
+ allowed: false,
58
+ reason: `🔒 Security: Path "${filePath}" is outside the workspace. Only files within "${this.workspace}" can be accessed. Set RESTRICT_TO_WORKSPACE=false in .env to disable this restriction.`
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Validate that a command is safe to execute
64
+ * @param {string} command - The shell command to validate
65
+ * @returns {{ allowed: boolean, reason: string }}
66
+ */
67
+ validateCommand(command) {
68
+ // Always check blocked patterns (even if sandbox is disabled)
69
+ for (const pattern of this.blockedPatterns) {
70
+ if (pattern.test(command)) {
71
+ return {
72
+ allowed: false,
73
+ reason: `🛡️ Safety Guard: Command blocked — dangerous pattern detected ("${command.substring(0, 50)}..."). This command could harm your system.`
74
+ };
75
+ }
76
+ }
77
+
78
+ // If sandbox is enabled, check that command paths are within workspace
79
+ if (this.enabled) {
80
+ // Extract paths from command (basic heuristic)
81
+ const pathRegex = /(?:^|\s)(\/[^\s;|&]+)/g;
82
+ let match;
83
+ while ((match = pathRegex.exec(command)) !== null) {
84
+ const cmdPath = match[1];
85
+ // Skip common system binaries
86
+ if (cmdPath.startsWith('/usr/') || cmdPath.startsWith('/bin/') ||
87
+ cmdPath.startsWith('/sbin/') || cmdPath.startsWith('/tmp/')) {
88
+ continue;
89
+ }
90
+ const validation = this.validatePath(cmdPath);
91
+ if (!validation.allowed) {
92
+ return {
93
+ allowed: false,
94
+ reason: `🔒 Security: Command references path outside workspace — ${validation.reason}`
95
+ };
96
+ }
97
+ }
98
+ }
99
+
100
+ return { allowed: true, reason: 'Command is safe' };
101
+ }
102
+
103
+ /**
104
+ * Get workspace info for status display
105
+ */
106
+ getStatus() {
107
+ return {
108
+ enabled: this.enabled,
109
+ workspace: this.workspace,
110
+ blockedPatterns: this.blockedPatterns.length
111
+ };
112
+ }
113
+ }
114
+
115
+ module.exports = new SandboxService();
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Voice Transcription Service for SamarthyaBot
3
+ * Uses Groq Whisper API to convert voice notes to text
4
+ * Supports OGG (Telegram), MP3, WAV, M4A formats
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ class VoiceService {
11
+ constructor() {
12
+ this.groqApiKey = process.env.GROQ_API_KEY;
13
+ this.model = 'whisper-large-v3-turbo'; // Groq's fast Whisper model
14
+ }
15
+
16
+ /**
17
+ * Transcribe an audio file using Groq Whisper API
18
+ * @param {string|Buffer} audioInput - File path or Buffer of audio data
19
+ * @param {string} fileName - Original filename (for MIME type detection)
20
+ * @param {string} language - Language hint (e.g., 'hi' for Hindi, 'en' for English)
21
+ * @returns {Promise<{text: string, language: string, duration: number}>}
22
+ */
23
+ async transcribe(audioInput, fileName = 'audio.ogg', language = null) {
24
+ if (!this.groqApiKey || this.groqApiKey === 'your_groq_api_key') {
25
+ return {
26
+ text: null,
27
+ error: 'Voice transcription not available — GROQ_API_KEY not configured. Get a free key at https://console.groq.com'
28
+ };
29
+ }
30
+
31
+ try {
32
+ let audioBuffer;
33
+ if (typeof audioInput === 'string') {
34
+ // It's a file path
35
+ audioBuffer = fs.readFileSync(audioInput);
36
+ } else {
37
+ audioBuffer = audioInput;
38
+ }
39
+
40
+ // Build multipart form data manually (no external dependency)
41
+ const boundary = '----SamarthyaBotVoice' + Date.now();
42
+ const mimeType = this.getMimeType(fileName);
43
+
44
+ const bodyParts = [];
45
+
46
+ // File part
47
+ bodyParts.push(
48
+ `--${boundary}\r\n`,
49
+ `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n`,
50
+ `Content-Type: ${mimeType}\r\n\r\n`
51
+ );
52
+
53
+ const headerBuffer = Buffer.from(bodyParts.join(''));
54
+ const footerParts = [`\r\n--${boundary}\r\n`,
55
+ `Content-Disposition: form-data; name="model"\r\n\r\n`,
56
+ `${this.model}\r\n`
57
+ ];
58
+
59
+ if (language) {
60
+ footerParts.push(
61
+ `--${boundary}\r\n`,
62
+ `Content-Disposition: form-data; name="language"\r\n\r\n`,
63
+ `${language}\r\n`
64
+ );
65
+ }
66
+
67
+ footerParts.push(
68
+ `--${boundary}\r\n`,
69
+ `Content-Disposition: form-data; name="response_format"\r\n\r\n`,
70
+ `verbose_json\r\n`,
71
+ `--${boundary}--\r\n`
72
+ );
73
+
74
+ const footerBuffer = Buffer.from(footerParts.join(''));
75
+ const body = Buffer.concat([headerBuffer, audioBuffer, footerBuffer]);
76
+
77
+ const response = await fetch('https://api.groq.com/openai/v1/audio/transcriptions', {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Authorization': `Bearer ${this.groqApiKey}`,
81
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`
82
+ },
83
+ body: body
84
+ });
85
+
86
+ if (!response.ok) {
87
+ const errText = await response.text();
88
+ console.error('❌ Voice transcription error:', errText);
89
+ return { text: null, error: `Transcription failed: ${response.status}` };
90
+ }
91
+
92
+ const data = await response.json();
93
+ console.log(`🎙️ Voice transcribed: "${(data.text || '').substring(0, 60)}..." (${data.language || 'auto'}, ${Math.round(data.duration || 0)}s)`);
94
+
95
+ return {
96
+ text: data.text || '',
97
+ language: data.language || 'unknown',
98
+ duration: data.duration || 0
99
+ };
100
+ } catch (error) {
101
+ console.error('❌ Voice service error:', error.message);
102
+ return { text: null, error: error.message };
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Download a file from URL and transcribe it (for Telegram voice notes)
108
+ * @param {string} fileUrl - URL of the audio file
109
+ * @param {string} language - Language hint
110
+ */
111
+ async transcribeFromUrl(fileUrl, language = null) {
112
+ try {
113
+ const response = await fetch(fileUrl);
114
+ if (!response.ok) {
115
+ return { text: null, error: `Failed to download audio: ${response.status}` };
116
+ }
117
+
118
+ const arrayBuffer = await response.arrayBuffer();
119
+ const buffer = Buffer.from(arrayBuffer);
120
+
121
+ // Extract filename from URL
122
+ const urlParts = fileUrl.split('/');
123
+ const fileName = urlParts[urlParts.length - 1] || 'audio.oga';
124
+
125
+ return this.transcribe(buffer, fileName, language);
126
+ } catch (error) {
127
+ console.error('❌ Voice download error:', error.message);
128
+ return { text: null, error: error.message };
129
+ }
130
+ }
131
+
132
+ getMimeType(fileName) {
133
+ const ext = path.extname(fileName).toLowerCase();
134
+ const mimeMap = {
135
+ '.ogg': 'audio/ogg',
136
+ '.oga': 'audio/ogg',
137
+ '.mp3': 'audio/mpeg',
138
+ '.wav': 'audio/wav',
139
+ '.m4a': 'audio/m4a',
140
+ '.webm': 'audio/webm',
141
+ '.flac': 'audio/flac'
142
+ };
143
+ return mimeMap[ext] || 'audio/ogg';
144
+ }
145
+
146
+ isAvailable() {
147
+ return !!(this.groqApiKey && this.groqApiKey !== 'your_groq_api_key');
148
+ }
149
+ }
150
+
151
+ module.exports = new VoiceService();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "samarthya-bot",
3
- "version": "1.1.4",
3
+ "version": "2.0.0",
4
4
  "description": "Privacy-First Local Agentic OS & Command Center",
5
5
  "main": "backend/server.js",
6
6
  "bin": {
@@ -47,4 +47,4 @@
47
47
  "type": "git",
48
48
  "url": "https://github.com/mebishnusahu0595/SamarthyaBot.git"
49
49
  }
50
- }
50
+ }
package/server.log ADDED
@@ -0,0 +1,35 @@
1
+
2
+ > samarthya-agent@1.0.0 start
3
+ > node server.js
4
+
5
+ [dotenv@17.3.1] injecting env (0) from .env -- tip: 🤖 agentic secret storage: https://dotenvx.com/as2
6
+ ✅ MongoDB Connected: localhost
7
+
8
+ ╔══════════════════════════════════════════════════════╗
9
+ ║ ║
10
+ ║ 🧠 SamarthyaBot Server v1.1.0 ║
11
+ ║ Privacy-first Personal AI Operator ║
12
+ ║ ║
13
+ ║ 🌐 Server: http://localhost:5000 ║
14
+ ║ 📡 Socket: ws://localhost:5000 ║
15
+ ║ 🔗 Health: http://localhost:5000/api/health ║
16
+ ║ 📱 WhatsApp: /api/whatsapp/webhook ║
17
+ ║ 🤖 Telegram: /api/telegram/webhook ║
18
+ ║ 👁️ Vision: /api/screen/analyze ║
19
+ ║ ║
20
+ ║ 🇮🇳 Built for Indian Workflows ║
21
+ ║ 📦 Ollama: ❌ Disabled ║
22
+ ║ 🔄 Autonomous Background Engine: ✅ Active ║
23
+ ║ 🔌 Dynamic Plugin Engine: ✅ Active ║
24
+ ║ 🧠 Active AI: GEMINI (gemini-2.5-flash) ║
25
+ ║ ║
26
+ ╚══════════════════════════════════════════════════════╝
27
+
28
+ 🔄 Background Autonomous Mode Started (Checking every 1 minute)
29
+ GET /api/health 200 13.413 ms - 121
30
+ GET / 200 5.827 ms - 947
31
+ GET /assets/index-J7XSVHCz.css 200 3.896 ms - 7514
32
+ GET /assets/index-BFRAq8Y1.js 200 5.924 ms - 409640
33
+ GET /logo.png 200 1.936 ms - 395392
34
+ GET /api/auth/profile 200 31.161 ms - 457
35
+ GET /favicon.svg 200 1.891 ms - 847