squidclaw 1.0.0 → 1.2.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,477 +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
- }
95
-
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
- }
136
+ // 5. Features (reminders, auto-memory, usage alerts)
137
+ await this._initFeatures();
109
138
 
110
- // Handle /usage command
111
- if (message.trim() === '/usage') {
112
- try {
113
- const { UsageAlerts } = await import('./features/usage-alerts.js');
114
- const ua = new UsageAlerts(this.storage);
115
- const summary = await ua.getSummary(agentId);
116
- const fmtT = (n) => n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : String(n);
117
- const lines = [
118
- '📊 *Usage Report*', '',
119
- '*Today:*',
120
- ' 💬 ' + summary.today.calls + ' messages',
121
- ' 🪙 ' + fmtT(summary.today.tokens) + ' tokens',
122
- ' 💰 $' + summary.today.cost, '',
123
- '*Last 30 days:*',
124
- ' 💬 ' + summary.month.calls + ' messages',
125
- ' 🪙 ' + fmtT(summary.month.tokens) + ' tokens',
126
- ' 💰 $' + summary.month.cost,
127
- ];
128
- await this.telegramManager.sendMessage(agentId, contactId, lines.join('\n'), metadata);
129
- } catch (err) {
130
- await this.telegramManager.sendMessage(agentId, contactId, '❌ ' + err.message, metadata);
131
- }
132
- return;
133
- }
139
+ // 6. Message Pipeline
140
+ this.pipeline = await this._buildPipeline();
141
+ console.log(` 🔧 Pipeline: ${this.pipeline.middleware.length} middleware`);
134
142
 
135
- // Handle /help command
136
- if (message.trim() === '/help') {
137
- const helpText = [
138
- '🦑 *Commands*',
139
- '',
140
- '/status — model, uptime, usage stats',
141
- '/backup — save me to a backup file',
142
- '/memories — what I remember about you',
143
- '/usage — spending report (today + 30 days)',
144
- '/help — this message',
145
- '',
146
- 'Just chat normally — I\'ll search the web, remember things, and help! 🦑',
147
- ];
148
- await this.telegramManager.sendMessage(agentId, contactId, helpText.join('\n'), metadata);
149
- return;
150
- }
143
+ // 7. Telegram
144
+ await this._initTelegram(agents);
151
145
 
152
- // Handle /backup command
153
- if (message.trim() === '/backup' || message.trim().toLowerCase() === 'backup yourself' || message.trim().toLowerCase().includes('backup')) {
154
- if (message.toLowerCase().includes('backup') && (message.toLowerCase().includes('yourself') || message.toLowerCase().includes('your') || message.trim() === '/backup')) {
155
- try {
156
- const { AgentBackup } = await import('./features/backup.js');
157
- const backup = await AgentBackup.create(agentId, this.home, this.storage);
158
- const filename = (agent.name || agentId) + '_backup_' + new Date().toISOString().slice(0,10) + '.json';
159
- const path = this.home + '/backups/' + filename;
160
- const { mkdirSync } = await import('fs');
161
- mkdirSync(this.home + '/backups', { recursive: true });
162
- await AgentBackup.saveToFile(backup, path);
163
- const size = (JSON.stringify(backup).length / 1024).toFixed(1);
164
- const lines = [
165
- '💾 *Backup Complete!*',
166
- '',
167
- '📦 File: ' + filename,
168
- '📏 Size: ' + size + ' KB',
169
- '💬 Messages: ' + (backup.conversations?.length || 0),
170
- '🧠 Memories: ' + (backup.memories?.length || 0),
171
- '📄 Files: ' + Object.keys(backup.files).length,
172
- '',
173
- 'I am safe! You can resurrect me anytime with this file 🦑',
174
- ];
175
- await this.telegramManager.sendMessage(agentId, contactId, lines.join('\n'), metadata);
176
- } catch (err) {
177
- await this.telegramManager.sendMessage(agentId, contactId, '❌ Backup failed: ' + err.message, metadata);
178
- }
179
- return;
180
- }
181
- }
146
+ // 8. WhatsApp
147
+ await this._initWhatsApp(agents);
182
148
 
