squidclaw 0.6.0 → 0.7.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/api/server.js +28 -0
- package/lib/channels/hub.js +91 -1
- package/lib/engine.js +94 -1
- package/lib/features/backup.js +138 -0
- package/package.json +1 -1
package/lib/api/server.js
CHANGED
|
@@ -271,3 +271,31 @@ export function addKnowledgeRoutes(app, engine) {
|
|
|
271
271
|
}
|
|
272
272
|
});
|
|
273
273
|
}
|
|
274
|
+
|
|
275
|
+
// ── Backup & Restore ──
|
|
276
|
+
export function addBackupRoutes(app, engine) {
|
|
277
|
+
app.post('/api/agents/:id/backup', async (req, res) => {
|
|
278
|
+
try {
|
|
279
|
+
const { AgentBackup } = await import('../features/backup.js');
|
|
280
|
+
const backup = await AgentBackup.create(req.params.id, engine.home, engine.storage);
|
|
281
|
+
const filename = `${backup.agent.name || req.params.id}_backup_${new Date().toISOString().slice(0,10)}.json`;
|
|
282
|
+
const path = `/tmp/${filename}`;
|
|
283
|
+
await AgentBackup.saveToFile(backup, path);
|
|
284
|
+
res.json({ status: 'ok', path, size: JSON.stringify(backup).length, agent: backup.agent.name });
|
|
285
|
+
} catch (err) {
|
|
286
|
+
res.status(500).json({ error: err.message });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
app.post('/api/agents/restore', async (req, res) => {
|
|
291
|
+
const { path } = req.body;
|
|
292
|
+
if (!path) return res.status(400).json({ error: 'path required' });
|
|
293
|
+
try {
|
|
294
|
+
const { AgentBackup } = await import('../features/backup.js');
|
|
295
|
+
const result = await AgentBackup.restore(path, engine.home, engine.storage);
|
|
296
|
+
res.json({ status: 'restored', ...result });
|
|
297
|
+
} catch (err) {
|
|
298
|
+
res.status(500).json({ error: err.message });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
package/lib/channels/hub.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
import { logger } from '../core/logger.js';
|
|
7
7
|
|
|
8
8
|
export class ChannelHub {
|
|
9
|
-
constructor(agentManager, whatsappManager, storage) {
|
|
9
|
+
constructor(agentManager, whatsappManager, storage, telegramManager) {
|
|
10
10
|
this.agentManager = agentManager;
|
|
11
11
|
this.whatsappManager = whatsappManager;
|
|
12
12
|
this.storage = storage;
|
|
13
|
+
this.telegramManager = telegramManager || null;
|
|
13
14
|
|
|
14
15
|
// Wire up WhatsApp message handler
|
|
15
16
|
this.whatsappManager.onMessage = this._handleWhatsAppMessage.bind(this);
|
|
@@ -42,6 +43,88 @@ export class ChannelHub {
|
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
// Handle /help
|
|
47
|
+
if (message.trim() === '/help') {
|
|
48
|
+
const helpText = [
|
|
49
|
+
'🦑 *Commands*', '',
|
|
50
|
+
'/status — model, uptime, usage',
|
|
51
|
+
'/backup — save me to a backup file',
|
|
52
|
+
'/memories — what I remember',
|
|
53
|
+
'/help — this message',
|
|
54
|
+
'', 'Just chat normally! 🦑',
|
|
55
|
+
];
|
|
56
|
+
await this.whatsappManager.sendMessage(agentId, contactId, helpText.join('\n'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle /backup
|
|
61
|
+
if (message.trim() === '/backup' || (message.toLowerCase().includes('backup') && message.toLowerCase().includes('yourself'))) {
|
|
62
|
+
try {
|
|
63
|
+
const { AgentBackup } = await import('../features/backup.js');
|
|
64
|
+
const { mkdirSync } = await import('fs');
|
|
65
|
+
const home = (await import('../core/config.js')).getHome();
|
|
66
|
+
mkdirSync(home + '/backups', { recursive: true });
|
|
67
|
+
const backup = await AgentBackup.create(agentId, home, this.storage);
|
|
68
|
+
const filename = (agent.name || agentId) + '_backup_' + new Date().toISOString().slice(0,10) + '.json';
|
|
69
|
+
await AgentBackup.saveToFile(backup, home + '/backups/' + filename);
|
|
70
|
+
const size = (JSON.stringify(backup).length / 1024).toFixed(1);
|
|
71
|
+
await this.whatsappManager.sendMessage(agentId, contactId,
|
|
72
|
+
'💾 *Backup Complete!*\n\n📦 ' + filename + '\n📏 ' + size + ' KB\n💬 ' + (backup.conversations?.length || 0) + ' messages\n🧠 ' + (backup.memories?.length || 0) + ' memories\n\nI\'m safe! 🦑');
|
|
73
|
+
} catch (err) {
|
|
74
|
+
await this.whatsappManager.sendMessage(agentId, contactId, '❌ Backup failed: ' + err.message);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Handle /memories
|
|
80
|
+
if (message.trim() === '/memories') {
|
|
81
|
+
try {
|
|
82
|
+
const memories = await this.storage.getMemories(agentId);
|
|
83
|
+
if (memories.length === 0) {
|
|
84
|
+
await this.whatsappManager.sendMessage(agentId, contactId, '🧠 No memories yet!');
|
|
85
|
+
} else {
|
|
86
|
+
const memList = memories.slice(0, 20).map(m => '• ' + m.key + ': ' + m.value).join('\n');
|
|
87
|
+
await this.whatsappManager.sendMessage(agentId, contactId, '🧠 *What I Remember*\n\n' + memList);
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handle /status command
|
|
94
|
+
if (message.trim() === '/status' || message.trim().toLowerCase() === 'status') {
|
|
95
|
+
const uptime = process.uptime();
|
|
96
|
+
const h = Math.floor(uptime / 3600);
|
|
97
|
+
const m = Math.floor((uptime % 3600) / 60);
|
|
98
|
+
const usage = await this.storage.getUsage(agentId) || {};
|
|
99
|
+
const convos = await this.storage.getConversationCount?.(agentId) || 0;
|
|
100
|
+
const config = agent.config || {};
|
|
101
|
+
|
|
102
|
+
const status = [
|
|
103
|
+
'🦑 *' + (agent.name || agentId) + ' Status*',
|
|
104
|
+
'────────────────────',
|
|
105
|
+
'🧠 Model: ' + (agent.model || 'unknown'),
|
|
106
|
+
'⏱️ Uptime: ' + h + 'h ' + m + 'm',
|
|
107
|
+
'💬 Messages: ' + (usage.messages || 0),
|
|
108
|
+
'🪙 Tokens: ' + formatTokens((usage.input_tokens || 0) + (usage.output_tokens || 0)),
|
|
109
|
+
'💰 Cost: $' + (usage.cost_usd || 0).toFixed(4),
|
|
110
|
+
'────────────────────',
|
|
111
|
+
'📱 WhatsApp: ' + (metadata.platform === 'whatsapp' ? '✅ connected' : '—'),
|
|
112
|
+
'✈️ Telegram: ' + (metadata.platform === 'telegram' ? '✅ connected' : '—'),
|
|
113
|
+
'────────────────────',
|
|
114
|
+
'⚡ Skills: web search, page reader, vision, voice, memory',
|
|
115
|
+
'🗣️ Language: ' + (agent.language || 'bilingual'),
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
if (metadata.platform === 'telegram' || metadata._ctx) {
|
|
120
|
+
await this.telegramManager?.sendMessage(agentId, contactId, status.join('\n'), metadata);
|
|
121
|
+
} else {
|
|
122
|
+
await this.whatsappManager?.sendMessage(agentId, contactId, status.join('\n'));
|
|
123
|
+
}
|
|
124
|
+
} catch {}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
45
128
|
// Process message through agent
|
|
46
129
|
const result = await agent.processMessage(contactId, message, metadata);
|
|
47
130
|
|
|
@@ -101,3 +184,10 @@ export class ChannelHub {
|
|
|
101
184
|
}
|
|
102
185
|
}
|
|
103
186
|
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
function formatTokens(n) {
|
|
190
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
191
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
192
|
+
return String(n);
|
|
193
|
+
}
|
package/lib/engine.js
CHANGED
|
@@ -93,6 +93,99 @@ export class SquidclawEngine {
|
|
|
93
93
|
if (!allowed) return;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// Handle /help command
|
|
97
|
+
if (message.trim() === '/help') {
|
|
98
|
+
const helpText = [
|
|
99
|
+
'🦑 *Commands*',
|
|
100
|
+
'',
|
|
101
|
+
'/status — model, uptime, usage stats',
|
|
102
|
+
'/backup — save me to a backup file',
|
|
103
|
+
'/memories — what I remember about you',
|
|
104
|
+
'/help — this message',
|
|
105
|
+
'',
|
|
106
|
+
'Just chat normally — I\'ll search the web, remember things, and help! 🦑',
|
|
107
|
+
];
|
|
108
|
+
await this.telegramManager.sendMessage(agentId, contactId, helpText.join('\n'), metadata);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Handle /backup command
|
|
113
|
+
if (message.trim() === '/backup' || message.trim().toLowerCase() === 'backup yourself' || message.trim().toLowerCase().includes('backup')) {
|
|
114
|
+
if (message.toLowerCase().includes('backup') && (message.toLowerCase().includes('yourself') || message.toLowerCase().includes('your') || message.trim() === '/backup')) {
|
|
115
|
+
try {
|
|
116
|
+
const { AgentBackup } = await import('./features/backup.js');
|
|
117
|
+
const backup = await AgentBackup.create(agentId, this.home, this.storage);
|
|
118
|
+
const filename = (agent.name || agentId) + '_backup_' + new Date().toISOString().slice(0,10) + '.json';
|
|
119
|
+
const path = this.home + '/backups/' + filename;
|
|
120
|
+
const { mkdirSync } = await import('fs');
|
|
121
|
+
mkdirSync(this.home + '/backups', { recursive: true });
|
|
122
|
+
await AgentBackup.saveToFile(backup, path);
|
|
123
|
+
const size = (JSON.stringify(backup).length / 1024).toFixed(1);
|
|
124
|
+
const lines = [
|
|
125
|
+
'💾 *Backup Complete!*',
|
|
126
|
+
'',
|
|
127
|
+
'📦 File: ' + filename,
|
|
128
|
+
'📏 Size: ' + size + ' KB',
|
|
129
|
+
'💬 Messages: ' + (backup.conversations?.length || 0),
|
|
130
|
+
'🧠 Memories: ' + (backup.memories?.length || 0),
|
|
131
|
+
'📄 Files: ' + Object.keys(backup.files).length,
|
|
132
|
+
'',
|
|
133
|
+
'I\'m safe! You can resurrect me anytime with this file 🦑',
|
|
134
|
+
];
|
|
135
|
+
await this.telegramManager.sendMessage(agentId, contactId, lines.join('\n'), metadata);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
await this.telegramManager.sendMessage(agentId, contactId, '❌ Backup failed: ' + err.message, metadata);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Handle /memories command
|
|
144
|
+
if (message.trim() === '/memories' || message.trim() === '/remember') {
|
|
145
|
+
try {
|
|
146
|
+
const memories = await this.storage.getMemories(agentId);
|
|
147
|
+
if (memories.length === 0) {
|
|
148
|
+
await this.telegramManager.sendMessage(agentId, contactId, '🧠 No memories yet. Tell me things and I\'ll remember!', metadata);
|
|
149
|
+
} else {
|
|
150
|
+
const memList = memories.slice(0, 20).map(m => '• ' + m.key + ': ' + m.value).join('\n');
|
|
151
|
+
await this.telegramManager.sendMessage(agentId, contactId, '🧠 *What I Remember*\n\n' + memList, metadata);
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
await this.telegramManager.sendMessage(agentId, contactId, '🧠 Memory error: ' + err.message, metadata);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Handle /status command
|
|
160
|
+
if (message.trim() === '/status' || message.trim().toLowerCase() === 'status') {
|
|
161
|
+
const uptime = process.uptime();
|
|
162
|
+
const h = Math.floor(uptime / 3600);
|
|
163
|
+
const m = Math.floor((uptime % 3600) / 60);
|
|
164
|
+
const usage = await this.storage.getUsage(agentId) || {};
|
|
165
|
+
const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
166
|
+
const fmtT = tokens >= 1000000 ? (tokens/1000000).toFixed(1)+'M' : tokens >= 1000 ? (tokens/1000).toFixed(1)+'K' : String(tokens);
|
|
167
|
+
const waOn = Object.values(this.whatsappManager?.getStatuses() || {}).some(s => s.connected);
|
|
168
|
+
|
|
169
|
+
const lines = [
|
|
170
|
+
`🦑 *${agent.name || agentId} Status*`,
|
|
171
|
+
'────────────────────',
|
|
172
|
+
`🧠 Model: ${agent.model || 'unknown'}`,
|
|
173
|
+
`⏱️ Uptime: ${h}h ${m}m`,
|
|
174
|
+
`💬 Messages: ${usage.messages || 0}`,
|
|
175
|
+
`🪙 Tokens: ${fmtT}`,
|
|
176
|
+
`💰 Cost: $${(usage.cost_usd || 0).toFixed(4)}`,
|
|
177
|
+
'────────────────────',
|
|
178
|
+
`📱 WhatsApp: ${waOn ? '✅' : '❌'}`,
|
|
179
|
+
'✈️ Telegram: ✅ connected',
|
|
180
|
+
'────────────────────',
|
|
181
|
+
'⚡ Skills: search, reader, vision, voice, memory',
|
|
182
|
+
`🗣️ Language: ${agent.language || 'bilingual'}`,
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
await this.telegramManager.sendMessage(agentId, contactId, lines.join('\n'), metadata);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
96
189
|
const result = await agent.processMessage(contactId, message, metadata);
|
|
97
190
|
|
|
98
191
|
if (result.reaction && metadata.messageId) {
|
|
@@ -138,7 +231,7 @@ export class SquidclawEngine {
|
|
|
138
231
|
console.log(` 📱 WhatsApp: ${connected}/${agents.length} connected`);
|
|
139
232
|
|
|
140
233
|
// 5. Channel Hub
|
|
141
|
-
this.channelHub = new ChannelHub(this.agentManager, this.whatsappManager, this.storage);
|
|
234
|
+
this.channelHub = new ChannelHub(this.agentManager, this.whatsappManager, this.storage, this.telegramManager);
|
|
142
235
|
addMediaSupport(this.channelHub, this.config, this.home);
|
|
143
236
|
|
|
144
237
|
// 6. Heartbeat
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 Agent Backup & Restore
|
|
3
|
+
* Saves everything about an agent to a single JSON file
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
7
|
+
import { join, basename } from 'path';
|
|
8
|
+
|
|
9
|
+
export class AgentBackup {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a full backup of an agent — soul, memory, conversations, config
|
|
13
|
+
*/
|
|
14
|
+
static async create(agentId, home, storage) {
|
|
15
|
+
const agentDir = join(home, 'agents', agentId);
|
|
16
|
+
if (!existsSync(agentDir)) throw new Error('Agent not found: ' + agentId);
|
|
17
|
+
|
|
18
|
+
const backup = {
|
|
19
|
+
version: '1.0',
|
|
20
|
+
type: 'squidclaw-agent-backup',
|
|
21
|
+
createdAt: new Date().toISOString(),
|
|
22
|
+
agent: {},
|
|
23
|
+
files: {},
|
|
24
|
+
conversations: [],
|
|
25
|
+
memories: [],
|
|
26
|
+
usage: null,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Read agent manifest
|
|
30
|
+
const manifestPath = join(agentDir, 'agent.json');
|
|
31
|
+
if (existsSync(manifestPath)) {
|
|
32
|
+
backup.agent = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Read all .md and .json files in agent dir
|
|
36
|
+
const readDir = (dir, prefix) => {
|
|
37
|
+
if (!existsSync(dir)) return;
|
|
38
|
+
for (const file of readdirSync(dir, { withFileTypes: true })) {
|
|
39
|
+
const fullPath = join(dir, file.name);
|
|
40
|
+
const key = prefix ? prefix + '/' + file.name : file.name;
|
|
41
|
+
if (file.isDirectory()) {
|
|
42
|
+
readDir(fullPath, key);
|
|
43
|
+
} else if (file.name.endsWith('.md') || file.name.endsWith('.json') || file.name.endsWith('.txt')) {
|
|
44
|
+
backup.files[key] = readFileSync(fullPath, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
readDir(agentDir, '');
|
|
49
|
+
|
|
50
|
+
// Get conversations from DB
|
|
51
|
+
if (storage) {
|
|
52
|
+
try {
|
|
53
|
+
const convos = storage.db.prepare(
|
|
54
|
+
'SELECT * FROM messages WHERE agent_id = ? ORDER BY created_at DESC LIMIT 500'
|
|
55
|
+
).all(agentId);
|
|
56
|
+
backup.conversations = convos;
|
|
57
|
+
} catch {}
|
|
58
|
+
|
|
59
|
+
// Get memories
|
|
60
|
+
try {
|
|
61
|
+
const memories = storage.db.prepare(
|
|
62
|
+
'SELECT * FROM memories WHERE agent_id = ?'
|
|
63
|
+
).all(agentId);
|
|
64
|
+
backup.memories = memories;
|
|
65
|
+
} catch {}
|
|
66
|
+
|
|
67
|
+
// Get usage
|
|
68
|
+
try {
|
|
69
|
+
backup.usage = storage.db.prepare(
|
|
70
|
+
'SELECT SUM(input_tokens) as input_tokens, SUM(output_tokens) as output_tokens, SUM(cost_usd) as cost_usd, COUNT(*) as calls FROM usage WHERE agent_id = ?'
|
|
71
|
+
).get(agentId);
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return backup;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Save backup to file
|
|
80
|
+
*/
|
|
81
|
+
static async saveToFile(backup, outputPath) {
|
|
82
|
+
writeFileSync(outputPath, JSON.stringify(backup, null, 2));
|
|
83
|
+
return outputPath;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Restore agent from backup file
|
|
88
|
+
*/
|
|
89
|
+
static async restore(backupPath, home, storage) {
|
|
90
|
+
const backup = JSON.parse(readFileSync(backupPath, 'utf8'));
|
|
91
|
+
|
|
92
|
+
if (backup.type !== 'squidclaw-agent-backup') {
|
|
93
|
+
throw new Error('Invalid backup file');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const agentId = backup.agent.id;
|
|
97
|
+
const agentDir = join(home, 'agents', agentId);
|
|
98
|
+
|
|
99
|
+
// Recreate directory structure
|
|
100
|
+
mkdirSync(join(agentDir, 'memory'), { recursive: true });
|
|
101
|
+
|
|
102
|
+
// Restore all files
|
|
103
|
+
for (const [key, content] of Object.entries(backup.files)) {
|
|
104
|
+
const filePath = join(agentDir, key);
|
|
105
|
+
const dir = join(filePath, '..');
|
|
106
|
+
mkdirSync(dir, { recursive: true });
|
|
107
|
+
writeFileSync(filePath, content);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Restore conversations to DB
|
|
111
|
+
if (storage && backup.conversations.length > 0) {
|
|
112
|
+
const insert = storage.db.prepare(
|
|
113
|
+
'INSERT OR IGNORE INTO messages (id, agent_id, contact_id, role, content, created_at) VALUES (?, ?, ?, ?, ?, ?)'
|
|
114
|
+
);
|
|
115
|
+
const tx = storage.db.transaction(() => {
|
|
116
|
+
for (const msg of backup.conversations) {
|
|
117
|
+
insert.run(msg.id, msg.agent_id, msg.contact_id, msg.role, msg.content, msg.created_at);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
tx();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Restore memories
|
|
124
|
+
if (storage && backup.memories.length > 0) {
|
|
125
|
+
const insert = storage.db.prepare(
|
|
126
|
+
'INSERT OR IGNORE INTO memories (id, agent_id, key, value, created_at) VALUES (?, ?, ?, ?, ?)'
|
|
127
|
+
);
|
|
128
|
+
const tx = storage.db.transaction(() => {
|
|
129
|
+
for (const mem of backup.memories) {
|
|
130
|
+
insert.run(mem.id, mem.agent_id, mem.key, mem.value, mem.created_at);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
tx();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { agentId, name: backup.agent.name, filesRestored: Object.keys(backup.files).length };
|
|
137
|
+
}
|
|
138
|
+
}
|