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.
@@ -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-DdCKkq38.js"></script>
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: '1.0.0',
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
- 🧠 SamarthyaBot Server v1.1.0
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
- 📱 WhatsApp: /api/whatsapp/webhook
115
- 🤖 Telegram: /api/telegram/webhook
116
- 👁️ Vision: /api/screen/analyze
117
-
118
- 🇮🇳 Built for Indian Workflows
119
- 📦 Ollama: ${process.env.USE_OLLAMA === 'true' ? '✅ Enabled'.padEnd(35) : '❌ Disabled'.padEnd(35)}
120
- 🔄 Autonomous Background Engine: ✅ Active
121
- 🔌 Dynamic Plugin Engine: ✅ Active
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
+ GET /api/health 200 10.663 ms - 122
@@ -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();