squidclaw 0.6.1 → 0.7.1

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,64 @@ export class ChannelHub {
43
43
  return;
44
44
  }
45
45
 
46
+ // Handle API key detection
47
+ const { detectApiKey, saveApiKey, getKeyConfirmation, detectSkillRequest, checkSkillAvailable, getKeyRequestMessage } = await import('../features/self-config.js');
48
+
49
+ const keyDetected = detectApiKey(message);
50
+ if (keyDetected && keyDetected.provider !== 'unknown') {
51
+ saveApiKey(keyDetected.provider, keyDetected.key);
52
+ const confirmation = getKeyConfirmation(keyDetected.provider);
53
+ await this.whatsappManager.sendMessage(agentId, contactId, confirmation);
54
+ return;
55
+ }
56
+
57
+ // Handle /help
58
+ if (message.trim() === '/help') {
59
+ const helpText = [
60
+ '🦑 *Commands*', '',
61
+ '/status — model, uptime, usage',
62
+ '/backup — save me to a backup file',
63
+ '/memories — what I remember',
64
+ '/help — this message',
65
+ '', 'Just chat normally! 🦑',
66
+ ];
67
+ await this.whatsappManager.sendMessage(agentId, contactId, helpText.join('\n'));
68
+ return;
69
+ }
70
+
71
+ // Handle /backup
72
+ if (message.trim() === '/backup' || (message.toLowerCase().includes('backup') && message.toLowerCase().includes('yourself'))) {
73
+ try {
74
+ const { AgentBackup } = await import('../features/backup.js');
75
+ const { mkdirSync } = await import('fs');
76
+ const home = (await import('../core/config.js')).getHome();
77
+ mkdirSync(home + '/backups', { recursive: true });
78
+ const backup = await AgentBackup.create(agentId, home, this.storage);
79
+ const filename = (agent.name || agentId) + '_backup_' + new Date().toISOString().slice(0,10) + '.json';
80
+ await AgentBackup.saveToFile(backup, home + '/backups/' + filename);
81
+ const size = (JSON.stringify(backup).length / 1024).toFixed(1);
82
+ await this.whatsappManager.sendMessage(agentId, contactId,
83
+ '💾 *Backup Complete!*\n\n📦 ' + filename + '\n📏 ' + size + ' KB\n💬 ' + (backup.conversations?.length || 0) + ' messages\n🧠 ' + (backup.memories?.length || 0) + ' memories\n\nI am safe! 🦑');
84
+ } catch (err) {
85
+ await this.whatsappManager.sendMessage(agentId, contactId, '❌ Backup failed: ' + err.message);
86
+ }
87
+ return;
88
+ }
89
+
90
+ // Handle /memories
91
+ if (message.trim() === '/memories') {
92
+ try {
93
+ const memories = await this.storage.getMemories(agentId);
94
+ if (memories.length === 0) {
95
+ await this.whatsappManager.sendMessage(agentId, contactId, '🧠 No memories yet!');
96
+ } else {
97
+ const memList = memories.slice(0, 20).map(m => '• ' + m.key + ': ' + m.value).join('\n');
98
+ await this.whatsappManager.sendMessage(agentId, contactId, '🧠 *What I Remember*\n\n' + memList);
99
+ }
100
+ } catch {}
101
+ return;
102
+ }
103
+
46
104
  // Handle /status command
