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.
- package/README.md +274 -98
- package/backend/.env.example +31 -7
- package/backend/bin/samarthya.js +114 -25
- package/backend/package-lock.json +47 -25
- package/backend/package.json +4 -3
- package/backend/public/assets/index-BFRAq8Y1.js +149 -0
- package/backend/public/index.html +1 -1
- package/backend/server.js +70 -22
- package/backend/server.log +29 -0
- package/backend/services/agent/spawnService.js +140 -0
- package/backend/services/discord/discordService.js +188 -0
- package/backend/services/heartbeat/heartbeatService.js +157 -0
- package/backend/services/llm/llmService.js +118 -1
- package/backend/services/security/sandboxService.js +115 -0
- package/backend/services/voice/voiceService.js +151 -0
- package/package.json +2 -2
- package/server.log +35 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<link rel="manifest" href="/manifest.json" />
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Noto+Sans+Devanagari:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
11
11
|
<title>SamarthyaBot - समर्थ्य बोट | Personal AI Operator</title>
|
|
12
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-BFRAq8Y1.js"></script>
|
|
13
13
|
<link rel="stylesheet" crossorigin href="/assets/index-J7XSVHCz.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
package/backend/server.js
CHANGED
|
@@ -19,6 +19,13 @@ const fileRoutes = require('./routes/files');
|
|
|
19
19
|
const platformRoutes = require('./routes/platform');
|
|
20
20
|
const backgroundService = require('./services/background/backgroundService');
|
|
21
21
|
|
|
22
|
+
// New PicoClaw-inspired services
|
|
23
|
+
const discordService = require('./services/discord/discordService');
|
|
24
|
+
const sandboxService = require('./services/security/sandboxService');
|
|
25
|
+
const heartbeatService = require('./services/heartbeat/heartbeatService');
|
|
26
|
+
const voiceService = require('./services/voice/voiceService');
|
|
27
|
+
const spawnService = require('./services/agent/spawnService');
|
|
28
|
+
|
|
22
29
|
const app = express();
|
|
23
30
|
const server = http.createServer(app);
|
|
24
31
|
|
|
@@ -58,14 +65,20 @@ app.use('/api/platform', platformRoutes);
|
|
|
58
65
|
// Serve static frontend UI (SamarthyaBot Dashboard)
|
|
59
66
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
60
67
|
|
|
61
|
-
// Health check
|
|
68
|
+
// Health check — now includes all service statuses
|
|
62
69
|
app.get('/api/health', (req, res) => {
|
|
63
70
|
res.json({
|
|
64
71
|
status: 'ok',
|
|
65
72
|
name: 'SamarthyaBot Server',
|
|
66
|
-
version: '
|
|
73
|
+
version: '2.0.0',
|
|
67
74
|
uptime: process.uptime(),
|
|
68
|
-
timestamp: new Date().toISOString()
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
services: {
|
|
77
|
+
sandbox: sandboxService.getStatus(),
|
|
78
|
+
heartbeat: heartbeatService.getStatus(),
|
|
79
|
+
voice: voiceService.isAvailable(),
|
|
80
|
+
spawn: spawnService.getStatus()
|
|
81
|
+
}
|
|
69
82
|
});
|
|
70
83
|
});
|
|
71
84
|
|
|
@@ -102,28 +115,63 @@ connectDB().then(() => {
|
|
|
102
115
|
const activeModel = process.env.ACTIVE_MODEL || 'gemini-2.5-flash';
|
|
103
116
|
const aiString = `🧠 Active AI: ${activeProvider} (${activeModel})`.padEnd(49, ' ');
|
|
104
117
|
|
|
118
|
+
const discordStatus = process.env.DISCORD_BOT_TOKEN ? '✅ Active' : '⚪ No Token';
|
|
119
|
+
const voiceStatus = voiceService.isAvailable() ? '✅ Groq Whisper' : '⚪ Not Configured';
|
|
120
|
+
const sandboxStatus = sandboxService.enabled ? '✅ Enabled' : '⚠️ Disabled';
|
|
121
|
+
const heartbeatStatus = heartbeatService.enabled ? `✅ Every ${heartbeatService.intervalMinutes}min` : '⚪ Disabled';
|
|
122
|
+
|
|
105
123
|
console.log(`
|
|
106
|
-
|
|
107
|
-
║
|
|
108
|
-
║
|
|
109
|
-
║ Privacy-first Personal AI Operator
|
|
110
|
-
║
|
|
111
|
-
║ 🌐 Server: http://localhost:${PORT}
|
|
112
|
-
║ 📡 Socket: ws://localhost:${PORT}
|
|
113
|
-
║ 🔗 Health: http://localhost:${PORT}/api/health
|
|
114
|
-
║
|
|
115
|
-
║
|
|
116
|
-
║
|
|
117
|
-
║
|
|
118
|
-
║
|
|
119
|
-
║
|
|
120
|
-
║
|
|
121
|
-
║
|
|
122
|
-
║ ${aiString}
|
|
123
|
-
║
|
|
124
|
-
|
|
124
|
+
╔══════════════════════════════════════════════════════════╗
|
|
125
|
+
║ ║
|
|
126
|
+
║ 🇮🇳 SamarthyaBot Server v2.0.0 ║
|
|
127
|
+
║ Privacy-first Personal AI Operator · Made in India ║
|
|
128
|
+
║ ║
|
|
129
|
+
║ 🌐 Server: http://localhost:${String(PORT).padEnd(25)}║
|
|
130
|
+
║ 📡 Socket: ws://localhost:${String(PORT).padEnd(26)}║
|
|
131
|
+
║ 🔗 Health: http://localhost:${PORT}/api/health${' '.repeat(8)}║
|
|
132
|
+
║ ║
|
|
133
|
+
║ ── Channels ────────────────────────────────────── ║
|
|
134
|
+
║ 🤖 Telegram: /api/telegram/webhook ║
|
|
135
|
+
║ 🟣 Discord: ${discordStatus.padEnd(39)}║
|
|
136
|
+
║ 📱 WhatsApp: /api/whatsapp/webhook ║
|
|
137
|
+
║ 👁️ Vision: /api/screen/analyze ║
|
|
138
|
+
║ ║
|
|
139
|
+
║ ── AI Engine ───────────────────────────────────── ║
|
|
140
|
+
║ ${aiString.padEnd(55)}║
|
|
141
|
+
║ 📦 Ollama: ${(process.env.USE_OLLAMA === 'true' ? '✅ Enabled' : '❌ Disabled').padEnd(39)}║
|
|
142
|
+
║ 🎙️ Voice: ${voiceStatus.padEnd(38)}║
|
|
143
|
+
║ ║
|
|
144
|
+
║ ── Services ────────────────────────────────────── ║
|
|
145
|
+
║ 🔄 Background Engine: ✅ Active ║
|
|
146
|
+
║ 🔌 Plugin Engine: ✅ Active ║
|
|
147
|
+
║ 🔒 Workspace Sandbox: ${sandboxStatus.padEnd(31)}║
|
|
148
|
+
║ 💓 Heartbeat: ${heartbeatStatus.padEnd(31)}║
|
|
149
|
+
║ 🚀 Sub-Agent Spawn: ✅ Ready ║
|
|
150
|
+
║ ║
|
|
151
|
+
║ 🇮🇳 Built with ❤️ in India by Bishnu Sahu ║
|
|
152
|
+
║ ║
|
|
153
|
+
╚══════════════════════════════════════════════════════════╝
|
|
125
154
|
`);
|
|
155
|
+
|
|
156
|
+
// Start all services
|
|
126
157
|
backgroundService.start();
|
|
158
|
+
|
|
159
|
+
// Start Discord bot (if configured)
|
|
160
|
+
discordService.start(async (message, userId, channel) => {
|
|
161
|
+
// Simple handler — route Discord messages through the chat pipeline
|
|
162
|
+
// This will be connected to your chatController in production
|
|
163
|
+
const chatController = require('./controllers/chatController');
|
|
164
|
+
if (chatController.handleExternalMessage) {
|
|
165
|
+
return chatController.handleExternalMessage(message, userId, channel);
|
|
166
|
+
}
|
|
167
|
+
return 'Namaste! Main SamarthyaBot hoon. Chat service starting up... 🚀';
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Start Heartbeat periodic tasks
|
|
171
|
+
heartbeatService.start(async (task, userId, channel) => {
|
|
172
|
+
console.log(`💓 Heartbeat executing: ${task.substring(0, 50)}`);
|
|
173
|
+
return null; // Tasks are logged, full pipeline integration comes next
|
|
174
|
+
});
|
|
127
175
|
});
|
|
128
176
|
}).catch(err => {
|
|
129
177
|
console.error('Failed to connect to database:', err);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
> samarthya-agent@1.0.0 start
|
|
3
|
+
> node server.js
|
|
4
|
+
|
|
5
|
+
[dotenv@17.3.1] injecting env (0) from .env -- tip: 🔐 prevent committing .env to code: https://dotenvx.com/precommit
|
|
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
|
+
[0mGET /api/health [32m200[0m 10.663 ms - 122[0m
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn (Sub-Agent) Service for SamarthyaBot
|
|
3
|
+
* Creates independent background agents for long-running tasks
|
|
4
|
+
* Inspired by PicoClaw's spawn feature
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
class SpawnService {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.activeAgents = new Map(); // id -> { worker, status, startTime, task }
|
|
13
|
+
this.maxConcurrent = parseInt(process.env.MAX_SPAWN_AGENTS || '5', 10);
|
|
14
|
+
this.idCounter = 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Spawn a new sub-agent to handle a long-running task
|
|
19
|
+
* @param {string} task - The task description
|
|
20
|
+
* @param {Function} chatHandler - The chat processing function
|
|
21
|
+
* @param {Function} onComplete - Callback when task completes
|
|
22
|
+
* @returns {{ id: string, status: string }}
|
|
23
|
+
*/
|
|
24
|
+
spawn(task, chatHandler, onComplete) {
|
|
25
|
+
if (this.activeAgents.size >= this.maxConcurrent) {
|
|
26
|
+
return {
|
|
27
|
+
id: null,
|
|
28
|
+
status: 'rejected',
|
|
29
|
+
reason: `Maximum concurrent agents reached (${this.maxConcurrent}). Wait for a running agent to finish.`
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const agentId = `spawn-${++this.idCounter}-${Date.now()}`;
|
|
34
|
+
|
|
35
|
+
const agentContext = {
|
|
36
|
+
id: agentId,
|
|
37
|
+
task: task,
|
|
38
|
+
status: 'running',
|
|
39
|
+
startTime: Date.now(),
|
|
40
|
+
result: null,
|
|
41
|
+
error: null
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
this.activeAgents.set(agentId, agentContext);
|
|
45
|
+
|
|
46
|
+
console.log(`🚀 Spawn: Agent ${agentId} started — "${task.substring(0, 60)}..."`);
|
|
47
|
+
|
|
48
|
+
// Execute asynchronously without blocking
|
|
49
|
+
(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const result = await chatHandler(
|
|
52
|
+
`[SPAWNED SUB-AGENT TASK] You are a background sub-agent. Complete this task independently and return only the result: ${task}`,
|
|
53
|
+
`spawn-${agentId}`,
|
|
54
|
+
'spawn'
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
agentContext.status = 'completed';
|
|
58
|
+
agentContext.result = result;
|
|
59
|
+
console.log(`✅ Spawn: Agent ${agentId} completed`);
|
|
60
|
+
|
|
61
|
+
if (onComplete) onComplete(agentId, result);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
agentContext.status = 'failed';
|
|
64
|
+
agentContext.error = error.message;
|
|
65
|
+
console.error(`❌ Spawn: Agent ${agentId} failed:`, error.message);
|
|
66
|
+
|
|
67
|
+
if (onComplete) onComplete(agentId, { error: error.message });
|
|
68
|
+
}
|
|
69
|
+
})();
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
id: agentId,
|
|
73
|
+
status: 'spawned',
|
|
74
|
+
message: `🚀 Sub-agent spawned (ID: ${agentId}). It will work in the background and report results when done.`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get status of a specific agent
|
|
80
|
+
*/
|
|
81
|
+
getAgentStatus(agentId) {
|
|
82
|
+
const agent = this.activeAgents.get(agentId);
|
|
83
|
+
if (!agent) return null;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
id: agent.id,
|
|
87
|
+
task: agent.task,
|
|
88
|
+
status: agent.status,
|
|
89
|
+
elapsed: `${Math.round((Date.now() - agent.startTime) / 1000)}s`,
|
|
90
|
+
result: agent.result,
|
|
91
|
+
error: agent.error
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* List all active agents
|
|
97
|
+
*/
|
|
98
|
+
listAgents() {
|
|
99
|
+
const agents = [];
|
|
100
|
+
for (const [id, agent] of this.activeAgents) {
|
|
101
|
+
agents.push({
|
|
102
|
+
id: agent.id,
|
|
103
|
+
task: agent.task.substring(0, 80),
|
|
104
|
+
status: agent.status,
|
|
105
|
+
elapsed: `${Math.round((Date.now() - agent.startTime) / 1000)}s`
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return agents;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Clean up completed agents (keep last 10)
|
|
113
|
+
*/
|
|
114
|
+
cleanup() {
|
|
115
|
+
const completed = [];
|
|
116
|
+
for (const [id, agent] of this.activeAgents) {
|
|
117
|
+
if (agent.status === 'completed' || agent.status === 'failed') {
|
|
118
|
+
completed.push(id);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Keep last 10 completed, remove the rest
|
|
123
|
+
if (completed.length > 10) {
|
|
124
|
+
const toRemove = completed.slice(0, completed.length - 10);
|
|
125
|
+
for (const id of toRemove) {
|
|
126
|
+
this.activeAgents.delete(id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getStatus() {
|
|
132
|
+
return {
|
|
133
|
+
active: [...this.activeAgents.values()].filter(a => a.status === 'running').length,
|
|
134
|
+
total: this.activeAgents.size,
|
|
135
|
+
maxConcurrent: this.maxConcurrent
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = new SpawnService();
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord Channel Service for SamarthyaBot
|
|
3
|
+
* Full Discord bot integration with mention-only mode support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let discordReady = false;
|
|
7
|
+
let discordClient = null;
|
|
8
|
+
|
|
9
|
+
class DiscordService {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.token = process.env.DISCORD_BOT_TOKEN;
|
|
12
|
+
this.allowFrom = (process.env.DISCORD_ALLOW_FROM || '').split(',').filter(Boolean);
|
|
13
|
+
this.mentionOnly = process.env.DISCORD_MENTION_ONLY === 'true';
|
|
14
|
+
this.ws = null;
|
|
15
|
+
this.heartbeatInterval = null;
|
|
16
|
+
this.sessionId = null;
|
|
17
|
+
this.resumeGatewayUrl = null;
|
|
18
|
+
this.seq = null;
|
|
19
|
+
this.botUserId = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the Discord bot (no external dependencies — uses raw WebSocket Gateway API)
|
|
24
|
+
*/
|
|
25
|
+
async start(chatHandler) {
|
|
26
|
+
if (!this.token || this.token === 'your_discord_bot_token') {
|
|
27
|
+
console.log('⚪ Discord: Skipped (no token configured)');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.chatHandler = chatHandler;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// Get gateway URL
|
|
35
|
+
const response = await fetch('https://discord.com/api/v10/gateway/bot', {
|
|
36
|
+
headers: { 'Authorization': `Bot ${this.token}` }
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
console.error('❌ Discord: Failed to get gateway URL:', await response.text());
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
this.connectToGateway(data.url);
|
|
46
|
+
console.log('🟣 Discord: Connecting...');
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('❌ Discord: Connection error:', error.message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
connectToGateway(url) {
|
|
53
|
+
// Dynamic import for WebSocket (Node.js built-in in v22+, or use ws package)
|
|
54
|
+
const WebSocket = require('ws') || globalThis.WebSocket;
|
|
55
|
+
this.ws = new WebSocket(`${url}?v=10&encoding=json`);
|
|
56
|
+
|
|
57
|
+
this.ws.on('open', () => {
|
|
58
|
+
console.log('🟣 Discord: WebSocket connected');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.ws.on('message', (data) => {
|
|
62
|
+
const payload = JSON.parse(data.toString());
|
|
63
|
+
this.handleGatewayEvent(payload);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.ws.on('close', (code) => {
|
|
67
|
+
console.log(`🟣 Discord: WebSocket closed (${code}). Reconnecting in 5s...`);
|
|
68
|
+
clearInterval(this.heartbeatInterval);
|
|
69
|
+
setTimeout(() => this.connectToGateway(url), 5000);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
this.ws.on('error', (err) => {
|
|
73
|
+
console.error('❌ Discord WebSocket error:', err.message);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
handleGatewayEvent(payload) {
|
|
78
|
+
const { op, d, s, t } = payload;
|
|
79
|
+
|
|
80
|
+
if (s) this.seq = s;
|
|
81
|
+
|
|
82
|
+
switch (op) {
|
|
83
|
+
case 10: // Hello — start heartbeat and identify
|
|
84
|
+
const heartbeatMs = d.heartbeat_interval;
|
|
85
|
+
this.heartbeatInterval = setInterval(() => {
|
|
86
|
+
this.ws.send(JSON.stringify({ op: 1, d: this.seq }));
|
|
87
|
+
}, heartbeatMs);
|
|
88
|
+
|
|
89
|
+
// Identify
|
|
90
|
+
this.ws.send(JSON.stringify({
|
|
91
|
+
op: 2,
|
|
92
|
+
d: {
|
|
93
|
+
token: this.token,
|
|
94
|
+
intents: 513 | 32768, // GUILDS + MESSAGE_CONTENT
|
|
95
|
+
properties: {
|
|
96
|
+
os: 'linux',
|
|
97
|
+
browser: 'samarthyabot',
|
|
98
|
+
device: 'samarthyabot'
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
102
|
+
break;
|
|
103
|
+
|
|
104
|
+
case 11: // Heartbeat ACK
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case 0: // Dispatch
|
|
108
|
+
if (t === 'READY') {
|
|
109
|
+
this.sessionId = d.session_id;
|
|
110
|
+
this.resumeGatewayUrl = d.resume_gateway_url;
|
|
111
|
+
this.botUserId = d.user?.id;
|
|
112
|
+
discordReady = true;
|
|
113
|
+
discordClient = this;
|
|
114
|
+
console.log(`🟣 Discord: ✅ Bot ready as ${d.user?.username}#${d.user?.discriminator}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (t === 'MESSAGE_CREATE') {
|
|
118
|
+
this.handleMessage(d);
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async handleMessage(message) {
|
|
125
|
+
// Ignore bot's own messages
|
|
126
|
+
if (message.author?.id === this.botUserId) return;
|
|
127
|
+
if (message.author?.bot) return;
|
|
128
|
+
|
|
129
|
+
// Allow-list check
|
|
130
|
+
if (this.allowFrom.length > 0 && !this.allowFrom.includes(message.author?.id)) return;
|
|
131
|
+
|
|
132
|
+
// Mention-only mode
|
|
133
|
+
if (this.mentionOnly) {
|
|
134
|
+
const mentioned = message.mentions?.some(m => m.id === this.botUserId);
|
|
135
|
+
if (!mentioned) return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const content = message.content?.replace(/<@!?\d+>/g, '').trim();
|
|
139
|
+
if (!content) return;
|
|
140
|
+
|
|
141
|
+
console.log(`🟣 Discord msg from ${message.author?.username}: ${content.substring(0, 50)}...`);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// Use the chat handler to process the message
|
|
145
|
+
if (this.chatHandler) {
|
|
146
|
+
const response = await this.chatHandler(content, message.author?.id, 'discord');
|
|
147
|
+
await this.sendMessage(message.channel_id, response);
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('❌ Discord message handling error:', error.message);
|
|
151
|
+
await this.sendMessage(message.channel_id, '❌ Kuch error aa gaya processing mein. Please try again.');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async sendMessage(channelId, content) {
|
|
156
|
+
try {
|
|
157
|
+
// Discord message limit is 2000 chars
|
|
158
|
+
const chunks = this.splitMessage(content, 2000);
|
|
159
|
+
for (const chunk of chunks) {
|
|
160
|
+
await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: {
|
|
163
|
+
'Content-Type': 'application/json',
|
|
164
|
+
'Authorization': `Bot ${this.token}`
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify({ content: chunk })
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('❌ Discord send error:', error.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
splitMessage(text, maxLen = 2000) {
|
|
175
|
+
if (text.length <= maxLen) return [text];
|
|
176
|
+
const chunks = [];
|
|
177
|
+
let remaining = text;
|
|
178
|
+
while (remaining.length > 0) {
|
|
179
|
+
let splitIndex = remaining.lastIndexOf('\n', maxLen);
|
|
180
|
+
if (splitIndex <= 0) splitIndex = maxLen;
|
|
181
|
+
chunks.push(remaining.substring(0, splitIndex));
|
|
182
|
+
remaining = remaining.substring(splitIndex).trimStart();
|
|
183
|
+
}
|
|
184
|
+
return chunks;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = new DiscordService();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heartbeat Service for SamarthyaBot
|
|
3
|
+
* Reads HEARTBEAT.md periodically and executes autonomous tasks
|
|
4
|
+
* Inspired by PicoClaw's heartbeat feature
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
class HeartbeatService {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.enabled = process.env.HEARTBEAT_ENABLED !== 'false'; // default: true
|
|
14
|
+
this.intervalMinutes = parseInt(process.env.HEARTBEAT_INTERVAL || '30', 10);
|
|
15
|
+
if (this.intervalMinutes < 5) this.intervalMinutes = 5; // Minimum 5 minutes
|
|
16
|
+
this.workspace = process.env.WORKSPACE_PATH || path.join(os.homedir(), 'SamarthyaBot_Files');
|
|
17
|
+
this.heartbeatFile = path.join(this.workspace, 'HEARTBEAT.md');
|
|
18
|
+
this.timer = null;
|
|
19
|
+
this.chatHandler = null;
|
|
20
|
+
this.isRunning = false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Start the heartbeat loop
|
|
25
|
+
* @param {Function} chatHandler - Function to process tasks (same as chat pipeline)
|
|
26
|
+
*/
|
|
27
|
+
start(chatHandler) {
|
|
28
|
+
if (!this.enabled) {
|
|
29
|
+
console.log('⚪ Heartbeat: Disabled');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.chatHandler = chatHandler;
|
|
34
|
+
|
|
35
|
+
// Create HEARTBEAT.md if it doesn't exist
|
|
36
|
+
this.ensureHeartbeatFile();
|
|
37
|
+
|
|
38
|
+
console.log(`💓 Heartbeat: Active (every ${this.intervalMinutes} minutes)`);
|
|
39
|
+
console.log(`💓 Heartbeat file: ${this.heartbeatFile}`);
|
|
40
|
+
|
|
41
|
+
// Run first check after a short delay (let server fully boot)
|
|
42
|
+
setTimeout(() => this.tick(), 30000);
|
|
43
|
+
|
|
44
|
+
// Schedule periodic checks
|
|
45
|
+
this.timer = setInterval(() => this.tick(), this.intervalMinutes * 60 * 1000);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
stop() {
|
|
49
|
+
if (this.timer) {
|
|
50
|
+
clearInterval(this.timer);
|
|
51
|
+
this.timer = null;
|
|
52
|
+
}
|
|
53
|
+
console.log('💓 Heartbeat: Stopped');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ensureHeartbeatFile() {
|
|
57
|
+
try {
|
|
58
|
+
if (!fs.existsSync(this.workspace)) {
|
|
59
|
+
fs.mkdirSync(this.workspace, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
if (!fs.existsSync(this.heartbeatFile)) {
|
|
62
|
+
const defaultContent = `# SamarthyaBot Heartbeat Tasks
|
|
63
|
+
|
|
64
|
+
## Quick Tasks
|
|
65
|
+
- Report the current date and time
|
|
66
|
+
|
|
67
|
+
## Long Tasks (Background)
|
|
68
|
+
# Add tasks here that should run periodically
|
|
69
|
+
# - Search the web for AI news and summarize
|
|
70
|
+
# - Check if any reminders are due
|
|
71
|
+
`;
|
|
72
|
+
fs.writeFileSync(this.heartbeatFile, defaultContent, 'utf-8');
|
|
73
|
+
console.log('💓 Created default HEARTBEAT.md');
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('💓 Heartbeat: Error creating file:', error.message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async tick() {
|
|
81
|
+
if (this.isRunning) {
|
|
82
|
+
console.log('💓 Heartbeat: Previous tick still running, skipping...');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.isRunning = true;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
if (!fs.existsSync(this.heartbeatFile)) {
|
|
90
|
+
this.isRunning = false;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const content = fs.readFileSync(this.heartbeatFile, 'utf-8');
|
|
95
|
+
const tasks = this.parseTasks(content);
|
|
96
|
+
|
|
97
|
+
if (tasks.length === 0) {
|
|
98
|
+
this.isRunning = false;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`💓 Heartbeat: Found ${tasks.length} tasks to execute`);
|
|
103
|
+
|
|
104
|
+
for (const task of tasks) {
|
|
105
|
+
try {
|
|
106
|
+
console.log(`💓 Executing: ${task.substring(0, 60)}...`);
|
|
107
|
+
if (this.chatHandler) {
|
|
108
|
+
await this.chatHandler(
|
|
109
|
+
`[HEARTBEAT TASK] ${task}. Keep response brief, max 2 sentences.`,
|
|
110
|
+
'heartbeat-system',
|
|
111
|
+
'heartbeat'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error(`💓 Task failed: ${err.message}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('💓 Heartbeat tick error:', error.message);
|
|
120
|
+
} finally {
|
|
121
|
+
this.isRunning = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Parse tasks from HEARTBEAT.md
|
|
127
|
+
* Extracts lines starting with "- " that are not comments (# prefixed content)
|
|
128
|
+
*/
|
|
129
|
+
parseTasks(content) {
|
|
130
|
+
const lines = content.split('\n');
|
|
131
|
+
const tasks = [];
|
|
132
|
+
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
const trimmed = line.trim();
|
|
135
|
+
// Match task lines: "- task description" but not "# - commented task"
|
|
136
|
+
if (trimmed.startsWith('- ') && !trimmed.startsWith('# ')) {
|
|
137
|
+
const task = trimmed.substring(2).trim();
|
|
138
|
+
if (task.length > 0) {
|
|
139
|
+
tasks.push(task);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return tasks;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getStatus() {
|
|
148
|
+
return {
|
|
149
|
+
enabled: this.enabled,
|
|
150
|
+
interval: `${this.intervalMinutes} minutes`,
|
|
151
|
+
file: this.heartbeatFile,
|
|
152
|
+
running: this.isRunning
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = new HeartbeatService();
|