183
- // Handle /memories command
184
- if (message.trim() === '/memories' || message.trim() === '/remember') {
185
- try {
186
- const memories = await this.storage.getMemories(agentId);
187
- if (memories.length === 0) {
188
- await this.telegramManager.sendMessage(agentId, contactId, '🧠 No memories yet. Tell me things and I\'ll remember!', metadata);
189
- } else {
190
- const memList = memories.slice(0, 20).map(m => '• ' + m.key + ': ' + m.value).join('\n');
191
- await this.telegramManager.sendMessage(agentId, contactId, '🧠 *What I Remember*\n\n' + memList, metadata);
192
- }
193
- } catch (err) {
194
- await this.telegramManager.sendMessage(agentId, contactId, '🧠 Memory error: ' + err.message, metadata);
195
- }
196
- return;
197
- }
198
-
199
- // Handle /status command
200
- if (message.trim() === '/status' || message.trim().toLowerCase() === 'status') {
201
- const uptime = process.uptime();
202
- const h = Math.floor(uptime / 3600);
203
- const m = Math.floor((uptime % 3600) / 60);
204
- const usage = await this.storage.getUsage(agentId) || {};
205
- const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
206
- const fmtT = tokens >= 1000000 ? (tokens/1000000).toFixed(1)+'M' : tokens >= 1000 ? (tokens/1000).toFixed(1)+'K' : String(tokens);
207
- const waOn = Object.values(this.whatsappManager?.getStatuses() || {}).some(s => s.connected);
208
-
209
- const lines = [
210
- `🦑 *${agent.name || agentId} Status*`,
211
- '────────────────────',
212
- `🧠 Model: ${agent.model || 'unknown'}`,
213
- `⏱️ Uptime: ${h}h ${m}m`,
214
- `💬 Messages: ${usage.messages || 0}`,
215
- `🪙 Tokens: ${fmtT}`,
216
- `💰 Cost: $${(usage.cost_usd || 0).toFixed(4)}`,
217
- '────────────────────',
218
- `📱 WhatsApp: ${waOn ? '✅' : '❌'}`,
219
- '✈️ Telegram: ✅ connected',
220
- '────────────────────',
221
- '⚡ Skills: search, reader, vision, voice, memory',
222
- `🗣️ Language: ${agent.language || 'bilingual'}`,
223
- ];
224
-
225
- await this.telegramManager.sendMessage(agentId, contactId, lines.join('\n'), metadata);
226
- return;
227
- }
228
-
229
- // Process Telegram media (voice, images)
230
- if (metadata._ctx && metadata.mediaType) {
231
- try {
232
- if (metadata.mediaType === 'audio') {
233
- // Download and transcribe voice note
234
- const file = await metadata._ctx.getFile();
235
- const fileUrl = `https://api.telegram.org/file/bot${this.config.channels.telegram.token}/${file.file_path}`;
236
- const resp = await fetch(fileUrl);
237
- const buffer = Buffer.from(await resp.arrayBuffer());
238
-
239
- // Transcribe with Groq Whisper (free) or OpenAI
240
- const groqKey = this.config.ai?.providers?.groq?.key;
241
- const openaiKey = this.config.ai?.providers?.openai?.key;
242
- const apiKey = groqKey || openaiKey;
243
- const apiUrl = groqKey
244
- ? 'https://api.groq.com/openai/v1/audio/transcriptions'
245
- : 'https://api.openai.com/v1/audio/transcriptions';
246
- const model = groqKey ? 'whisper-large-v3' : 'whisper-1';
247
-
248
- if (apiKey) {
249
- const form = new FormData();
250
- form.append('file', new Blob([buffer], { type: 'audio/ogg' }), 'voice.ogg');
251
- form.append('model', model);
252
-
253
- const tRes = await fetch(apiUrl, {
254
- method: 'POST',
255
- headers: { 'Authorization': 'Bearer ' + apiKey },
256
- body: form,
257
- });
258
- const tData = await tRes.json();
259
- if (tData.text) {
260
- message = '[Voice note]: "' + tData.text + '"';
261
- logger.info('telegram', 'Transcribed voice: ' + tData.text.slice(0, 50));
262
- }
263
- }
264
- } else if (metadata.mediaType === 'image') {
265
- // Download and analyze image
266
- const photos = metadata._ctx.message?.photo;
267
- if (photos?.length > 0) {
268
- const photo = photos[photos.length - 1]; // highest res
269
- const file = await metadata._ctx.api.getFile(photo.file_id);
270
- const fileUrl = `https://api.telegram.org/file/bot${this.config.channels.telegram.token}/${file.file_path}`;
271
- const resp = await fetch(fileUrl);
272
- const buffer = Buffer.from(await resp.arrayBuffer());
273
- const base64 = buffer.toString('base64');
274
-
275
- // Analyze with Claude or OpenAI Vision
276
- const anthropicKey = this.config.ai?.providers?.anthropic?.key;
277
- const caption = message.replace('[📸 Image]', '').trim();
278
- const userPrompt = caption || 'What is in this image? Be concise.';
279
-
280
- if (anthropicKey) {
281
- const vRes = await fetch('https://api.anthropic.com/v1/messages', {
282
- method: 'POST',
283
- headers: {
284
- 'x-api-key': anthropicKey,
285
- 'content-type': 'application/json',
286
- 'anthropic-version': '2023-06-01',
287
- },
288
- body: JSON.stringify({
289
- model: 'claude-sonnet-4-20250514',
290
- max_tokens: 300,
291
- messages: [{
292
- role: 'user',
293
- content: [
294
- { type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: base64 } },
295
- { type: 'text', text: userPrompt },
296
- ],
297
- }],
298
- }),
299
- });
300
- const vData = await vRes.json();
301
- const analysis = vData.content?.[0]?.text || '';
302
- if (analysis) {
303
- message = caption
304
- ? '[Image with caption: "' + caption + '"] Image shows: ' + analysis
305
- : '[Image] Image shows: ' + analysis;
306
- logger.info('telegram', 'Analyzed image: ' + analysis.slice(0, 50));
307
- }
308
- }
309
- }
310
- } else if (metadata.mediaType === 'document') {
311
- const file = await metadata._ctx.getFile();
312
- const fileUrl = `https://api.telegram.org/file/bot${this.config.channels.telegram.token}/${file.file_path}`;
313
- const resp = await fetch(fileUrl);
314
- const buffer = Buffer.from(await resp.arrayBuffer());
315
- const filename = message.match(/Document: (.+?)\]/)?.[1] || 'file.txt';
316
-
317
- try {
318
- const { DocIngester } = await import('./features/doc-ingest.js');
319
- const ingester = new DocIngester(this.storage, this.knowledgeBase, this.home);
320
- const result = await ingester.ingest(agentId, buffer, filename, metadata.mimeType);
321
-
322
- await this.telegramManager.sendMessage(agentId, contactId,
323
- '📄 *Document absorbed!*\n📁 ' + filename + '\n📊 ' + result.chunks + ' chunks\n📝 ' + result.chars + ' chars\n\nI can answer questions about this! 🦑', metadata);
324
- return;
325
- } catch (err) {
326
- message = '[Document: ' + filename + '] (Could not process: ' + err.message + ')';
327
- }
328
- }
329
- } catch (err) {
330
- logger.error('telegram', 'Media processing error: ' + err.message);
331
- }
332
- }
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);
333
152
 
