squidclaw 1.1.0 → 1.3.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/lib/engine.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Main entry point — starts all subsystems
2
+ * 🦑 Squidclaw Engine
3
+ * Clean middleware-based architecture
3
4
  */
4
- // Knowledge, tools, telegram — loaded dynamically
5
5
 
6
6
  import { loadConfig, getHome, ensureHome } from './core/config.js';
7
7
  import { initLogger, logger } from './core/logger.js';
@@ -13,6 +13,7 @@ import { ChannelHub } from './channels/hub.js';
13
13
  import { HeartbeatSystem } from './features/heartbeat.js';
14
14
  import { createAPIServer } from './api/server.js';
15
15
  import { addMediaSupport } from './channels/hub-media.js';
16
+ import { MessagePipeline, createContext } from './middleware/pipeline.js';
16
17
 
17
18
  export class SquidclawEngine {
18
19
  constructor(options = {}) {
@@ -23,496 +24,263 @@ export class SquidclawEngine {
23
24
  this.aiGateway = null;
24
25
  this.agentManager = null;
25
26
  this.whatsappManager = null;
27
+ this.telegramManager = null;
26
28
  this.channelHub = null;
27
29
  this.heartbeat = null;
28
30
  this.server = null;
31
+ this.pipeline = null;
32
+ this.knowledgeBase = null;
33
+ this.toolRouter = null;
34
+ this.reminders = null;
35
+ this.autoMemory = null;
36
+ this.usageAlerts = null;
29
37
  }
30
38
 
39
+ // ──────────────────────────────────────────
40
+ // Pipeline Setup
41
+ // ──────────────────────────────────────────
42
+
43
+ async _buildPipeline() {
44
+ const pipeline = new MessagePipeline();
45
+
46
+ // Order matters! Each middleware runs in sequence.
47
+ const { allowlistMiddleware } = await import('./middleware/allowlist.js');
48
+ const { apiKeyDetectMiddleware } = await import('./middleware/api-key-detect.js');
49
+ const { commandsMiddleware } = await import('./middleware/commands.js');
50
+ const { mediaMiddleware } = await import('./middleware/media.js');
51
+ const { autoLinksMiddleware } = await import('./middleware/auto-links.js');
52
+ const { autoMemoryMiddleware } = await import('./middleware/auto-memory.js');
53
+ const { skillCheckMiddleware } = await import('./middleware/skill-check.js');
54
+ const { typingMiddleware } = await import('./middleware/typing.js');
55
+ const { aiProcessMiddleware } = await import('./middleware/ai-process.js');
56
+ const { usageAlertsMiddleware } = await import('./middleware/usage-alerts.js');
57
+ const { responseSenderMiddleware } = await import('./middleware/response-sender.js');
58
+
59
+ pipeline.use('allowlist', allowlistMiddleware);
60
+ pipeline.use('api-key-detect', apiKeyDetectMiddleware);
61
+ pipeline.use('commands', commandsMiddleware);
62
+ pipeline.use('media', mediaMiddleware);
63
+ pipeline.use('auto-links', autoLinksMiddleware);
64
+ pipeline.use('auto-memory', autoMemoryMiddleware);
65
+ pipeline.use('skill-check', skillCheckMiddleware);
66
+ pipeline.use('typing', typingMiddleware); // wraps AI call with typing indicator
67
+ pipeline.use('ai-process', aiProcessMiddleware);
68
+ pipeline.use('usage-alerts', usageAlertsMiddleware);
69
+ pipeline.use('response-sender', responseSenderMiddleware);
70
+
71
+ return pipeline;
72
+ }
73
+
74
+ // ──────────────────────────────────────────
75
+ // Message Handler (used by both Telegram & WhatsApp)
76
+ // ──────────────────────────────────────────
77
+
78
+ async handleMessage(agentId, contactId, message, metadata) {
79
+ const agent = this.agentManager.get(agentId);
80
+ if (!agent) return;
81
+
82
+ const ctx = createContext(this, agentId, contactId, message, metadata);
83
+
84
+ try {
85
+ await this.pipeline.process(ctx);
86
+
87
+ // If pipeline short-circuited with a reply, send it
88
+ if (ctx.handled && ctx.response?.messages?.length > 0) {
89
+ if (ctx.platform === 'telegram' && this.telegramManager) {
90
+ await this.telegramManager.sendMessages(agentId, contactId, ctx.response.messages, metadata);
91
+ } else if (this.whatsappManager) {
92
+ for (const msg of ctx.response.messages) {
93
+ await this.whatsappManager.sendMessage(agentId, contactId, msg);
94
+ }
95
+ }
96
+ }
97
+ } catch (err) {
98
+ logger.error('engine', `Message handling error: ${err.message}`);
99
+ logger.error('engine', err.stack);
100
+ }
101
+ }
102
+
103
+ // ──────────────────────────────────────────
104
+ // Engine Startup
105
+ // ──────────────────────────────────────────
106
+
31
107
  async start() {
32
108
  ensureHome();
33
109
  initLogger(this.config.engine?.logLevel || 'info');
34
110
 
35
111
  console.log(`
36
- 🦑 Squidclaw Engine v0.1.0
112
+ 🦑 Squidclaw Engine v1.1.0
37
113
  ──────────────────────────`);
38
114
 
39
115
  // 1. Storage
40
116
  const dbPath = this.config.storage?.sqlite?.path || `${this.home}/squidclaw.db`;
41
117
  this.storage = new SQLiteStorage(dbPath);
42
- console.log(` 💾 Storage: SQLite (${dbPath})`);
118
+ console.log(` 💾 Storage: SQLite`);
43
119
 
44
120
  // 2. AI Gateway
45
121
  this.aiGateway = new AIGateway(this.config);
46
122
  const providers = Object.entries(this.config.ai?.providers || {})
47
- .filter(([_, v]) => v.key)
48
- .map(([k]) => k);
49
- console.log(` 🧠 AI: ${providers.join(', ') || 'none configured'}`);
50
- if (this.config.ai?.defaultModel) {
51
- console.log(` 🎯 Default model: ${this.config.ai.defaultModel}`);
52
- }
123
+ .filter(([_, v]) => v.key).map(([k]) => k);
124
+ console.log(` 🧠 AI: ${providers.join(', ') || 'none'}`);
125
+ if (this.config.ai?.defaultModel) console.log(` 🎯 Model: ${this.config.ai.defaultModel}`);
53
126
 
54
127
  // 3. Agent Manager
55
128
  this.agentManager = new AgentManager(this.storage, this.aiGateway);
56
129
  await this.agentManager.loadAll();
57
130
  const agents = this.agentManager.getAll();
58
- console.log(` 👥 Agents: ${agents.length} loaded`);
131
+ console.log(` 👥 Agents: ${agents.length}`);
59
132
 
60
- // 3b. Knowledge Base + Tools (optional)
61
- try {
62
- const { KnowledgeBase } = await import("./memory/knowledge.js");
63
- const { ToolRouter } = await import("./tools/router.js");
64
- const { addToolSupport } = await import("./core/agent-tools-mixin.js");
65
- this.knowledgeBase = new KnowledgeBase(this.storage, this.config);
66
- this.toolRouter = new ToolRouter(this.config, this.knowledgeBase);
67
- const toolList = ["web search", "page reader"];
68
- if (this.config.tools?.google?.oauthToken) toolList.push("calendar", "email");
69
- console.log(" 🔧 Tools: " + toolList.join(", "));
70
- for (const agent of agents) {
71
- addToolSupport(agent, this.toolRouter, this.knowledgeBase);
72
- }
73
- } catch (err) {
74
- console.log(" 🔧 Tools: basic mode");
75
- }
133
+ // 4. Knowledge + Tools
134
+ await this._initTools(agents);
76
135
 
77
- // 3e. Telegram Bot
78
- if (this.config.channels?.telegram?.enabled && this.config.channels?.telegram?.token) {
79
- try {
80
- const { TelegramManager } = await import('./channels/telegram/bot.js');
81
- this.telegramManager = new TelegramManager(this.config, this.agentManager);
82
-
83
- // Set up message handler
84
- this.telegramManager.onMessage = async (agentId, contactId, message, metadata) => {
85
- const agent = this.agentManager.get(agentId);
86
- if (!agent) return;
87
-
88
- // Allowlist check
89
- const allowFrom = this.config.channels?.telegram?.allowFrom;
90
- if (allowFrom && allowFrom !== '*') {
91
- const senderId = contactId.replace('tg_', '');
92
- const allowed = Array.isArray(allowFrom) ? allowFrom.includes(senderId) : String(allowFrom) === senderId;
93
- if (!allowed) return;
94
- }
136
+ // 5. Features (reminders, auto-memory, usage alerts)
137
+ await this._initFeatures();
95
138
 
96
- // Handle API key detection — user pasting a key
97
- const { detectApiKey, saveApiKey, getKeyConfirmation, detectSkillRequest, checkSkillAvailable, getKeyRequestMessage } = await import('./features/self-config.js');
98
-
99
- const keyDetected = detectApiKey(message);
100
- if (keyDetected && keyDetected.provider !== 'unknown') {
101
- saveApiKey(keyDetected.provider, keyDetected.key);
102
- // Reload config so skills see the new key
103
- const { loadConfig } = await import('./core/config.js');
104
- this.config = loadConfig();
105
- const confirmation = getKeyConfirmation(keyDetected.provider);
106
- await this.telegramManager.sendMessage(agentId, contactId, confirmation, metadata);
107
- return;
108
- }
139
+ // 6. Message Pipeline
140
+ this.pipeline = await this._buildPipeline();
141
+ console.log(` 🔧 Pipeline: ${this.pipeline.middleware.length} middleware`);
109
142
 
110
- // Handle /tasks command
111
- if (message.trim() === '/tasks' || message.trim() === '/todo') {
112
- try {
113
- const { TaskManager } = await import('./tools/tasks.js');
114
- const tm = new TaskManager(this.storage);
115
- const tasks = tm.list(agentId, contactId);
116
- if (tasks.length === 0) {
117
- await this.telegramManager.sendMessage(agentId, contactId, '📋 No pending tasks! Tell me to add one.', metadata);
118
- } else {
119
- const list = tasks.map((t, i) => (i+1) + '. ' + t.task).join('\n');
120
- await this.telegramManager.sendMessage(agentId, contactId, '📋 *Your Tasks*\n\n' + list, metadata);
121
- }
122
- } catch (err) {
123
- await this.telegramManager.sendMessage(agentId, contactId, '❌ ' + err.message, metadata);
124
- }
125
- return;
126
- }
143
+ // 7. Telegram
144
+ await this._initTelegram(agents);
127
145
 
128
- // Handle /usage command
129
- if (message.trim() === '/usage') {
130
- try {
131
- const { UsageAlerts } = await import('./features/usage-alerts.js');
132
- const ua = new UsageAlerts(this.storage);
133
- const summary = await ua.getSummary(agentId);
134
- const fmtT = (n) => n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : String(n);
135
- const lines = [
136
- '📊 *Usage Report*', '',
137
- '*Today:*',
138
- ' 💬 ' + summary.today.calls + ' messages',
139
- ' 🪙 ' + fmtT(summary.today.tokens) + ' tokens',
140
- ' 💰 $' + summary.today.cost, '',
141
- '*Last 30 days:*',
142
- ' 💬 ' + summary.month.calls + ' messages',
143
- ' 🪙 ' + fmtT(summary.month.tokens) + ' tokens',
144
- ' 💰 $' + summary.month.cost,
145
- ];
146
- await this.telegramManager.sendMessage(agentId, contactId, lines.join('\n'), metadata);
147
- } catch (err) {
148
- await this.telegramManager.sendMessage(agentId, contactId, '❌ ' + err.message, metadata);
149
- }
150
- return;
151
- }
146
+ // 8. WhatsApp
147
+ await this._initWhatsApp(agents);
152
148
 
153
- // Handle /help command
154
- if (message.trim() === '/help') {
155
- const helpText = [
156
- '🦑 *Commands*',
157
- '',
158
- '/status — model, uptime, usage stats',
159
- '/backup — save me to a backup file',
160
- '/memories — what I remember about you',
161
- '/tasks — your todo list',
162
- '/usage — spending report (today + 30 days)',
163
- '/help — this message',
164
- '',
165
- 'Just chat normally — I\'ll search the web, remember things, and help! 🦑',
166
- ];
167
- await this.telegramManager.sendMessage(agentId, contactId, helpText.join('\n'), metadata);
168
- return;
169
- }
149
+ // 9. Channel Hub
150
+ this.channelHub = new ChannelHub(this.agentManager, this.whatsappManager, this.storage, this.telegramManager);
151
+ addMediaSupport(this.channelHub, this.config, this.home);
170
152
 
171
- // Handle /backup command
172
- if (message.trim() === '/backup' || message.trim().toLowerCase() === 'backup yourself' || message.trim().toLowerCase().includes('backup')) {
173
- if (message.toLowerCase().includes('backup') && (message.toLowerCase().includes('yourself') || message.toLowerCase().includes('your') || message.trim() === '/backup')) {
174
- try {
175
- const { AgentBackup } = await import('./features/backup.js');
176
- const backup = await AgentBackup.create(agentId, this.home, this.storage);
177
- const filename = (agent.name || agentId) + '_backup_' + new Date().toISOString().slice(0,10) + '.json';
178
- const path = this.home + '/backups/' + filename;
179
- const { mkdirSync } = await import('fs');
180
- mkdirSync(this.home + '/backups', { recursive: true });
181
- await AgentBackup.saveToFile(backup, path);
182
- const size = (JSON.stringify(backup).length / 1024).toFixed(1);
183
- const lines = [
184
- '💾 *Backup Complete!*',
185
- '',
186
- '📦 File: ' + filename,
187
- '📏 Size: ' + size + ' KB',
188
- '💬 Messages: ' + (backup.conversations?.length || 0),
189
- '🧠 Memories: ' + (backup.memories?.length || 0),
190
- '📄 Files: ' + Object.keys(backup.files).length,
191
- '',
192
- 'I am safe! You can resurrect me anytime with this file 🦑',
193
- ];
194
- await this.telegramManager.sendMessage(agentId, contactId, lines.join('\n'), metadata);
195
- } catch (err) {
196
- await this.telegramManager.sendMessage(agentId, contactId, '❌ Backup failed: ' + err.message, metadata);
197
- }
198
- return;
199
- }
200
- }
153
+ // 10. Heartbeat
154
+ this.heartbeat = new HeartbeatSystem(this.agentManager, this.whatsappManager, this.storage);
155
+ this.heartbeat.start();
156
+ console.log(` 💓 Heartbeat: active`);
201
157
 
202
- // Handle /memories command
203
- if (message.trim() === '/memories' || message.trim() === '/remember') {
204
- try {
205
- const memories = await this.storage.getMemories(agentId);
206
- if (memories.length === 0) {
207
- await this.telegramManager.sendMessage(agentId, contactId, '🧠 No memories yet. Tell me things and I\'ll remember!', metadata);
208
- } else {
209
- const memList = memories.slice(0, 20).map(m => '• ' + m.key + ': ' + m.value).join('\n');
210
- await this.telegramManager.sendMessage(agentId, contactId, '🧠 *What I Remember*\n\n' + memList, metadata);
211
- }
212
- } catch (err) {
213
- await this.telegramManager.sendMessage(agentId, contactId, '🧠 Memory error: ' + err.message, metadata);
214
- }
215
- return;
216
- }
158
+ // 11. API + Dashboard
159
+ await this._initAPI();
217
160
 
218
- // Handle /status command
219
- if (message.trim() === '/status' || message.trim().toLowerCase() === 'status') {
220
- const uptime = process.uptime();
221
- const h = Math.floor(uptime / 3600);
222
- const m = Math.floor((uptime % 3600) / 60);
223
- const usage = await this.storage.getUsage(agentId) || {};
224
- const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
225
- const fmtT = tokens >= 1000000 ? (tokens/1000000).toFixed(1)+'M' : tokens >= 1000 ? (tokens/1000).toFixed(1)+'K' : String(tokens);
226
- const waOn = Object.values(this.whatsappManager?.getStatuses() || {}).some(s => s.connected);
227
-
228
- const lines = [
229
- `🦑 *${agent.name || agentId} Status*`,
230
- '────────────────────',
231
- `🧠 Model: ${agent.model || 'unknown'}`,
232
- `⏱️ Uptime: ${h}h ${m}m`,
233
- `💬 Messages: ${usage.messages || 0}`,
234
- `🪙 Tokens: ${fmtT}`,
235
- `💰 Cost: $${(usage.cost_usd || 0).toFixed(4)}`,
236
- '────────────────────',
237
- `📱 WhatsApp: ${waOn ? '✅' : '❌'}`,
238
- '✈️ Telegram: ✅ connected',
239
- '────────────────────',
240
- '⚡ Skills: search, reader, vision, voice, memory',
241
- `🗣️ Language: ${agent.language || 'bilingual'}`,
242
- ];
243
-
244
- await this.telegramManager.sendMessage(agentId, contactId, lines.join('\n'), metadata);
245
- return;
246
- }
161
+ // Graceful shutdown
162
+ process.on('SIGINT', () => this.stop());
163
+ process.on('SIGTERM', () => this.stop());
164
+ }
247
165
 
248
- // Process Telegram media (voice, images)
249
- if (metadata._ctx && metadata.mediaType) {
250
- try {
251
- if (metadata.mediaType === 'audio') {
252
- // Download and transcribe voice note
253
- const file = await metadata._ctx.getFile();
254
- const fileUrl = `https://api.telegram.org/file/bot${this.config.channels.telegram.token}/${file.file_path}`;
255
- const resp = await fetch(fileUrl);
256
- const buffer = Buffer.from(await resp.arrayBuffer());
257
-
258
- // Transcribe with Groq Whisper (free) or OpenAI
259
- const groqKey = this.config.ai?.providers?.groq?.key;
260
- const openaiKey = this.config.ai?.providers?.openai?.key;
261
- const apiKey = groqKey || openaiKey;
262
- const apiUrl = groqKey
263
- ? 'https://api.groq.com/openai/v1/audio/transcriptions'
264
- : 'https://api.openai.com/v1/audio/transcriptions';
265
- const model = groqKey ? 'whisper-large-v3' : 'whisper-1';
266
-
267
- if (apiKey) {
268
- const form = new FormData();
269
- form.append('file', new Blob([buffer], { type: 'audio/ogg' }), 'voice.ogg');
270
- form.append('model', model);
271
-
272
- const tRes = await fetch(apiUrl, {
273
- method: 'POST',
274
- headers: { 'Authorization': 'Bearer ' + apiKey },
275
- body: form,
276
- });
277
- const tData = await tRes.json();
278
- if (tData.text) {
279
- message = '[Voice note]: "' + tData.text + '"';
280
- logger.info('telegram', 'Transcribed voice: ' + tData.text.slice(0, 50));
281
- }
282
- }
283
- } else if (metadata.mediaType === 'image') {
284
- // Download and analyze image
285
- const photos = metadata._ctx.message?.photo;
286
- if (photos?.length > 0) {
287
- const photo = photos[photos.length - 1]; // highest res
288
- const file = await metadata._ctx.api.getFile(photo.file_id);
289
- const fileUrl = `https://api.telegram.org/file/bot${this.config.channels.telegram.token}/${file.file_path}`;
290
- const resp = await fetch(fileUrl);
291
- const buffer = Buffer.from(await resp.arrayBuffer());
292
- const base64 = buffer.toString('base64');
293
-
294
- // Analyze with Claude or OpenAI Vision
295
- const anthropicKey = this.config.ai?.providers?.anthropic?.key;
296
- const caption = message.replace('[📸 Image]', '').trim();
297
- const userPrompt = caption || 'What is in this image? Be concise.';
298
-
299
- if (anthropicKey) {
300
- const vRes = await fetch('https://api.anthropic.com/v1/messages', {
301
- method: 'POST',
302
- headers: {
303
- 'x-api-key': anthropicKey,
304
- 'content-type': 'application/json',
305
- 'anthropic-version': '2023-06-01',
306
- },
307
- body: JSON.stringify({
308
- model: 'claude-sonnet-4-20250514',
309
- max_tokens: 300,
310
- messages: [{
311
- role: 'user',
312
- content: [
313
- { type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: base64 } },
314
- { type: 'text', text: userPrompt },
315
- ],
316
- }],
317
- }),
318
- });
319
- const vData = await vRes.json();
320
- const analysis = vData.content?.[0]?.text || '';
321
- if (analysis) {
322
- message = caption
323
- ? '[Image with caption: "' + caption + '"] Image shows: ' + analysis
324
- : '[Image] Image shows: ' + analysis;
325
- logger.info('telegram', 'Analyzed image: ' + analysis.slice(0, 50));
326
- }
327
- }
328
- }
329
- } else if (metadata.mediaType === 'document') {
330
- const file = await metadata._ctx.getFile();
331
- const fileUrl = `https://api.telegram.org/file/bot${this.config.channels.telegram.token}/${file.file_path}`;
332
- const resp = await fetch(fileUrl);
333
- const buffer = Buffer.from(await resp.arrayBuffer());
334
- const filename = message.match(/Document: (.+?)\]/)?.[1] || 'file.txt';
335
-
336
- try {
337
- const { DocIngester } = await import('./features/doc-ingest.js');
338
- const ingester = new DocIngester(this.storage, this.knowledgeBase, this.home);
339
- const result = await ingester.ingest(agentId, buffer, filename, metadata.mimeType);
340
-
341
- await this.telegramManager.sendMessage(agentId, contactId,
342
- '📄 *Document absorbed!*\n📁 ' + filename + '\n📊 ' + result.chunks + ' chunks\n📝 ' + result.chars + ' chars\n\nI can answer questions about this! 🦑', metadata);
343
- return;
344
- } catch (err) {
345
- message = '[Document: ' + filename + '] (Could not process: ' + err.message + ')';
346
- }
347
- }
348
- } catch (err) {
349
- logger.error('telegram', 'Media processing error: ' + err.message);
350
- }
351
- }
166
+ // ──────────────────────────────────────────
167
+ // Init Helpers
168
+ // ──────────────────────────────────────────
352
169
 
353
- // Auto-read links in message
354
- const linkRegex = /https?:\/\/[^\s<>"')\]]+/gi;
355
- if (linkRegex.test(message) && this.toolRouter) {
356
- try {
357
- const { extractAndReadLinks } = await import('./features/auto-links.js');
358
- const linkContext = await extractAndReadLinks(message, this.toolRouter.browser);
359
- if (linkContext) {
360
- metadata._linkContext = linkContext;
361
- }
362
- } catch {}
363
- }
170
+ async _initTools(agents) {
171
+ try {
172
+ const { KnowledgeBase } = await import('./memory/knowledge.js');
173
+ const { ToolRouter } = await import('./tools/router.js');
174
+ const { addToolSupport } = await import('./core/agent-tools-mixin.js');
175
+
176
+ this.knowledgeBase = new KnowledgeBase(this.storage, this.config);
177
+ this.toolRouter = new ToolRouter(this.config, this.knowledgeBase);
364
178
 
365
- // Check usage alerts
366
- if (this.usageAlerts) {
367
- try {
368
- const alert = await this.usageAlerts.check(agentId);
369
- if (alert.alert) {
370
- await this.telegramManager.sendMessage(agentId, contactId,
371
- '⚠️ *Usage Alert*\nYou have spent $' + alert.total + ' in the last 24h (threshold: $' + alert.threshold + ')', metadata);
372
- }
373
- } catch {}
374
- }
179
+ for (const agent of agents) {
180
+ addToolSupport(agent, this.toolRouter, this.knowledgeBase);
181
+ }
182
+ console.log(' 🔧 Tools: active');
183
+ } catch (err) {
184
+ console.log(' 🔧 Tools: basic');
185
+ }
186
+ }
375
187
 
376
- // Auto-extract facts from user message
377
- if (this.autoMemory) {
378
- try { await this.autoMemory.extract(agentId, contactId, message); } catch {}
188
+ async _initFeatures() {
189
+ // Reminders
190
+ try {
191
+ const { ReminderManager } = await import('./features/reminders.js');
192
+ this.reminders = new ReminderManager(this.storage);
193
+ this.reminders.onFire = async (agentId, contactId, message, platform, metadata) => {
194
+ try {
195
+ const msg = '⏰ *Reminder!*\n\n' + message;
196
+ if (platform === 'telegram' && this.telegramManager) {
197
+ await this.telegramManager.sendMessage(agentId, contactId, msg, metadata);
198
+ } else if (this.whatsappManager) {
199
+ await this.whatsappManager.sendMessage(agentId, contactId, msg);
379
200
  }
201
+ } catch (err) {
202
+ logger.error('reminder', 'Delivery failed: ' + err.message);
203
+ }
204
+ };
205
+ const pending = this.storage.db.prepare("SELECT COUNT(*) as c FROM reminders WHERE fired = 0 AND fire_at > datetime('now')").get();
206
+ if (pending.c > 0) console.log(` ⏰ Reminders: ${pending.c} pending`);
207
+ } catch {}
380
208
 
381
- // Check if user is asking for a skill we don't have
382
- const skillRequest = detectSkillRequest(message);
383
- if (skillRequest) {
384
- const availability = checkSkillAvailable(skillRequest, this.config);
385
- if (!availability.available) {
386
- const keyMsg = getKeyRequestMessage(skillRequest);
387
- if (keyMsg) {
388
- await this.telegramManager.sendMessage(agentId, contactId, keyMsg, metadata);
389
- return;
390
- }
391
- }
392
- }
209
+ // Auto-memory
210
+ try {
211
+ const { AutoMemory } = await import('./features/auto-memory.js');
212
+ this.autoMemory = new AutoMemory(this.storage);
213
+ } catch {}
393
214
 
394
- // Show typing indicator while processing
395
- const chatId = metadata.chatId || contactId.replace('tg_', '');
396
- const botInfo = this.telegramManager?.bots?.get(agentId);
397
- let typingInterval;
398
- if (botInfo?.bot) {
399
- const sendTyping = () => { try { botInfo.bot.api.sendChatAction(chatId, 'typing').catch(() => {}); } catch {} };
400
- sendTyping();
401
- typingInterval = setInterval(sendTyping, 4000);
402
- }
215
+ // Usage alerts
216
+ try {
217
+ const { UsageAlerts } = await import('./features/usage-alerts.js');
218
+ this.usageAlerts = new UsageAlerts(this.storage);
219
+ } catch {}
220
+ }
403
221
 
404
- let result;
405
- try {
406
- result = await agent.processMessage(contactId, message, metadata);
407
- } finally {
408
- if (typingInterval) clearInterval(typingInterval);
409
- }
410
-
411
- if (result.reaction && metadata.messageId) {
412
- try { await this.telegramManager.sendReaction(agentId, contactId, metadata.messageId, result.reaction, metadata); } catch {}
413
- }
414
-
415
- if (result.messages && result.messages.length > 0) {
416
- // Handle reminder if requested
417
- if (result._reminder && this.reminders) {
418
- const { time, message: remMsg } = result._reminder;
419
- this.reminders.add(agentId, contactId, remMsg, time, 'telegram', metadata);
420
- }
421
-
422
- // Send image if generated
423
- if (result.image) {
424
- const photoData = result.image.url ? { url: result.image.url } : { base64: result.image.base64 };
425
- const caption = result.messages?.[0] || '';
426
- await this.telegramManager.sendPhoto(agentId, contactId, photoData, caption, metadata);
427
- } else {
428
- // Handle reminder if requested
429
- if (result._reminder && this.reminders) {
430
- const { time, message: remMsg } = result._reminder;
431
- this.reminders.add(agentId, contactId, remMsg, time, 'telegram', metadata);
432
- }
433
-
434
- // Send image if generated
435
- if (result.image) {
436
- await this.telegramManager.sendPhoto(agentId, contactId, result.image, result.messages?.[0] || '', metadata);
437
- } else if (metadata.originalType === 'voice' && result.messages.length === 1 && result.messages[0].length < 500) {
438
- // Reply with voice when user sent voice
439
- try {
440
- const { VoiceReply } = await import('./features/voice-reply.js');
441
- const vr = new VoiceReply(this.config);
442
- const lang = /[\u0600-\u06FF]/.test(result.messages[0]) ? 'ar' : 'en';
443
- const audio = await vr.generate(result.messages[0], { language: lang });
444
- await this.telegramManager.sendVoice(agentId, contactId, audio, metadata);
445
- } catch (err) {
446
- // Fallback to text
447
- logger.warn('voice', 'Voice reply failed, sending text: ' + err.message);
448
- await this.telegramManager.sendMessages(agentId, contactId, result.messages, metadata);
449
- }
450
- } else {
451
- await this.telegramManager.sendMessages(agentId, contactId, result.messages, metadata);
452
- }
453
- }
454
- }
455
- };
222
+ async _initTelegram(agents) {
223
+ if (!this.config.channels?.telegram?.enabled || !this.config.channels?.telegram?.token) return;
456
224
 
457
- // Start bot for all agents with telegram config
458
- // Only start ONE bot instance (shared token = shared bot)
459
- try {
460
- const firstAgent = agents[0];
461
- if (firstAgent) {
462
- await this.telegramManager.startBot(firstAgent.id, this.config.channels.telegram.token);
463
- }
464
- } catch (err) {
465
- console.log(' ✈️ Telegram: ' + err.message);
466
- }
467
- console.log(' ✈️ Telegram: connected');
468
- } catch (err) {
469
- console.log(' ✈️ Telegram: failed (' + err.message + ')');
225
+ try {
226
+ const { TelegramManager } = await import('./channels/telegram/bot.js');
227
+ this.telegramManager = new TelegramManager(this.config, this.agentManager);
228
+
229
+ // Route all messages through the pipeline
230
+ this.telegramManager.onMessage = (agentId, contactId, message, metadata) => {
231
+ metadata.platform = 'telegram';
232
+ metadata.chatId = metadata.chatId || contactId.replace('tg_', '');
233
+ return this.handleMessage(agentId, contactId, message, metadata);
234
+ };
235
+
236
+ const firstAgent = agents[0];
237
+ if (firstAgent) {
238
+ await this.telegramManager.startBot(firstAgent.id, this.config.channels.telegram.token);
470
239
  }
240
+ console.log(' ✈️ Telegram: connected');
241
+ } catch (err) {
242
+ console.log(' ✈️ Telegram: ' + err.message);
471
243
  }
244
+ }
472
245
 
473
- // 4. WhatsApp Manager
246
+ async _initWhatsApp(agents) {
474
247
  this.whatsappManager = new WhatsAppManager(this.config, this.agentManager, this.home);
475
248
 
476
- // Start WhatsApp sessions for all agents that have them configured
477
249
  for (const agent of agents) {
478
250
  if (agent.status === 'active' && agent.whatsappNumber) {
479
- try {
480
- await this.whatsappManager.startSession(agent.id);
481
- } catch (err) {
482
- logger.warn('engine', `Failed to start WhatsApp for "${agent.name}": ${err.message}`);
251
+ try { await this.whatsappManager.startSession(agent.id); } catch (err) {
252
+ logger.warn('engine', `WhatsApp for "${agent.name}": ${err.message}`);
483
253
  }
484
254
  }
485
255
  }
256
+
486
257
  const waStatuses = this.whatsappManager.getStatuses();
487
258
  const connected = Object.values(waStatuses).filter(s => s.connected).length;
488
- console.log(` 📱 WhatsApp: ${connected}/${agents.length} connected`);
489
-
490
- // 5. Channel Hub
491
- this.channelHub = new ChannelHub(this.agentManager, this.whatsappManager, this.storage, this.telegramManager);
492
- addMediaSupport(this.channelHub, this.config, this.home);
493
-
494
- // 6. Heartbeat
495
- this.heartbeat = new HeartbeatSystem(this.agentManager, this.whatsappManager, this.storage);
496
- this.heartbeat.start();
497
- console.log(` 💓 Heartbeat: active`);
259
+ console.log(` 📱 WhatsApp: ${connected}/${agents.length}`);
260
+ }
498
261
 
499
- // 7. API Server + Dashboard
262
+ async _initAPI() {
500
263
  const app = createAPIServer(this);
501
- try { const { addDashboardRoutes } = await import('./api/dashboard.js'); addDashboardRoutes(app, this); } catch {}
264
+ try {
265
+ const { addDashboardRoutes } = await import('./api/dashboard.js');
266
+ addDashboardRoutes(app, this);
267
+ } catch {}
268
+
502
269
  this.server = app.listen(this.port, this.config.engine?.bind || '0.0.0.0', () => {
503
- console.log(` 🌐 API: http://${this.config.engine?.bind || '0.0.0.0'}:${this.port}`);
270
+ console.log(` 🌐 API: http://0.0.0.0:${this.port}`);
504
271
  console.log(` ──────────────────────────`);
505
272
  console.log(` ✅ Engine running!\n`);
506
273
  });
507
-
508
- // Graceful shutdown
509
- process.on('SIGINT', () => this.stop());
510
- process.on('SIGTERM', () => this.stop());
511
274
  }
512
275
 
276
+ // ──────────────────────────────────────────
277
+ // Shutdown
278
+ // ──────────────────────────────────────────
279
+
513
280
  async stop() {
514
281
  console.log('\n 🦑 Shutting down...');
515
282
  this.heartbeat?.stop();
283
+ this.reminders?.destroy();
516
284
  await this.whatsappManager?.stopAll();
517
285
  this.storage?.close();
518
286
  this.server?.close();