47
105
  if (message.trim() === '/status' || message.trim().toLowerCase() === 'status') {
48
106
  const uptime = process.uptime();
@@ -78,6 +136,20 @@ export class ChannelHub {
78
136
  return;
79
137
  }
80
138
 
139
+ // Check if asking for unavailable skill
140
+ const skillRequest = detectSkillRequest(message);
141
+ if (skillRequest) {
142
+ const config = (await import('../core/config.js')).loadConfig();
143
+ const availability = checkSkillAvailable(skillRequest, config);
144
+ if (!availability.available) {
145
+ const keyMsg = getKeyRequestMessage(skillRequest);
146
+ if (keyMsg) {
147
+ await this.whatsappManager.sendMessage(agentId, contactId, keyMsg);
148
+ return;
149
+ }
150
+ }
151
+ }
152
+
81
153
  // Process message through agent
82
154
  const result = await agent.processMessage(contactId, message, metadata);
83
155
 
package/lib/engine.js CHANGED
@@ -93,6 +93,80 @@ export class SquidclawEngine {
93
93
  if (!allowed) return;
94
94
  }
95
95
 
96
+ // Handle API key detection — user pasting a key
97
+ const { detectApiKey, saveApiKey, getKeyConfirmation, detectSkillRequest, checkSkillAvailable, getKeyRequestMessage } = await import('./features/self-config.js');
98
+
99
+ const keyDetected = detectApiKey(message);
100
+ if (keyDetected && keyDetected.provider !== 'unknown') {
101
+ saveApiKey(keyDetected.provider, keyDetected.key);
102
+ const confirmation = getKeyConfirmation(keyDetected.provider);
103
+ await this.telegramManager.sendMessage(agentId, contactId, confirmation, metadata);
104
+ return;
105
+ }
106
+
107
+ // Handle /help command
108
+ if (message.trim() === '/help') {
109
+ const helpText = [
110
+ '🦑 *Commands*',
111
+ '',
112
+ '/status — model, uptime, usage stats',
113
+ '/backup — save me to a backup file',
114
+ '/memories — what I remember about you',
115
+ '/help — this message',
116
+ '',
117
+ 'Just chat normally — I\'ll search the web, remember things, and help! 🦑',
118
+ ];
119
+ await this.telegramManager.sendMessage(agentId, contactId, helpText.join('\n'), metadata);
120
+ return;
121
+ }
122
+
123
+ // Handle /backup command
124
+ if (message.trim() === '/backup' || message.trim().toLowerCase() === 'backup yourself' || message.trim().toLowerCase().includes('backup')) {
125
+ if (message.toLowerCase().includes('backup') && (message.toLowerCase().includes('yourself') || message.toLowerCase().includes('your') || message.trim() === '/backup')) {
126
+ try {
127
+ const { AgentBackup } = await import('./features/backup.js');
128
+ const backup = await AgentBackup.create(agentId, this.home, this.storage);
129
+ const filename = (agent.name || agentId) + '_backup_' + new Date().toISOString().slice(0,10) + '.json';
130
+ const path = this.home + '/backups/' + filename;
131
+ const { mkdirSync } = await import('fs');
132
+ mkdirSync(this.home + '/backups', { recursive: true });
133
+ await AgentBackup.saveToFile(backup, path);
134
+ const size = (JSON.stringify(backup).length / 1024).toFixed(1);
135
+ const lines = [
136
+ '💾 *Backup Complete!*',
137
+ '',
138
+ '📦 File: ' + filename,
139
+ '📏 Size: ' + size + ' KB',
140
+ '💬 Messages: ' + (backup.conversations?.length || 0),
141
+ '🧠 Memories: ' + (backup.memories?.length || 0),
142
+ '📄 Files: ' + Object.keys(backup.files).length,
143
+ '',
144
+ 'I am safe! You can resurrect me anytime with this file 🦑',
145
+ ];
146
+ await this.telegramManager.sendMessage(agentId, contactId, lines.join('\n'), metadata);
147
+ } catch (err) {
148
+ await this.telegramManager.sendMessage(agentId, contactId, '❌ Backup failed: ' + err.message, metadata);
149
+ }
150
+ return;
151
+ }
152
+ }
153
+
154
+ // Handle /memories command
155
+ if (message.trim() === '/memories' || message.trim() === '/remember') {
156
+ try {
157
+ const memories = await this.storage.getMemories(agentId);
158
+ if (memories.length === 0) {
159
+ await this.telegramManager.sendMessage(agentId, contactId, '🧠 No memories yet. Tell me things and I\'ll remember!', metadata);
160
+ } else {
161
+ const memList = memories.slice(0, 20).map(m => '• ' + m.key + ': ' + m.value).join('\n');
162
+ await this.telegramManager.sendMessage(agentId, contactId, '🧠 *What I Remember*\n\n' + memList, metadata);
163
+ }
164
+ } catch (err) {
165
+ await this.telegramManager.sendMessage(agentId, contactId, '🧠 Memory error: ' + err.message, metadata);
166
+ }
167
+ return;
168
+ }
169
+
96
170
  // Handle /status command
97
171
  if (message.trim() === '/status' || message.trim().toLowerCase() === 'status') {
98
172
  const uptime = process.uptime();
@@ -123,6 +197,19 @@ export class SquidclawEngine {
123
197
  return;
124
198
  }
125
199
 
200
+ // Check if user is asking for a skill we don't have
201
+ const skillRequest = detectSkillRequest(message);
202
+ if (skillRequest) {
203
+ const availability = checkSkillAvailable(skillRequest, this.config);
204
+ if (!availability.available) {
205
+ const keyMsg = getKeyRequestMessage(skillRequest);
206
+ if (keyMsg) {
207
+ await this.telegramManager.sendMessage(agentId, contactId, keyMsg, metadata);
208
+ return;
209
+ }
210
+ }
211
+ }
212
+
126
213
  const result = await agent.processMessage(contactId, message, metadata);
127
214
 
128
215
  if (result.reaction && metadata.messageId) {
@@ -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
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * 🦑 Self-Configuration
3
+ * Agent can request and add API keys through chat
4
+ */
5
+
6
+ import { loadConfig, saveConfig } from '../core/config.js';
7
+ import { logger } from '../core/logger.js';
8
+
9
+ // Skills that need specific API keys
10
+ const SKILL_REQUIREMENTS = {
11
+ 'image_generation': {
12
+ name: 'Image Generation',
13
+ providers: [
14
+ { id: 'openai', label: 'OpenAI (DALL-E)', keyUrl: 'platform.openai.com/api-keys', keyPrefix: 'sk-' },
15
+ { id: 'google', label: 'Google (Gemini/Imagen)', keyUrl: 'aistudio.google.dev/apikey', keyPrefix: 'AI' },
16
+ ],
17
+ },
18
+ 'vision': {
19
+ name: 'Image Understanding',
20
+ providers: [
21
+ { id: 'anthropic', label: 'Anthropic (Claude Vision)', keyUrl: 'console.anthropic.com/keys', keyPrefix: 'sk-ant-' },
22
+ { id: 'openai', label: 'OpenAI (GPT-4 Vision)', keyUrl: 'platform.openai.com/api-keys', keyPrefix: 'sk-' },
23
+ ],
24
+ },
25
+ 'voice_tts': {
26
+ name: 'Voice Notes (TTS)',
27
+ providers: [
28
+ { id: 'edge', label: 'Microsoft Edge TTS (FREE)', keyPrefix: null },
29
+ { id: 'openai', label: 'OpenAI TTS', keyUrl: 'platform.openai.com/api-keys', keyPrefix: 'sk-' },
30
+ ],
31
+ },
32
+ 'transcription': {
33
+ name: 'Voice Transcription',
34
+ providers: [
35
+ { id: 'groq', label: 'Groq Whisper (FREE)', keyUrl: 'console.groq.com/keys', keyPrefix: 'gsk_' },
36
+ { id: 'openai', label: 'OpenAI Whisper', keyUrl: 'platform.openai.com/api-keys', keyPrefix: 'sk-' },
37
+ ],
38
+ },
39
+ 'web_search': {
40
+ name: 'Web Search',
41
+ providers: [
42
+ { id: 'duckduckgo', label: 'DuckDuckGo (FREE)', keyPrefix: null },
43
+ ],
44
+ },
45
+ 'code_execution': {
46
+ name: 'Code Execution',
47
+ providers: [
48
+ { id: 'local', label: 'Local (sandboxed)', keyPrefix: null },
49
+ ],
50
+ },
51
+ };
52
+
53
+ // Detect what skill the user is asking for
54
+ export function detectSkillRequest(message) {
55
+ const lower = message.toLowerCase();
56
+
57
+ if (lower.match(/generat.*image|create.*image|draw|make.*picture|dall.?e|imagen|image.*generat/)) {
58
+ return 'image_generation';
59
+ }
60
+ if (lower.match(/analyz.*image|understand.*image|what.*this.*photo|vision|see.*image|look.*at/)) {
61
+ return 'vision';
62
+ }
63
+ if (lower.match(/voice.*note|speak|tts|text.*to.*speech|send.*voice/)) {
64
+ return 'voice_tts';
65
+ }
66
+ if (lower.match(/transcrib|speech.*to.*text|whisper/)) {
67
+ return 'transcription';
68
+ }
69
+ if (lower.match(/run.*code|execut.*code|python|javascript/)) {
70
+ return 'code_execution';
71
+ }
72
+ return null;
73
+ }
74
+
75
+ // Check if we have the key for a skill
76
+ export function checkSkillAvailable(skill, config) {
77
+ const req = SKILL_REQUIREMENTS[skill];
78
+ if (!req) return { available: true };
79
+
80
+ for (const prov of req.providers) {
81
+ if (!prov.keyPrefix) return { available: true, provider: prov }; // Free skill
82
+ const key = config.ai?.providers?.[prov.id]?.key;
83
+ if (key && key !== 'local' && key.length > 5) {
84
+ return { available: true, provider: prov };
85
+ }
86
+ }
87
+
88
+ return { available: false, skill: req, requirements: req.providers.filter(p => p.keyPrefix) };
89
+ }
90
+
91
+ // Generate "I need a key" message
92
+ export function getKeyRequestMessage(skill) {
93
+ const req = SKILL_REQUIREMENTS[skill];
94
+ if (!req) return null;
95
+
96
+ const providers = req.providers.filter(p => p.keyPrefix);
97
+ if (providers.length === 0) return null;
98
+
99
+ const lines = [
100
+ `🔑 I need an API key to ${req.name.toLowerCase()}!`,
101
+ '',
102
+ ];
103
+
104
+ for (const prov of providers) {
105
+ lines.push(`*${prov.label}*`);
106
+ lines.push(`Get yours at: ${prov.keyUrl}`);
107
+ lines.push('');
108
+ }
109
+
110
+ lines.push('Just paste the key here and I will add it myself! 🦑');
111
+
112
+ return lines.join('\n');
113
+ }
114
+
115
+ // Detect if a message is an API key
116
+ export function detectApiKey(message) {
117
+ const trimmed = message.trim();
118
+
119
+ // Anthropic
120
+ if (trimmed.startsWith('sk-ant-')) {
121
+ return { provider: 'anthropic', key: trimmed };
122
+ }
123
+ // OpenAI
124
+ if (trimmed.startsWith('sk-') && !trimmed.startsWith('sk-ant-') && trimmed.length > 20) {
125
+ return { provider: 'openai', key: trimmed };
126
+ }
127
+ // Groq
128
+ if (trimmed.startsWith('gsk_')) {
129
+ return { provider: 'groq', key: trimmed };
130
+ }
131
+ // Google
132
+ if (trimmed.startsWith('AI') && trimmed.length > 20 && !trimmed.includes(' ')) {
133
+ return { provider: 'google', key: trimmed };
134
+ }
135
+ // Together
136
+ if (trimmed.length > 40 && !trimmed.includes(' ') && /^[a-f0-9]+$/.test(trimmed)) {
137
+ return { provider: 'together', key: trimmed };
138
+ }
139
+ // Generic long token
140
+ if (trimmed.length > 30 && !trimmed.includes(' ') && !trimmed.includes('\n')) {
141
+ return { provider: 'unknown', key: trimmed };
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ // Save an API key to config
148
+ export function saveApiKey(provider, key) {
149
+ const config = loadConfig();
150
+ config.ai = config.ai || { providers: {} };
151
+ config.ai.providers = config.ai.providers || {};
152
+ config.ai.providers[provider] = config.ai.providers[provider] || {};
153
+ config.ai.providers[provider].key = key;
154
+ saveConfig(config);
155
+
156
+ logger.info('self-config', `API key saved for provider: ${provider}`);
157
+ return true;
158
+ }
159
+
160
+ // Get confirmation message after key is saved
161
+ export function getKeyConfirmation(provider) {
162
+ const providerNames = {
163
+ anthropic: 'Anthropic (Claude)',
164
+ openai: 'OpenAI',
165
+ google: 'Google (Gemini)',
166
+ groq: 'Groq',
167
+ together: 'Together AI',
168
+ cerebras: 'Cerebras',
169
+ mistral: 'Mistral',
170
+ };
171
+
172
+ const skills = {
173
+ anthropic: ['Chat (Claude)', 'Image Understanding', 'Code Analysis'],
174
+ openai: ['Chat (GPT-4o)', 'Image Generation (DALL-E)', 'Voice (Whisper + TTS)', 'Image Understanding'],
175
+ google: ['Chat (Gemini)', 'Image Generation (Imagen)', 'Embeddings'],
176
+ groq: ['Chat (Llama)', 'Voice Transcription (Whisper) — FREE'],
177
+ together: ['Chat (Llama, DeepSeek)'],
178
+ cerebras: ['Chat (Llama) — FREE'],
179
+ mistral: ['Chat (Mistral)'],
180
+ };
181
+
182
+ const name = providerNames[provider] || provider;
183
+ const newSkills = skills[provider] || ['Chat'];
184
+
185
+ const lines = [
186
+ `✅ *${name} key added!*`,
187
+ '',
188
+ '🆕 New skills unlocked:',
189
+ ...newSkills.map(s => ` • ${s}`),
190
+ '',
191
+ 'I am now more powerful! Try asking me to use these 🦑',
192
+ ];
193
+
194
+ return lines.join('\n');
195
+ }
196
+
197
+ export { SKILL_REQUIREMENTS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squidclaw",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
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": {