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/core/agent-tools-mixin.js +2 -0
- package/lib/engine.js +196 -409
- 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/calculator.js +27 -0
- package/lib/tools/password.js +18 -0
- package/lib/tools/router.js +90 -0
- package/lib/tools/shortener.js +9 -0
- package/lib/tools/tasks.js +42 -0
- package/lib/tools/weather.js +25 -0
- package/package.json +1 -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,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
|
|
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
|
-
}
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
161
|
+
// Graceful shutdown
|
|
162
|
+
process.on('SIGINT', () => this.stop());
|
|
163
|
+
process.on('SIGTERM', () => this.stop());
|
|
164
|
+
}
|
|
361
165
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
439
|
-
|
|
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
|
|
442
|
-
if (
|
|
443
|
-
await this.telegramManager.
|
|
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
|
-
|
|
202
|
+
logger.error('reminder', 'Delivery failed: ' + err.message);
|
|
447
203
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
262
|
+
async _initAPI() {
|
|
481
263
|
const app = createAPIServer(this);
|
|
482
|
-
try {
|
|
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
|
|
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();
|