squidclaw 1.1.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 +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/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,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();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core AI processing — sends message to agent, gets response
|
|
3
|
+
*/
|
|
4
|
+
import { logger } from '../core/logger.js';
|
|
5
|
+
|
|
6
|
+
export async function aiProcessMiddleware(ctx, next) {
|
|
7
|
+
if (ctx.handled) return;
|
|
8
|
+
|
|
9
|
+
const result = await ctx.agent.processMessage(ctx.contactId, ctx.message, ctx.metadata);
|
|
10
|
+
|
|
11
|
+
ctx.response = {
|
|
12
|
+
messages: result.messages || [],
|
|
13
|
+
reaction: result.reaction || null,
|
|
14
|
+
image: result.image || null,
|
|
15
|
+
_reminder: result._reminder || null,
|
|
16
|
+
};
|
|
17
|
+
ctx.metadata.originalType = ctx.metadata.originalType || null;
|
|
18
|
+
|
|
19
|
+
await next();
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowlist middleware — blocks unauthorized senders
|
|
3
|
+
*/
|
|
4
|
+
export async function allowlistMiddleware(ctx, next) {
|
|
5
|
+
const allowFrom = ctx.config.channels?.[ctx.platform]?.allowFrom;
|
|
6
|
+
if (allowFrom && allowFrom !== '*') {
|
|
7
|
+
const senderId = ctx.contactId.replace('tg_', '');
|
|
8
|
+
const allowed = Array.isArray(allowFrom) ? allowFrom.includes(senderId) : String(allowFrom) === senderId;
|
|
9
|
+
if (!allowed) {
|
|
10
|
+
ctx.handled = true;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
await next();
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects and saves API keys pasted in chat
|
|
3
|
+
*/
|
|
4
|
+
export async function apiKeyDetectMiddleware(ctx, next) {
|
|
5
|
+
const { detectApiKey, saveApiKey, getKeyConfirmation } = await import('../features/self-config.js');
|
|
6
|
+
|
|
7
|
+
const keyDetected = detectApiKey(ctx.message);
|
|
8
|
+
if (keyDetected && keyDetected.provider !== 'unknown') {
|
|
9
|
+
saveApiKey(keyDetected.provider, keyDetected.key);
|
|
10
|
+
const { loadConfig } = await import('../core/config.js');
|
|
11
|
+
ctx.engine.config = loadConfig();
|
|
12
|
+
ctx.config = ctx.engine.config;
|
|
13
|
+
await ctx.reply(getKeyConfirmation(keyDetected.provider));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await next();
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-detect and read URLs in messages
|
|
3
|
+
*/
|
|
4
|
+
export async function autoLinksMiddleware(ctx, next) {
|
|
5
|
+
const urlRegex = /https?:\/\/[^\s<>"')\]]+/gi;
|
|
6
|
+
if (urlRegex.test(ctx.message) && ctx.engine.toolRouter?.browser) {
|
|
7
|
+
try {
|
|
8
|
+
const { extractAndReadLinks } = await import('../features/auto-links.js');
|
|
9
|
+
const content = await extractAndReadLinks(ctx.message, ctx.engine.toolRouter.browser);
|
|
10
|
+
if (content) ctx.metadata._linkContext = content;
|
|
11
|
+
} catch {}
|
|
12
|
+
}
|
|
13
|
+
await next();
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract facts from messages automatically
|
|
3
|
+
*/
|
|
4
|
+
export async function autoMemoryMiddleware(ctx, next) {
|
|
5
|
+
if (ctx.engine.autoMemory) {
|
|
6
|
+
try { await ctx.engine.autoMemory.extract(ctx.agentId, ctx.contactId, ctx.message); } catch {}
|
|
7
|
+
}
|
|
8
|
+
await next();
|
|
9
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle slash commands: /help, /status, /backup, /memories, /tasks, /usage
|
|
3
|
+
*/
|
|
4
|
+
export async function commandsMiddleware(ctx, next) {
|
|
5
|
+
const msg = ctx.message.trim();
|
|
6
|
+
const cmd = msg.startsWith('/') ? msg.split(' ')[0].toLowerCase() : null;
|
|
7
|
+
|
|
8
|
+
// Also match plain text commands
|
|
9
|
+
const textCmd = msg.toLowerCase();
|
|
10
|
+
|
|
11
|
+
if (cmd === '/help') {
|
|
12
|
+
await ctx.reply([
|
|
13
|
+
'🦑 *Commands*', '',
|
|
14
|
+
'/status — model, uptime, usage stats',
|
|
15
|
+
'/backup — save me to a backup file',
|
|
16
|
+
'/memories — what I remember about you',
|
|
17
|
+
'/tasks — your todo list',
|
|
18
|
+
'/usage — spending report',
|
|
19
|
+
'/help — this message',
|
|
20
|
+
'', 'Just chat normally! 🦑',
|
|
21
|
+
].join('\n'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (cmd === '/status' || textCmd === 'status') {
|
|
26
|
+
const uptime = process.uptime();
|
|
27
|
+
const h = Math.floor(uptime / 3600);
|
|
28
|
+
const m = Math.floor((uptime % 3600) / 60);
|
|
29
|
+
const usage = await ctx.storage.getUsage(ctx.agentId) || {};
|
|
30
|
+
const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
31
|
+
const fmtT = tokens >= 1e6 ? (tokens/1e6).toFixed(1)+'M' : tokens >= 1e3 ? (tokens/1e3).toFixed(1)+'K' : String(tokens);
|
|
32
|
+
const waOn = Object.values(ctx.engine.whatsappManager?.getStatuses() || {}).some(s => s.connected);
|
|
33
|
+
|
|
34
|
+
await ctx.reply([
|
|
35
|
+
`🦑 *${ctx.agent?.name || ctx.agentId} Status*`,
|
|
36
|
+
'────────────────────',
|
|
37
|
+
`🧠 Model: ${ctx.agent?.model || 'unknown'}`,
|
|
38
|
+
`⏱️ Uptime: ${h}h ${m}m`,
|
|
39
|
+
`💬 Messages: ${usage.messages || 0}`,
|
|
40
|
+
`🪙 Tokens: ${fmtT}`,
|
|
41
|
+
`💰 Cost: $${(usage.cost_usd || 0).toFixed(4)}`,
|
|
42
|
+
'────────────────────',
|
|
43
|
+
`📱 WhatsApp: ${waOn ? '✅' : '❌'}`,
|
|
44
|
+
`✈️ Telegram: ${ctx.engine.telegramManager ? '✅' : '❌'}`,
|
|
45
|
+
'────────────────────',
|
|
46
|
+
`⚡ Skills: 20+`,
|
|
47
|
+
`🗣️ Language: ${ctx.agent?.language || 'bilingual'}`,
|
|
48
|
+
].join('\n'));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (cmd === '/backup' || (textCmd.includes('backup') && textCmd.includes('yourself'))) {
|
|
53
|
+
try {
|
|
54
|
+
const { AgentBackup } = await import('../features/backup.js');
|
|
55
|
+
const { mkdirSync } = await import('fs');
|
|
56
|
+
const backup = await AgentBackup.create(ctx.agentId, ctx.engine.home, ctx.storage);
|
|
57
|
+
const filename = (ctx.agent?.name || ctx.agentId) + '_backup_' + new Date().toISOString().slice(0, 10) + '.json';
|
|
58
|
+
mkdirSync(ctx.engine.home + '/backups', { recursive: true });
|
|
59
|
+
await AgentBackup.saveToFile(backup, ctx.engine.home + '/backups/' + filename);
|
|
60
|
+
const size = (JSON.stringify(backup).length / 1024).toFixed(1);
|
|
61
|
+
await ctx.reply([
|
|
62
|
+
'💾 *Backup Complete!*', '',
|
|
63
|
+
'📦 ' + filename,
|
|
64
|
+
'📏 ' + size + ' KB',
|
|
65
|
+
'💬 ' + (backup.conversations?.length || 0) + ' messages',
|
|
66
|
+
'🧠 ' + (backup.memories?.length || 0) + ' memories',
|
|
67
|
+
'', 'I am safe! 🦑',
|
|
68
|
+
].join('\n'));
|
|
69
|
+
} catch (err) {
|
|
70
|
+
await ctx.reply('❌ Backup failed: ' + err.message);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (cmd === '/memories' || cmd === '/remember') {
|
|
76
|
+
const memories = await ctx.storage.getMemories(ctx.agentId);
|
|
77
|
+
if (memories.length === 0) {
|
|
78
|
+
await ctx.reply('🧠 No memories yet. Tell me things!');
|
|
79
|
+
} else {
|
|
80
|
+
const list = memories.slice(0, 20).map(m => '• ' + m.key + ': ' + m.value).join('\n');
|
|
81
|
+
await ctx.reply('🧠 *What I Remember*\n\n' + list);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (cmd === '/tasks' || cmd === '/todo') {
|
|
87
|
+
try {
|
|
88
|
+
const { TaskManager } = await import('../tools/tasks.js');
|
|
89
|
+
const tm = new TaskManager(ctx.storage);
|
|
90
|
+
const tasks = tm.list(ctx.agentId, ctx.contactId);
|
|
91
|
+
if (tasks.length === 0) {
|
|
92
|
+
await ctx.reply('📋 No pending tasks! Tell me to add one.');
|
|
93
|
+
} else {
|
|
94
|
+
const list = tasks.map((t, i) => (i + 1) + '. ' + t.task).join('\n');
|
|
95
|
+
await ctx.reply('📋 *Your Tasks*\n\n' + list);
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
await ctx.reply('❌ ' + err.message);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (cmd === '/usage') {
|
|
104
|
+
try {
|
|
105
|
+
const { UsageAlerts } = await import('../features/usage-alerts.js');
|
|
106
|
+
const ua = new UsageAlerts(ctx.storage);
|
|
107
|
+
const s = await ua.getSummary(ctx.agentId);
|
|
108
|
+
const fmtT = (n) => n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : String(n);
|
|
109
|
+
await ctx.reply([
|
|
110
|
+
'📊 *Usage Report*', '',
|
|
111
|
+
'*Today:*', ' 💬 ' + s.today.calls + ' msgs · 🪙 ' + fmtT(s.today.tokens) + ' tokens · 💰 $' + s.today.cost, '',
|
|
112
|
+
'*30 days:*', ' 💬 ' + s.month.calls + ' msgs · 🪙 ' + fmtT(s.month.tokens) + ' tokens · 💰 $' + s.month.cost,
|
|
113
|
+
].join('\n'));
|
|
114
|
+
} catch (err) {
|
|
115
|
+
await ctx.reply('❌ ' + err.message);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await next();
|
|
121
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process media: voice → transcribe, image → analyze, document → ingest
|
|
3
|
+
*/
|
|
4
|
+
import { logger } from '../core/logger.js';
|
|
5
|
+
|
|
6
|
+
export async function mediaMiddleware(ctx, next) {
|
|
7
|
+
if (!ctx.metadata._ctx || !ctx.metadata.mediaType) {
|
|
8
|
+
await next();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const token = ctx.config.channels?.telegram?.token;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
if (ctx.metadata.mediaType === 'audio') {
|
|
16
|
+
await _transcribeVoice(ctx, token);
|
|
17
|
+
} else if (ctx.metadata.mediaType === 'image') {
|
|
18
|
+
await _analyzeImage(ctx, token);
|
|
19
|
+
} else if (ctx.metadata.mediaType === 'document') {
|
|
20
|
+
await _ingestDocument(ctx, token);
|
|
21
|
+
if (ctx.handled) return;
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
logger.error('media', 'Processing error: ' + err.message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await next();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function _transcribeVoice(ctx, token) {
|
|
31
|
+
const file = await ctx.metadata._ctx.getFile();
|
|
32
|
+
const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
|
33
|
+
const buffer = Buffer.from(await (await fetch(url)).arrayBuffer());
|
|
34
|
+
|
|
35
|
+
const groqKey = ctx.config.ai?.providers?.groq?.key;
|
|
36
|
+
const openaiKey = ctx.config.ai?.providers?.openai?.key;
|
|
37
|
+
const apiKey = groqKey || openaiKey;
|
|
38
|
+
if (!apiKey) return;
|
|
39
|
+
|
|
40
|
+
const apiUrl = groqKey ? 'https://api.groq.com/openai/v1/audio/transcriptions' : 'https://api.openai.com/v1/audio/transcriptions';
|
|
41
|
+
const model = groqKey ? 'whisper-large-v3' : 'whisper-1';
|
|
42
|
+
|
|
43
|
+
const form = new FormData();
|
|
44
|
+
form.append('file', new Blob([buffer], { type: 'audio/ogg' }), 'voice.ogg');
|
|
45
|
+
form.append('model', model);
|
|
46
|
+
|
|
47
|
+
const res = await fetch(apiUrl, { method: 'POST', headers: { 'Authorization': 'Bearer ' + apiKey }, body: form });
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
if (data.text) {
|
|
50
|
+
ctx.message = '[Voice note]: "' + data.text + '"';
|
|
51
|
+
ctx.metadata.originalType = 'voice';
|
|
52
|
+
logger.info('media', 'Transcribed: ' + data.text.slice(0, 50));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function _analyzeImage(ctx, token) {
|
|
57
|
+
const photos = ctx.metadata._ctx.message?.photo;
|
|
58
|
+
if (!photos?.length) return;
|
|
59
|
+
|
|
60
|
+
const photo = photos[photos.length - 1];
|
|
61
|
+
const file = await ctx.metadata._ctx.api.getFile(photo.file_id);
|
|
62
|
+
const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
|
63
|
+
const buffer = Buffer.from(await (await fetch(url)).arrayBuffer());
|
|
64
|
+
const base64 = buffer.toString('base64');
|
|
65
|
+
|
|
66
|
+
const anthropicKey = ctx.config.ai?.providers?.anthropic?.key;
|
|
67
|
+
if (!anthropicKey) return;
|
|
68
|
+
|
|
69
|
+
const caption = ctx.message.replace('[📸 Image]', '').trim();
|
|
70
|
+
const prompt = caption || 'What is in this image? Be concise.';
|
|
71
|
+
|
|
72
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'x-api-key': anthropicKey, 'content-type': 'application/json', 'anthropic-version': '2023-06-01' },
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
model: 'claude-sonnet-4-20250514',
|
|
77
|
+
max_tokens: 300,
|
|
78
|
+
messages: [{ role: 'user', content: [
|
|
79
|
+
{ type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: base64 } },
|
|
80
|
+
{ type: 'text', text: prompt },
|
|
81
|
+
]}],
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
const analysis = data.content?.[0]?.text;
|
|
86
|
+
if (analysis) {
|
|
87
|
+
ctx.message = caption
|
|
88
|
+
? `[Image: "${caption}"] ${analysis}`
|
|
89
|
+
: `[Image] ${analysis}`;
|
|
90
|
+
logger.info('media', 'Analyzed image: ' + analysis.slice(0, 50));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function _ingestDocument(ctx, token) {
|
|
95
|
+
const file = await ctx.metadata._ctx.getFile();
|
|
96
|
+
const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
|
|
97
|
+
const buffer = Buffer.from(await (await fetch(url)).arrayBuffer());
|
|
98
|
+
const filename = ctx.message.match(/Document: (.+?)\]/)?.[1] || 'file.txt';
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const { DocIngester } = await import('../features/doc-ingest.js');
|
|
102
|
+
const ingester = new DocIngester(ctx.storage, ctx.engine.knowledgeBase, ctx.engine.home);
|
|
103
|
+
const result = await ingester.ingest(ctx.agentId, buffer, filename, ctx.metadata.mimeType);
|
|
104
|
+
await ctx.reply(`📄 *Document absorbed!*\n📁 ${filename}\n📊 ${result.chunks} chunks\n📝 ${result.chars} chars\n\nAsk me anything about it! 🦑`);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
ctx.message = `[Document: ${filename}] (Error: ${err.message})`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 Message Pipeline
|
|
3
|
+
* Clean middleware-based message processing
|
|
4
|
+
*
|
|
5
|
+
* Flow: message → [middleware1] → [middleware2] → ... → response
|
|
6
|
+
* Each middleware can: modify context, short-circuit (return response), or pass through
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from '../core/logger.js';
|
|
10
|
+
|
|
11
|
+
export class MessagePipeline {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.middleware = [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add middleware to the pipeline
|
|
18
|
+
* @param {string} name - middleware name for logging
|
|
19
|
+
* @param {Function} fn - async (ctx, next) => void
|
|
20
|
+
*/
|
|
21
|
+
use(name, fn) {
|
|
22
|
+
this.middleware.push({ name, fn });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Process a message through the pipeline
|
|
27
|
+
* @param {object} ctx - message context
|
|
28
|
+
* @returns {object} ctx with response populated
|
|
29
|
+
*/
|
|
30
|
+
async process(ctx) {
|
|
31
|
+
let index = 0;
|
|
32
|
+
|
|
33
|
+
const next = async () => {
|
|
34
|
+
if (index >= this.middleware.length) return;
|
|
35
|
+
if (ctx.handled) return; // short-circuited
|
|
36
|
+
|
|
37
|
+
const mw = this.middleware[index++];
|
|
38
|
+
try {
|
|
39
|
+
await mw.fn(ctx, next);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
logger.error('pipeline', `Middleware "${mw.name}" error: ${err.message}`);
|
|
42
|
+
// Don't crash — continue to next middleware
|
|
43
|
+
await next();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
await next();
|
|
48
|
+
return ctx;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a message context object
|
|
54
|
+
*/
|
|
55
|
+
export function createContext(engine, agentId, contactId, message, metadata) {
|
|
56
|
+
return {
|
|
57
|
+
// Input
|
|
58
|
+
engine,
|
|
59
|
+
agentId,
|
|
60
|
+
contactId,
|
|
61
|
+
message, // mutable — middleware can modify
|
|
62
|
+
originalMessage: message,
|
|
63
|
+
metadata,
|
|
64
|
+
platform: metadata.platform || 'telegram',
|
|
65
|
+
|
|
66
|
+
// Agent
|
|
67
|
+
agent: engine.agentManager.get(agentId),
|
|
68
|
+
config: engine.config,
|
|
69
|
+
storage: engine.storage,
|
|
70
|
+
|
|
71
|
+
// State (middleware populates these)
|
|
72
|
+
handled: false, // true = stop pipeline, send response
|
|
73
|
+
response: null, // { messages: string[], reaction: string|null, image: object|null, voice: Buffer|null, _reminder: object|null }
|
|
74
|
+
linkContext: null, // fetched URL content
|
|
75
|
+
memoryExtracted: [], // auto-extracted facts
|
|
76
|
+
|
|
77
|
+
// Helpers
|
|
78
|
+
reply: async function(text) {
|
|
79
|
+
this.handled = true;
|
|
80
|
+
this.response = { messages: [text] };
|
|
81
|
+
},
|
|
82
|
+
replyMulti: async function(messages) {
|
|
83
|
+
this.handled = true;
|
|
84
|
+
this.response = { messages };
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send the response back to the user via the appropriate channel
|
|
3
|
+
*/
|
|
4
|
+
import { logger } from '../core/logger.js';
|
|
5
|
+
|
|
6
|
+
export async function responseSenderMiddleware(ctx, next) {
|
|
7
|
+
if (!ctx.response) return;
|
|
8
|
+
|
|
9
|
+
const tm = ctx.engine.telegramManager;
|
|
10
|
+
const wm = ctx.engine.whatsappManager;
|
|
11
|
+
const { agentId, contactId, metadata, response } = ctx;
|
|
12
|
+
|
|
13
|
+
// Handle reminder
|
|
14
|
+
if (response._reminder && ctx.engine.reminders) {
|
|
15
|
+
ctx.engine.reminders.add(agentId, contactId, response._reminder.message, response._reminder.time, ctx.platform, metadata);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Send reaction
|
|
19
|
+
if (response.reaction && metadata.messageId) {
|
|
20
|
+
try {
|
|
21
|
+
if (ctx.platform === 'telegram' && tm) {
|
|
22
|
+
await tm.sendReaction(agentId, contactId, metadata.messageId, response.reaction, metadata);
|
|
23
|
+
}
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// No messages to send
|
|
28
|
+
if (!response.messages?.length && !response.image) return;
|
|
29
|
+
|
|
30
|
+
// Send via appropriate channel
|
|
31
|
+
if (ctx.platform === 'telegram' && tm) {
|
|
32
|
+
if (response.image) {
|
|
33
|
+
const photoData = response.image.url ? { url: response.image.url } : { base64: response.image.base64 };
|
|
34
|
+
await tm.sendPhoto(agentId, contactId, photoData, response.messages?.[0] || '', metadata);
|
|
35
|
+
} else if (metadata.originalType === 'voice' && response.messages.length === 1 && response.messages[0].length < 500) {
|
|
36
|
+
try {
|
|
37
|
+
const { VoiceReply } = await import('../features/voice-reply.js');
|
|
38
|
+
const vr = new VoiceReply(ctx.config);
|
|
39
|
+
const lang = /[\u0600-\u06FF]/.test(response.messages[0]) ? 'ar' : 'en';
|
|
40
|
+
const audio = await vr.generate(response.messages[0], { language: lang });
|
|
41
|
+
await tm.sendVoice(agentId, contactId, audio, metadata);
|
|
42
|
+
} catch {
|
|
43
|
+
await tm.sendMessages(agentId, contactId, response.messages, metadata);
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
await tm.sendMessages(agentId, contactId, response.messages, metadata);
|
|
47
|
+
}
|
|
48
|
+
} else if (wm) {
|
|
49
|
+
for (const msg of response.messages) {
|
|
50
|
+
await wm.sendMessage(agentId, contactId, msg);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await next();
|
|
55
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if user is asking for a skill that needs an API key
|
|
3
|
+
*/
|
|
4
|
+
export async function skillCheckMiddleware(ctx, next) {
|
|
5
|
+
const { detectSkillRequest, checkSkillAvailable, getKeyRequestMessage } = await import('../features/self-config.js');
|
|
6
|
+
const skill = detectSkillRequest(ctx.message);
|
|
7
|
+
if (skill) {
|
|
8
|
+
const avail = checkSkillAvailable(skill, ctx.config);
|
|
9
|
+
if (!avail.available) {
|
|
10
|
+
const msg = getKeyRequestMessage(skill);
|
|
11
|
+
if (msg) { await ctx.reply(msg); return; }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
await next();
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Show typing indicator while AI processes
|
|
3
|
+
* Wraps the next() call with typing on/off
|
|
4
|
+
*/
|
|
5
|
+
export async function typingMiddleware(ctx, next) {
|
|
6
|
+
if (ctx.handled || ctx.platform !== 'telegram') {
|
|
7
|
+
await next();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const chatId = ctx.metadata.chatId || ctx.contactId.replace('tg_', '');
|
|
12
|
+
const botInfo = ctx.engine.telegramManager?.bots?.get(ctx.agentId);
|
|
13
|
+
|
|
14
|
+
let interval;
|
|
15
|
+
if (botInfo?.bot) {
|
|
16
|
+
const sendTyping = () => { try { botInfo.bot.api.sendChatAction(chatId, 'typing').catch(() => {}); } catch {} };
|
|
17
|
+
sendTyping();
|
|
18
|
+
interval = setInterval(sendTyping, 4000);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await next();
|
|
23
|
+
} finally {
|
|
24
|
+
if (interval) clearInterval(interval);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check spending thresholds and alert
|
|
3
|
+
*/
|
|
4
|
+
export async function usageAlertsMiddleware(ctx, next) {
|
|
5
|
+
await next(); // Run after AI responds
|
|
6
|
+
|
|
7
|
+
if (ctx.engine.usageAlerts) {
|
|
8
|
+
try {
|
|
9
|
+
const alert = await ctx.engine.usageAlerts.check(ctx.agentId);
|
|
10
|
+
if (alert.alert) {
|
|
11
|
+
// Append alert to response
|
|
12
|
+
const alertMsg = '⚠️ *Usage Alert* — $' + alert.total + ' spent in 24h';
|
|
13
|
+
if (ctx.response?.messages) {
|
|
14
|
+
ctx.response.messages.push(alertMsg);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
}
|