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 +195 -427
- package/lib/middleware/ai-process.js +20 -0
- package/lib/middleware/allowlist.js +15 -0
- package/lib/middleware/api-key-detect.js +17 -0
- package/lib/middleware/auto-links.js +14 -0
- package/lib/middleware/auto-memory.js +9 -0
- package/lib/middleware/commands.js +121 -0
- package/lib/middleware/media.js +108 -0
- package/lib/middleware/pipeline.js +87 -0
- package/lib/middleware/response-sender.js +55 -0
- package/lib/middleware/skill-check.js +15 -0
- package/lib/middleware/typing.js +26 -0
- package/lib/middleware/usage-alerts.js +19 -0
- package/lib/tools/browser-control.js +218 -0
- package/lib/tools/router.js +43 -0
- package/package.json +2 -1
package/lib/engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
49
|
-
console.log(`
|
|
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}
|
|
131
|
+
console.log(` 👥 Agents: ${agents.length}`);
|
|
59
132
|
|
|
60
|
-
//
|
|
61
|
-
|
|
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
|
-
//
|
|
78
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
262
|
+
async _initAPI() {
|
|
500
263
|
const app = createAPIServer(this);
|
|
501
|
-
try {
|
|
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
|
|
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();
|