squidclaw 0.7.0 ā 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/channels/hub.js +26 -1
- package/lib/engine.js +25 -1
- package/lib/features/self-config.js +197 -0
- package/package.json +1 -1
package/lib/channels/hub.js
CHANGED
|
@@ -43,6 +43,17 @@ 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
|
+
|
|
46
57
|
// Handle /help
|
|
47
58
|
if (message.trim() === '/help') {
|
|
48
59
|
const helpText = [
|
|
@@ -69,7 +80,7 @@ export class ChannelHub {
|
|
|
69
80
|
await AgentBackup.saveToFile(backup, home + '/backups/' + filename);
|
|
70
81
|
const size = (JSON.stringify(backup).length / 1024).toFixed(1);
|
|
71
82
|
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
|
|
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! š¦');
|
|
73
84
|
} catch (err) {
|
|
74
85
|
await this.whatsappManager.sendMessage(agentId, contactId, 'ā Backup failed: ' + err.message);
|
|
75
86
|
}
|
|
@@ -125,6 +136,20 @@ export class ChannelHub {
|
|
|
125
136
|
return;
|
|
126
137
|
}
|
|
127
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
|
+
|
|
128
153
|
// Process message through agent
|
|
129
154
|
const result = await agent.processMessage(contactId, message, metadata);
|
|
130
155
|
|
package/lib/engine.js
CHANGED
|
@@ -93,6 +93,17 @@ 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
|
+
|
|
96
107
|
// Handle /help command
|
|
97
108
|
if (message.trim() === '/help') {
|
|
98
109
|
const helpText = [
|
|
@@ -130,7 +141,7 @@ export class SquidclawEngine {
|
|
|
130
141
|
'š§ Memories: ' + (backup.memories?.length || 0),
|
|
131
142
|
'š Files: ' + Object.keys(backup.files).length,
|
|
132
143
|
'',
|
|
133
|
-
'I
|
|
144
|
+
'I am safe! You can resurrect me anytime with this file š¦',
|
|
134
145
|
];
|
|
135
146
|
await this.telegramManager.sendMessage(agentId, contactId, lines.join('\n'), metadata);
|
|
136
147
|
} catch (err) {
|
|
@@ -186,6 +197,19 @@ export class SquidclawEngine {
|
|
|
186
197
|
return;
|
|
187
198
|
}
|
|
188
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
|
+
|
|
189
213
|
const result = await agent.processMessage(contactId, message, metadata);
|
|
190
214
|
|
|
191
215
|
if (result.reaction && metadata.messageId) {
|
|
@@ -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 };
|