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 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
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squidclaw",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "\ud83e\udd91 AI agent platform \u2014 human-like agents for WhatsApp, Telegram & more",
5
5
  "main": "lib/engine.js",
6
6
  "bin": {