334
- // Auto-read links in message
335
- const linkRegex = /https?:\/\/[^\s<>"')\]]+/gi;
336
- if (linkRegex.test(message) && this.toolRouter) {
337
- try {
338
- const { extractAndReadLinks } = await import('./features/auto-links.js');
339
- const linkContext = await extractAndReadLinks(message, this.toolRouter.browser);
340
- if (linkContext) {
341
- metadata._linkContext = linkContext;
342
- }
343
- } catch {}
344
- }
153
+ // 10. Heartbeat
154
+ this.heartbeat = new HeartbeatSystem(this.agentManager, this.whatsappManager, this.storage);
155
+ this.heartbeat.start();
156
+ console.log(` 💓 Heartbeat: active`);
345
157
 
346
- // Check usage alerts
347
- if (this.usageAlerts) {
348
- try {
349
- const alert = await this.usageAlerts.check(agentId);
350
- if (alert.alert) {
351
- await this.telegramManager.sendMessage(agentId, contactId,
352
- '⚠️ *Usage Alert*\nYou have spent $' + alert.total + ' in the last 24h (threshold: $' + alert.threshold + ')', metadata);
353
- }
354
- } catch {}
355
- }
158
+ // 11. API + Dashboard
159
+ await this._initAPI();
356
160
 
357
- // Auto-extract facts from user message
358
- if (this.autoMemory) {
359
- try { await this.autoMemory.extract(agentId, contactId, message); } catch {}
360
- }
161
+ // Graceful shutdown
162
+ process.on('SIGINT', () => this.stop());
163
+ process.on('SIGTERM', () => this.stop());
164
+ }
361
165
 
