squidclaw 0.7.0 → 0.7.2

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.
@@ -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\'m safe! šŸ¦‘');
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\'m safe! You can resurrect me anytime with this file šŸ¦‘',
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,204 @@
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 contains an API key
116
+ export function detectApiKey(message) {
117
+ const trimmed = message.trim();
118
+
119
+ // Split by whitespace and newlines — check each token
120
+ const tokens = trimmed.split(/[\s\n]+/);
121
+
122
+ for (const token of tokens) {
123
+ const t = token.trim();
124
+ if (t.length < 10) continue;
125
+
126
+ // Anthropic
127
+ if (t.startsWith('sk-ant-')) {
128
+ return { provider: 'anthropic', key: t };
129
+ }
130
+ // OpenAI
131
+ if (t.startsWith('sk-') && !t.startsWith('sk-ant-') && t.length > 20) {
132
+ return { provider: 'openai', key: t };
133
+ }
134
+ // Groq
135
+ if (t.startsWith('gsk_') && t.length > 15) {
136
+ return { provider: 'groq', key: t };
137
+ }
138
+ // Google (AIza...)
139
+ if (t.startsWith('AIza') && t.length > 20) {
140
+ return { provider: 'google', key: t };
141
+ }
142
+ }
143
+
144
+ // Check full message for long unbroken tokens (generic key)
145
+ for (const token of tokens) {
146
+ if (token.length > 30 && !/\s/.test(token) && /^[A-Za-z0-9_\-]+$/.test(token)) {
147
+ return { provider: 'unknown', key: token };
148
+ }
149
+ }
150
+
151
+ return null;
152
+ }
153
+
154
+ // Save an API key to config
155
+ export function saveApiKey(provider, key) {
156
+ const config = loadConfig();
157
+ config.ai = config.ai || { providers: {} };
158
+ config.ai.providers = config.ai.providers || {};
159
+ config.ai.providers[provider] = config.ai.providers[provider] || {};
160
+ config.ai.providers[provider].key = key;
161
+ saveConfig(config);
162
+
163
+ logger.info('self-config', `API key saved for provider: ${provider}`);
164
+ return true;
165
+ }
166
+
167
+ // Get confirmation message after key is saved
168
+ export function getKeyConfirmation(provider) {
169
+ const providerNames = {
170
+ anthropic: 'Anthropic (Claude)',
171
+ openai: 'OpenAI',
172
+ google: 'Google (Gemini)',
173
+ groq: 'Groq',
174
+ together: 'Together AI',
175
+ cerebras: 'Cerebras',
176
+ mistral: 'Mistral',
177
+ };
178
+
179
+ const skills = {
180
+ anthropic: ['Chat (Claude)', 'Image Understanding', 'Code Analysis'],
181
+ openai: ['Chat (GPT-4o)', 'Image Generation (DALL-E)', 'Voice (Whisper + TTS)', 'Image Understanding'],
182
+ google: ['Chat (Gemini)', 'Image Generation (Imagen)', 'Embeddings'],
183
+ groq: ['Chat (Llama)', 'Voice Transcription (Whisper) — FREE'],
184
+ together: ['Chat (Llama, DeepSeek)'],
185
+ cerebras: ['Chat (Llama) — FREE'],
186
+ mistral: ['Chat (Mistral)'],
187
+ };
188
+
189
+ const name = providerNames[provider] || provider;
190
+ const newSkills = skills[provider] || ['Chat'];
191
+
192
+ const lines = [
193
+ `āœ… *${name} key added!*`,
194
+ '',
195
+ 'šŸ†• New skills unlocked:',
196
+ ...newSkills.map(s => ` • ${s}`),
197
+ '',
198
+ 'I am now more powerful! Try asking me to use these šŸ¦‘',
199
+ ];
200
+
201
+ return lines.join('\n');
202
+ }
203
+
204
+ export { SKILL_REQUIREMENTS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squidclaw",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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": {