squidclaw 0.6.1 → 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
+ }
@@ -43,6 +43,53 @@ export class ChannelHub {
43
43
  return;
44
44
  }
45
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
+
46
93
  // Handle /status command
47
94
  if (message.trim() === '/status' || message.trim().toLowerCase() === 'status') {
48
95
  const uptime = process.uptime();
package/lib/engine.js CHANGED
@@ -93,6 +93,69 @@ 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
+
96
159
  // Handle /status command
97
160
  if (message.trim() === '/status' || message.trim().toLowerCase() === 'status') {
98
161
  const uptime = process.uptime();
@@ -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.1",
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": {