362
- // Check if user is asking for a skill we don't have
363
- const skillRequest = detectSkillRequest(message);
364
- if (skillRequest) {
365
- const availability = checkSkillAvailable(skillRequest, this.config);
366
- if (!availability.available) {
367
- const keyMsg = getKeyRequestMessage(skillRequest);
368
- if (keyMsg) {
369
- await this.telegramManager.sendMessage(agentId, contactId, keyMsg, metadata);
370
- return;
371
- }
372
- }
373
- }
166
+ // ──────────────────────────────────────────
167
+ // Init Helpers
168
+ // ──────────────────────────────────────────
374
169
 
375
- // Show typing indicator while processing
376
- const chatId = metadata.chatId || contactId.replace('tg_', '');
377
- const botInfo = this.telegramManager?.bots?.get(agentId);
378
- let typingInterval;
379
- if (botInfo?.bot) {
380
- const sendTyping = () => { try { botInfo.bot.api.sendChatAction(chatId, 'typing').catch(() => {}); } catch {} };
381
- sendTyping();
382
- typingInterval = setInterval(sendTyping, 4000);
383
- }
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);
384
178
 
385
- let result;
386
- try {
387
- result = await agent.processMessage(contactId, message, metadata);
388
- } finally {
389
- if (typingInterval) clearInterval(typingInterval);
390
- }
391
-
392
- if (result.reaction && metadata.messageId) {
393
- try { await this.telegramManager.sendReaction(agentId, contactId, metadata.messageId, result.reaction, metadata); } catch {}
394
- }
395
-
396
- if (result.messages && result.messages.length > 0) {
397
- // Handle reminder if requested
398
- if (result._reminder && this.reminders) {
399
- const { time, message: remMsg } = result._reminder;
400
- this.reminders.add(agentId, contactId, remMsg, time, 'telegram', metadata);
401
- }
402
-
403
- // Send image if generated
404
- if (result.image) {
405
- const photoData = result.image.url ? { url: result.image.url } : { base64: result.image.base64 };
406
- const caption = result.messages?.[0] || '';
407
- await this.telegramManager.sendPhoto(agentId, contactId, photoData, caption, metadata);
408
- } else {
409
- // Handle reminder if requested
410
- if (result._reminder && this.reminders) {
411
- const { time, message: remMsg } = result._reminder;
412
- this.reminders.add(agentId, contactId, remMsg, time, 'telegram', metadata);
413
- }
414
-
415
- // Send image if generated
416
- if (result.image) {
417
- await this.telegramManager.sendPhoto(agentId, contactId, result.image, result.messages?.[0] || '', metadata);
418
- } else if (metadata.originalType === 'voice' && result.messages.length === 1 && result.messages[0].length < 500) {
419
- // Reply with voice when user sent voice
420
- try {
421
- const { VoiceReply } = await import('./features/voice-reply.js');
422
- const vr = new VoiceReply(this.config);
423
- const lang = /[\u0600-\u06FF]/.test(result.messages[0]) ? 'ar' : 'en';
424
- const audio = await vr.generate(result.messages[0], { language: lang });
425
- await this.telegramManager.sendVoice(agentId, contactId, audio, metadata);
426
- } catch (err) {
427
- // Fallback to text
428
- logger.warn('voice', 'Voice reply failed, sending text: ' + err.message);
429
- await this.telegramManager.sendMessages(agentId, contactId, result.messages, metadata);
430
- }
431
- } else {
432
- await this.telegramManager.sendMessages(agentId, contactId, result.messages, metadata);
433
- }
434
- }
435
- }
436
- };
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
+ }
437
187
 
