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 +28 -0
- package/lib/channels/hub.js +72 -0
- package/lib/engine.js +87 -0
- package/lib/features/backup.js +138 -0
- package/lib/features/self-config.js +197 -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
|
@@ -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 };
|