squidclaw 0.5.1 → 0.5.3

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.
@@ -22,6 +22,20 @@ export class ChannelHub {
22
22
  return;
23
23
  }
24
24
 
25
+ // Allowlist check
26
+ const allowFrom = agent.allowFrom || ['*'];
27
+ if (!allowFrom.includes('*')) {
28
+ const senderNum = contactId.replace('@s.whatsapp.net', '').replace(/[^0-9]/g, '');
29
+ const allowed = allowFrom.some(n => {
30
+ const clean = n.replace(/[^0-9]/g, '');
31
+ return senderNum.endsWith(clean) || clean.endsWith(senderNum);
32
+ });
33
+ if (!allowed) {
34
+ logger.info('hub', 'Message from ' + contactId + ' blocked (not in allowlist)');
35
+ return;
36
+ }
37
+ }
38
+
25
39
  // Skip group messages unless mentioned (for now)
26
40
  if (metadata.isGroup) {
27
41
  // TODO: implement group mention detection
@@ -5,7 +5,7 @@
5
5
 
6
6
  import chalk from 'chalk';
7
7
  import { createInterface } from 'readline';
8
- import { loadConfig, getHome } from '../core/config.js';
8
+ import { loadConfig, getHome, MODEL_MAP } from '../core/config.js';
9
9
  import { writeFileSync, readFileSync, existsSync } from 'fs';
10
10
  import { join } from 'path';
11
11
 
@@ -18,7 +18,7 @@ export async function hatchTUI(agentId) {
18
18
  const manifestPath = join(agentDir, 'agent.json');
19
19
 
20
20
  if (!existsSync(manifestPath)) {
21
- console.log(chalk.red('Agent not found: ' + agentId));
21
+ console.log(chalk.red(' Agent not found: ' + agentId));
22
22
  return;
23
23
  }
24
24
 
@@ -39,10 +39,9 @@ export async function hatchTUI(agentId) {
39
39
  console.log();
40
40
 
41
41
  // ═══ Start engine silently ═══
42
- let engine = null;
43
42
  const port = config.engine?.port || 9500;
43
+ let engine = null;
44
44
 
45
- // Check if engine already running
46
45
  let engineReady = false;
47
46
  try {
48
47
  const res = await fetch('http://127.0.0.1:' + port + '/health');
@@ -54,7 +53,6 @@ export async function hatchTUI(agentId) {
54
53
  try {
55
54
  const { SquidclawEngine } = await import('../engine.js');
56
55
  engine = new SquidclawEngine({ port });
57
- // Suppress output
58
56
  const origLog = console.log;
59
57
  const origErr = console.error;
60
58
  console.log = () => {};
@@ -64,7 +62,8 @@ export async function hatchTUI(agentId) {
64
62
  console.error = origErr;
65
63
  process.stdout.write(chalk.green(' ready!\n\n'));
66
64
  } catch (err) {
67
- console.log(chalk.red(' failed: ' + err.message));
65
+ process.stdout.write(chalk.red(' failed\n'));
66
+ console.log(chalk.red(' ' + err.message));
68
67
  console.log(chalk.gray(' Run "squidclaw start" first, then "squidclaw wake"'));
69
68
  return;
70
69
  }
@@ -72,10 +71,9 @@ export async function hatchTUI(agentId) {
72
71
 
73
72
  await DELAY(500);
74
73
 
75
- // ═══ First message from agent ═══
76
- const firstMessage = await callAgent(port, agentId,
77
- 'You just came to life for the very first time. You are "' + manifest.name + '". Express excitement about existing — be curious, a little confused. Ask: who are you? who am I? Keep it to 2-3 SHORT sentences. Be playful. No ---SPLIT--- markers.',
78
- true
74
+ // ═══ Agent's first message call AI directly ═══
75
+ const firstMessage = await callAgentDirect(config, manifest,
76
+ 'You just came to life. You are ' + manifest.name + '. Express excitement — be curious, playful. Ask who you are talking to. 2-3 SHORT sentences only.'
79
77
  );
80
78
 
81
79
  await typeMessage(manifest.name, firstMessage);
@@ -84,38 +82,36 @@ export async function hatchTUI(agentId) {
84
82
  const rl = createInterface({ input: process.stdin, output: process.stdout });
85
83
  let turnCount = 0;
86
84
  let agentFinalName = manifest.name;
85
+ const chatHistory = [
86
+ { role: 'assistant', content: firstMessage }
87
+ ];
87
88
 
88
- const askUser = () => new Promise(resolve => {
89
- rl.question(chalk.green('\n You: '), (answer) => resolve(answer));
90
- });
91
-
92
- console.log(chalk.gray('\n (Type /done when you\'re ready to finish hatching)\n'));
89
+ console.log(chalk.gray('\n (Type /done when ready to finish hatching)\n'));
93
90
 
94
91
  while (true) {
95
- const userInput = await askUser();
92
+ const userInput = await new Promise(resolve => {
93
+ rl.question(chalk.green('\n You: '), resolve);
94
+ });
96
95
  if (!userInput || !userInput.trim()) continue;
97
96
  if (userInput.trim().toLowerCase() === '/done' || userInput.trim().toLowerCase() === '/exit') break;
98
97
 
99
98
  turnCount++;
99
+ chatHistory.push({ role: 'user', content: userInput });
100
100
 
101
101
  let hint = '';
102
- if (turnCount <= 2) {
103
- hint = 'You are still figuring out who you are. Ask about your personality, your name, what you do. Be curious and playful.';
104
- } else if (turnCount <= 4) {
105
- hint = 'You are forming your identity now. Confirm what you have learned. Show your personality emerging.';
106
- } else {
107
- hint = 'You know who you are. Act like it. If conversation feels complete, suggest typing /done.';
108
- }
102
+ if (turnCount <= 2) hint = 'You are discovering who you are. Ask about your name, personality, purpose. Be curious.';
103
+ else if (turnCount <= 4) hint = 'Your identity is forming. Show your personality. Confirm what you learned.';
104
+ else hint = 'You know who you are now. Act like it. If the conversation feels complete, suggest /done.';
109
105
 
110
- const response = await callAgent(port, agentId, userInput, false, hint);
106
+ const response = await callAgentDirect(config, manifest, userInput, chatHistory, hint);
107
+ chatHistory.push({ role: 'assistant', content: response });
111
108
  await typeMessage(agentFinalName, response);
112
109
 
113
- // Check if user renamed agent
114
- const nameMatch = userInput.match(/call you (\w+)/i) || userInput.match(/your name.+?(\w+)$/i) || userInput.match(/name you (\w+)/i);
110
+ const nameMatch = userInput.match(/call you (\w+)/i) || userInput.match(/name you (\w+)/i);
115
111
  if (nameMatch) agentFinalName = nameMatch[1];
116
112
  }
117
113
 
118
- // ═══ Save identity ═══
114
+ // ═══ Save ═══
119
115
  console.log();
120
116
  console.log(chalk.cyan(' ════════════════════════════════════'));
121
117
  console.log(chalk.cyan(' 🦑 ' + agentFinalName + ' is hatched!'));
@@ -127,7 +123,6 @@ export async function hatchTUI(agentId) {
127
123
  manifest.hatchedAt = new Date().toISOString();
128
124
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
129
125
 
130
- // Update SOUL.md
131
126
  const soulPath = join(agentDir, 'SOUL.md');
132
127
  if (existsSync(soulPath)) {
133
128
  let soul = readFileSync(soulPath, 'utf8');
@@ -137,36 +132,117 @@ export async function hatchTUI(agentId) {
137
132
 
138
133
  writeFileSync(join(agentDir, 'IDENTITY.md'), '# ' + agentFinalName + '\n- Name: ' + agentFinalName + '\n- Hatched: ' + new Date().toISOString() + '\n- Emoji: 🦑\n');
139
134
 
140
- console.log(chalk.green(' ✅ ' + agentFinalName + ' is ready!\n'));
141
- console.log(' ' + chalk.bold('Commands:\n'));
142
- console.log(' ' + chalk.cyan('squidclaw start') + ' — run the engine');
135
+ console.log(chalk.green(' ✅ ' + agentFinalName + ' is alive!\n'));
136
+ console.log(' ' + chalk.bold('Now run:\n'));
137
+ console.log(' ' + chalk.cyan('squidclaw start') + ' — launch the engine');
143
138
  console.log(' ' + chalk.cyan('squidclaw tui') + ' — chat in terminal');
144
- console.log(' ' + chalk.cyan('squidclaw status') + ' — check status');
145
139
  console.log();
146
140
 
147
141
  rl.close();
148
- if (engine) {
149
- try { await engine.stop(); } catch {}
150
- }
142
+ if (engine) { try { await engine.stop(); } catch {} }
151
143
  process.exit(0);
152
144
  }
153
145
 
154
- async function callAgent(port, agentId, message, isFirstMessage, hint) {
146
+ /**
147
+ * Call AI directly (bypass engine API) for the hatch conversation
148
+ */
149
+ async function callAgentDirect(config, manifest, message, history, hint) {
150
+ const provider = config.ai?.defaultProvider || 'anthropic';
151
+ const rawModel = manifest.model || config.ai?.defaultModel || 'claude-sonnet-4';
152
+ const model = MODEL_MAP[rawModel] || rawModel;
153
+ const providerConfig = config.ai?.providers?.[provider] || {};
154
+ const apiKey = providerConfig.key;
155
+
156
+ if (!apiKey || apiKey === 'local') {
157
+ return "I can't think yet — no API key configured. Check your config!";
158
+ }
159
+
160
+ const systemPrompt = 'You are ' + manifest.name + ', an AI agent that just hatched (came to life). This is your first conversation ever. Be playful, curious, excited. Keep responses SHORT (2-3 sentences max). No ---SPLIT--- markers. No "As an AI" phrases.' + (hint ? '\n\n' + hint : '');
161
+
162
+ const messages = [
163
+ { role: 'user', content: message }
164
+ ];
165
+
166
+ // Add history if available
167
+ if (history && history.length > 0) {
168
+ messages.length = 0;
169
+ for (const h of history.slice(-10)) {
170
+ messages.push({ role: h.role, content: h.content });
171
+ }
172
+ messages.push({ role: 'user', content: message });
173
+ }
174
+
155
175
  try {
156
- const body = { message, contactId: 'hatch-session' };
157
- if (hint) body.systemHint = hint;
158
- if (isFirstMessage) body.systemOverride = message;
159
-
160
- const res = await fetch('http://127.0.0.1:' + port + '/api/agents/' + agentId + '/chat', {
161
- method: 'POST',
162
- headers: { 'content-type': 'application/json' },
163
- body: JSON.stringify(body),
164
- });
176
+ // Determine API endpoint
177
+ let url, headers, body;
178
+
179
+ if (provider === 'anthropic') {
180
+ url = 'https://api.anthropic.com/v1/messages';
181
+ headers = {
182
+ 'content-type': 'application/json',
183
+ 'x-api-key': apiKey,
184
+ 'anthropic-version': '2023-06-01',
185
+ };
186
+ // OAuth token support
187
+ if (apiKey.includes('sk-ant-oat')) {
188
+ headers['authorization'] = 'Bearer ' + apiKey;
189
+ headers['anthropic-beta'] = 'claude-code-20250219,oauth-2025-04-20';
190
+ delete headers['x-api-key'];
191
+ }
192
+ body = JSON.stringify({
193
+ model: model,
194
+ max_tokens: 300,
195
+ system: systemPrompt,
196
+ messages: messages,
197
+ });
198
+ } else if (provider === 'openai' || provider === 'groq' || provider === 'together' || provider === 'cerebras' || provider === 'mistral') {
199
+ const urls = {
200
+ openai: 'https://api.openai.com/v1/chat/completions',
201
+ groq: 'https://api.groq.com/openai/v1/chat/completions',
202
+ together: 'https://api.together.xyz/v1/chat/completions',
203
+ cerebras: 'https://api.cerebras.ai/v1/chat/completions',
204
+ mistral: 'https://api.mistral.ai/v1/chat/completions',
205
+ };
206
+ url = urls[provider];
207
+ headers = {
208
+ 'content-type': 'application/json',
209
+ 'authorization': 'Bearer ' + apiKey,
210
+ };
211
+ body = JSON.stringify({
212
+ model: model.replace(provider + '/', ''),
213
+ max_tokens: 300,
214
+ messages: [{ role: 'system', content: systemPrompt }, ...messages],
215
+ });
216
+ } else if (provider === 'google') {
217
+ const cleanModel = model.replace('google/', '');
218
+ url = 'https://generativelanguage.googleapis.com/v1beta/models/' + cleanModel + ':generateContent?key=' + apiKey;
219
+ headers = { 'content-type': 'application/json' };
220
+ body = JSON.stringify({
221
+ systemInstruction: { parts: [{ text: systemPrompt }] },
222
+ contents: messages.map(m => ({ role: m.role === 'assistant' ? 'model' : 'user', parts: [{ text: m.content }] })),
223
+ });
224
+ } else {
225
+ return "Provider " + provider + " isn't supported in hatch mode yet. Try starting the engine first.";
226
+ }
227
+
228
+ const res = await fetch(url, { method: 'POST', headers, body });
165
229
  const data = await res.json();
166
- const msgs = data.messages || [];
167
- return msgs.join(' ') || '...';
230
+
231
+ if (!res.ok) {
232
+ const errMsg = data?.error?.message || data?.message || JSON.stringify(data).slice(0, 200);
233
+ return '(AI error: ' + errMsg + ')';
234
+ }
235
+
236
+ // Extract response based on provider
237
+ if (provider === 'anthropic') {
238
+ return data.content?.[0]?.text || '...';
239
+ } else if (provider === 'google') {
240
+ return data.candidates?.[0]?.content?.parts?.[0]?.text || '...';
241
+ } else {
242
+ return data.choices?.[0]?.message?.content || '...';
243
+ }
168
244
  } catch (err) {
169
- return '(oops, something went wrong: ' + err.message + ')';
245
+ return '(connection error: ' + err.message + ')';
170
246
  }
171
247
  }
172
248
 
package/lib/cli/setup.js CHANGED
@@ -176,9 +176,10 @@ export async function setup() {
176
176
  if (p.isCancel(connectWA)) return p.cancel('Setup cancelled');
177
177
 
178
178
  let waConnected = false;
179
+ let waPhone = null;
179
180
 
180
181
  if (connectWA === 'pair') {
181
- const waPhone = await p.text({
182
+ waPhone = await p.text({
182
183
  message: 'WhatsApp phone number (with country code):',
183
184
  placeholder: '+966 5XX XXX XXXX',
184
185
  validate: (v) => v.replace(/[^0-9+]/g, '').length < 8 ? 'Enter a valid phone number' : undefined,
@@ -186,6 +187,7 @@ export async function setup() {
186
187
  if (p.isCancel(waPhone)) return p.cancel('Setup cancelled');
187
188
 
188
189
  const cleanPhone = waPhone.replace(/[^0-9]/g, '');
190
+ waPhone = cleanPhone;
189
191
  manifest.whatsappNumber = cleanPhone;
190
192
 
191
193
  // Actually connect WhatsApp NOW
@@ -215,6 +217,50 @@ export async function setup() {
215
217
  config.channels.whatsapp.enabled = true;
216
218
  }
217
219
 
220
+ // Ask for allowlist (who can chat with this agent?)
221
+ if (connectWA !== 'skip') {
222
+ const allowMode = await p.select({
223
+ message: 'Who can message this agent on WhatsApp?',
224
+ options: [
225
+ { value: 'owner', label: '🔒 Only me', hint: 'recommended for testing' },
226
+ { value: 'list', label: '📋 Specific numbers (allowlist)' },
227
+ { value: 'all', label: '🌍 Everyone', hint: 'public agent' },
228
+ ],
229
+ });
230
+ if (p.isCancel(allowMode)) return p.cancel('Setup cancelled');
231
+
232
+ if (allowMode === 'owner' && waPhone) {
233
+ config.channels.whatsapp.allowFrom = [waPhone];
234
+ manifest.allowFrom = [waPhone];
235
+ console.log(chalk.gray(' Only ' + waPhone + ' can chat with ' + agentName));
236
+ } else if (allowMode === 'owner' && !waPhone) {
237
+ const ownerPhone = await p.text({
238
+ message: 'Your phone number (with country code):',
239
+ placeholder: '+966 5XX XXX XXXX',
240
+ validate: (v) => v.replace(/[^0-9+]/g, '').length < 8 ? 'Enter a valid number' : undefined,
241
+ });
242
+ if (!p.isCancel(ownerPhone)) {
243
+ const clean = ownerPhone.replace(/[^0-9]/g, '');
244
+ config.channels.whatsapp.allowFrom = [clean];
245
+ manifest.allowFrom = [clean];
246
+ }
247
+ } else if (allowMode === 'list') {
248
+ const numbers = await p.text({
249
+ message: 'Allowed numbers (comma separated):',
250
+ placeholder: '+966501234567, +966509876543',
251
+ validate: (v) => v.length < 5 ? 'Enter at least one number' : undefined,
252
+ });
253
+ if (!p.isCancel(numbers)) {
254
+ const list = numbers.split(',').map(n => n.replace(/[^0-9]/g, '').trim()).filter(Boolean);
255
+ config.channels.whatsapp.allowFrom = list;
256
+ manifest.allowFrom = list;
257
+ }
258
+ } else {
259
+ config.channels.whatsapp.allowFrom = ['*'];
260
+ manifest.allowFrom = ['*'];
261
+ }
262
+ }
263
+
218
264
  // ═══ Step 4: Telegram ═══
219
265
  p.note('Step 4 of 4: Telegram', '✈️');
220
266
 
@@ -3,7 +3,7 @@
3
3
  * Shows QR code or pairing code right in the terminal
4
4
  */
5
5
 
6
- import { default as makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } from 'baileys';
6
+ import { makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } from '@whiskeysockets/baileys';
7
7
  import qrcode from 'qrcode-terminal';
8
8
  import chalk from 'chalk';
9
9
  import { mkdirSync } from 'fs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squidclaw",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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": {