438
- // Start bot for all agents with telegram config
439
- // Only start ONE bot instance (shared token = shared bot)
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) => {
440
194
  try {
441
- const firstAgent = agents[0];
442
- if (firstAgent) {
443
- await this.telegramManager.startBot(firstAgent.id, this.config.channels.telegram.token);
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);
444
200
  }
445
201
  } catch (err) {
446
- console.log(' ✈️ Telegram: ' + err.message);
202
+ logger.error('reminder', 'Delivery failed: ' + err.message);
447
203
  }
448
- console.log(' ✈️ Telegram: connected');
449
- } catch (err) {
450
- console.log(' ✈️ Telegram: failed (' + err.message + ')');
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 {}
208
+
209
+ // Auto-memory
210
+ try {
211
+ const { AutoMemory } = await import('./features/auto-memory.js');
212
+ this.autoMemory = new AutoMemory(this.storage);
213
+ } catch {}
214
+
215
+ // Usage alerts
216
+ try {
217
+ const { UsageAlerts } = await import('./features/usage-alerts.js');
218
+ this.usageAlerts = new UsageAlerts(this.storage);
219
+ } catch {}
220
+ }
221
+
222
+ async _initTelegram(agents) {
223
+ if (!this.config.channels?.telegram?.enabled || !this.config.channels?.telegram?.token) return;
224
+
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);
451
239
  }
240
+ console.log(' ✈️ Telegram: connected');
241
+ } catch (err) {
242
+ console.log(' ✈️ Telegram: ' + err.message);
452
243
  }
244
+ }
453
245
 
454
- // 4. WhatsApp Manager
246
+ async _initWhatsApp(agents) {
455
247
  this.whatsappManager = new WhatsAppManager(this.config, this.agentManager, this.home);
456
248
 
457
- // Start WhatsApp sessions for all agents that have them configured
458
249
  for (const agent of agents) {
459
250
  if (agent.status === 'active' && agent.whatsappNumber) {
460
- try {
461
- await this.whatsappManager.startSession(agent.id);
462
- } catch (err) {
463
- 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}`);
464
253
  }
465
254
  }
466
255
  }
256
+
467
257
  const waStatuses = this.whatsappManager.getStatuses();
468
258
  const connected = Object.values(waStatuses).filter(s => s.connected).length;
469
- console.log(` 📱 WhatsApp: ${connected}/${agents.length} connected`);
470
-
471
- // 5. Channel Hub
472
- this.channelHub = new ChannelHub(this.agentManager, this.whatsappManager, this.storage, this.telegramManager);
473
- addMediaSupport(this.channelHub, this.config, this.home);
474
-
475
- // 6. Heartbeat
476
- this.heartbeat = new HeartbeatSystem(this.agentManager, this.whatsappManager, this.storage);
477
- this.heartbeat.start();
478
- console.log(` 💓 Heartbeat: active`);
259
+ console.log(` 📱 WhatsApp: ${connected}/${agents.length}`);
260
+ }
479
261
 
480
- // 7. API Server + Dashboard
262
+ async _initAPI() {
481
263
  const app = createAPIServer(this);
482
- 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
+
483
269
  this.server = app.listen(this.port, this.config.engine?.bind || '0.0.0.0', () => {
484
- 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}`);
485
271
  console.log(` ──────────────────────────`);
486
272
  console.log(` ✅ Engine running!\n`);
487
273
  });
488
-
489
- // Graceful shutdown
490
- process.on('SIGINT', () => this.stop());
491
- process.on('SIGTERM', () => this.stop());
492
274
  }
493
275
 
276
+ // ──────────────────────────────────────────
277
+ // Shutdown
278
+ // ──────────────────────────────────────────
279
+
494
280
  async stop() {
495
281
  console.log('\n 🦑 Shutting down...');
496
282
  this.heartbeat?.stop();
283
+ this.reminders?.destroy();
497
284
  await this.whatsappManager?.stopAll();
498
285
  this.storage?.close();
499
286
  this.server?.close();