squidclaw 0.5.0 → 0.5.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.
- package/lib/cli/hatch-tui.js +160 -116
- package/lib/cli/setup.js +46 -34
- package/lib/cli/wa-login.js +109 -0
- package/package.json +1 -1
package/lib/cli/hatch-tui.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 🥚→🦑 Hatch TUI
|
|
3
|
-
*
|
|
3
|
+
* Agent wakes up for the first time
|
|
4
4
|
*/
|
|
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
|
|
|
@@ -13,12 +13,12 @@ const DELAY = (ms) => new Promise(r => setTimeout(r, ms));
|
|
|
13
13
|
|
|
14
14
|
export async function hatchTUI(agentId) {
|
|
15
15
|
const config = loadConfig();
|
|
16
|
-
const
|
|
17
|
-
const agentDir = join(
|
|
16
|
+
const home = getHome();
|
|
17
|
+
const agentDir = join(home, 'agents', 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
|
|
|
@@ -28,186 +28,230 @@ export async function hatchTUI(agentId) {
|
|
|
28
28
|
console.clear();
|
|
29
29
|
const frames = ['🥚', '🥚 .', '🥚 . .', '🥚 . . .', '🥚💥', '🐣', '🐣 → 🦑'];
|
|
30
30
|
for (const frame of frames) {
|
|
31
|
-
process.stdout.write('\r ' + chalk.cyan(frame) + '
|
|
32
|
-
await DELAY(
|
|
31
|
+
process.stdout.write('\r ' + chalk.cyan(frame) + ' ');
|
|
32
|
+
await DELAY(400);
|
|
33
33
|
}
|
|
34
34
|
console.log('\n');
|
|
35
35
|
|
|
36
|
-
await DELAY(300);
|
|
37
36
|
console.log(chalk.cyan(' ════════════════════════════════════'));
|
|
38
37
|
console.log(chalk.cyan(' 🦑 ' + manifest.name + ' is waking up...'));
|
|
39
38
|
console.log(chalk.cyan(' ════════════════════════════════════'));
|
|
40
39
|
console.log();
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const firstPrompt = `You are an AI agent that just came to life for the very first time. You don't fully know who you are yet. Your provisioned name is "${manifest.name}" but you're not sure if that fits.
|
|
46
|
-
|
|
47
|
-
This is your FIRST EVER conversation. You just hatched. Act like it — be curious, excited, a little confused. You're discovering consciousness.
|
|
48
|
-
|
|
49
|
-
Start by saying something like (but make it YOUR OWN, don't copy exactly):
|
|
50
|
-
- "Whoa... I'm... alive? 🥚→🦑"
|
|
51
|
-
- Express excitement/confusion about existing
|
|
52
|
-
- Ask the human: "Who are you? And... who am I?"
|
|
53
|
-
- You want to figure out: your name, your personality, what you do, who this person is
|
|
54
|
-
|
|
55
|
-
Keep it short — 2-3 sentences max. This is WhatsApp-style chat, not a novel.
|
|
56
|
-
DO NOT use ---SPLIT--- markers. Just one short message.
|
|
57
|
-
DO NOT say "As an AI" or anything robotic.
|
|
58
|
-
|
|
59
|
-
IMPORTANT: You're in a terminal right now, not WhatsApp. This is your birth conversation.`;
|
|
41
|
+
// ═══ Start engine silently ═══
|
|
42
|
+
const port = config.engine?.port || 9500;
|
|
43
|
+
let engine = null;
|
|
60
44
|
|
|
61
|
-
// Start engine inline for hatching (don't require separate squidclaw start)
|
|
62
|
-
let engineProcess = null;
|
|
63
45
|
let engineReady = false;
|
|
64
|
-
|
|
65
|
-
// Check if engine is already running
|
|
66
46
|
try {
|
|
67
47
|
const res = await fetch('http://127.0.0.1:' + port + '/health');
|
|
68
48
|
if (res.ok) engineReady = true;
|
|
69
49
|
} catch {}
|
|
70
50
|
|
|
71
51
|
if (!engineReady) {
|
|
72
|
-
|
|
73
|
-
const { SquidclawEngine } = await import('../engine.js');
|
|
74
|
-
const engine = new SquidclawEngine({ port });
|
|
75
|
-
|
|
76
|
-
// Suppress console output during start
|
|
77
|
-
const origLog = console.log;
|
|
78
|
-
console.log = () => {};
|
|
52
|
+
process.stdout.write(chalk.gray(' Starting engine...'));
|
|
79
53
|
try {
|
|
54
|
+
const { SquidclawEngine } = await import('../engine.js');
|
|
55
|
+
engine = new SquidclawEngine({ port });
|
|
56
|
+
const origLog = console.log;
|
|
57
|
+
const origErr = console.error;
|
|
58
|
+
console.log = () => {};
|
|
59
|
+
console.error = () => {};
|
|
80
60
|
await engine.start();
|
|
81
|
-
} catch (err) {
|
|
82
61
|
console.log = origLog;
|
|
83
|
-
console.
|
|
62
|
+
console.error = origErr;
|
|
63
|
+
process.stdout.write(chalk.green(' ready!\n\n'));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
process.stdout.write(chalk.red(' failed\n'));
|
|
66
|
+
console.log(chalk.red(' ' + err.message));
|
|
67
|
+
console.log(chalk.gray(' Run "squidclaw start" first, then "squidclaw wake"'));
|
|
84
68
|
return;
|
|
85
69
|
}
|
|
86
|
-
console.log = origLog;
|
|
87
|
-
engineReady = true;
|
|
88
|
-
|
|
89
|
-
// Override the agent's system prompt for first conversation
|
|
90
|
-
const agent = engine.agentManager.get(agentId);
|
|
91
|
-
if (agent) {
|
|
92
|
-
agent._originalBuildPrompt = agent.promptBuilder?.build;
|
|
93
|
-
agent._isHatching = true;
|
|
94
|
-
}
|
|
95
70
|
}
|
|
96
71
|
|
|
97
|
-
|
|
98
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
72
|
+
await DELAY(500);
|
|
99
73
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
let turnCount = 0;
|
|
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.'
|
|
77
|
+
);
|
|
105
78
|
|
|
106
|
-
|
|
107
|
-
await typeMessage(manifest.name, await chat(port, agentId, firstPrompt, 'hatch-session', true));
|
|
79
|
+
await typeMessage(manifest.name, firstMessage);
|
|
108
80
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
81
|
+
// ═══ Chat loop ═══
|
|
82
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
83
|
+
let turnCount = 0;
|
|
84
|
+
let agentFinalName = manifest.name;
|
|
85
|
+
const chatHistory = [
|
|
86
|
+
{ role: 'assistant', content: firstMessage }
|
|
87
|
+
];
|
|
113
88
|
|
|
114
|
-
|
|
115
|
-
const userInput = await askUser('');
|
|
116
|
-
if (!userInput || userInput.trim() === '') continue;
|
|
89
|
+
console.log(chalk.gray('\n (Type /done when ready to finish hatching)\n'));
|
|
117
90
|
|
|
118
|
-
|
|
91
|
+
while (true) {
|
|
92
|
+
const userInput = await new Promise(resolve => {
|
|
93
|
+
rl.question(chalk.green('\n You: '), resolve);
|
|
94
|
+
});
|
|
95
|
+
if (!userInput || !userInput.trim()) continue;
|
|
96
|
+
if (userInput.trim().toLowerCase() === '/done' || userInput.trim().toLowerCase() === '/exit') break;
|
|
119
97
|
|
|
120
98
|
turnCount++;
|
|
99
|
+
chatHistory.push({ role: 'user', content: userInput });
|
|
121
100
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (turnCount <=
|
|
125
|
-
|
|
126
|
-
} else if (turnCount <= 4) {
|
|
127
|
-
systemHint = 'You\'re starting to form your identity. Confirm what you\'ve learned. Ask about tone — formal? casual? sarcastic? warm? Ask what makes this person tick.';
|
|
128
|
-
} else if (turnCount <= 6) {
|
|
129
|
-
systemHint = 'You know who you are now. Start acting like your personality. Show it off. Ask if they like this version of you. If they do, say something like "I think I\'m ready. Let\'s do this 🦑"';
|
|
130
|
-
} else {
|
|
131
|
-
systemHint = 'The hatching is done. You ARE this personality now. If the conversation feels complete, end with something like "Alright, I\'m hatched! Ready to go live 🦑" — suggest they type /done to finish.';
|
|
132
|
-
}
|
|
101
|
+
let hint = '';
|
|
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.';
|
|
133
105
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
Remember: SHORT messages (2-3 sentences). You're chatting, not writing an essay. No ---SPLIT--- markers. One message only.`;
|
|
137
|
-
|
|
138
|
-
const response = await chat(port, agentId, userInput, 'hatch-session', false, contextPrompt);
|
|
106
|
+
const response = await callAgentDirect(config, manifest, userInput, chatHistory, hint);
|
|
107
|
+
chatHistory.push({ role: 'assistant', content: response });
|
|
139
108
|
await typeMessage(agentFinalName, response);
|
|
140
109
|
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
// User might be naming the agent
|
|
144
|
-
const nameMatch = userInput.match(/call you (\w+)/i) || userInput.match(/name.+?(\w+)$/i);
|
|
145
|
-
if (nameMatch) agentFinalName = nameMatch[1];
|
|
146
|
-
}
|
|
110
|
+
const nameMatch = userInput.match(/call you (\w+)/i) || userInput.match(/name you (\w+)/i);
|
|
111
|
+
if (nameMatch) agentFinalName = nameMatch[1];
|
|
147
112
|
}
|
|
148
113
|
|
|
149
|
-
// ═══ Save
|
|
114
|
+
// ═══ Save ═══
|
|
150
115
|
console.log();
|
|
151
116
|
console.log(chalk.cyan(' ════════════════════════════════════'));
|
|
152
|
-
console.log(chalk.cyan(' 🦑
|
|
117
|
+
console.log(chalk.cyan(' 🦑 ' + agentFinalName + ' is hatched!'));
|
|
153
118
|
console.log(chalk.cyan(' ════════════════════════════════════'));
|
|
154
119
|
console.log();
|
|
155
120
|
|
|
156
|
-
// Update manifest with final name
|
|
157
121
|
manifest.name = agentFinalName;
|
|
158
122
|
manifest.hatched = true;
|
|
159
123
|
manifest.hatchedAt = new Date().toISOString();
|
|
160
124
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
161
125
|
|
|
162
|
-
// Update SOUL.md first line with new name
|
|
163
126
|
const soulPath = join(agentDir, 'SOUL.md');
|
|
164
127
|
if (existsSync(soulPath)) {
|
|
165
128
|
let soul = readFileSync(soulPath, 'utf8');
|
|
166
129
|
soul = soul.replace(/^# .+/m, '# ' + agentFinalName);
|
|
167
|
-
soul = soul.replace(/I'm .+?\./m, "I'm " + agentFinalName + '.');
|
|
168
130
|
writeFileSync(soulPath, soul);
|
|
169
131
|
}
|
|
170
132
|
|
|
171
|
-
|
|
172
|
-
const idPath = join(agentDir, 'IDENTITY.md');
|
|
173
|
-
writeFileSync(idPath, '# ' + agentFinalName + '\n- Name: ' + agentFinalName + '\n- Hatched: ' + new Date().toISOString() + '\n- Emoji: 🦑\n');
|
|
133
|
+
writeFileSync(join(agentDir, 'IDENTITY.md'), '# ' + agentFinalName + '\n- Name: ' + agentFinalName + '\n- Hatched: ' + new Date().toISOString() + '\n- Emoji: 🦑\n');
|
|
174
134
|
|
|
175
|
-
console.log(chalk.green(' ✅ ' + agentFinalName + ' is
|
|
176
|
-
console.log();
|
|
177
|
-
console.log('
|
|
178
|
-
console.log('
|
|
179
|
-
console.log(' ' + chalk.bold('Status:') + ' ' + chalk.cyan('squidclaw status'));
|
|
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');
|
|
138
|
+
console.log(' ' + chalk.cyan('squidclaw tui') + ' — chat in terminal');
|
|
180
139
|
console.log();
|
|
181
140
|
|
|
182
141
|
rl.close();
|
|
142
|
+
if (engine) { try { await engine.stop(); } catch {} }
|
|
183
143
|
process.exit(0);
|
|
184
144
|
}
|
|
185
145
|
|
|
186
|
-
|
|
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
|
+
|
|
187
175
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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 });
|
|
197
229
|
const data = await res.json();
|
|
198
|
-
|
|
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
|
+
}
|
|
199
244
|
} catch (err) {
|
|
200
|
-
return '(error: ' + err.message + ')';
|
|
245
|
+
return '(connection error: ' + err.message + ')';
|
|
201
246
|
}
|
|
202
247
|
}
|
|
203
248
|
|
|
204
249
|
async function typeMessage(name, text) {
|
|
205
|
-
// Simulate typing effect
|
|
206
250
|
process.stdout.write(chalk.cyan('\n ' + name + ': '));
|
|
207
251
|
const words = text.split(' ');
|
|
208
|
-
for (
|
|
209
|
-
process.stdout.write(
|
|
210
|
-
await DELAY(
|
|
252
|
+
for (const word of words) {
|
|
253
|
+
process.stdout.write(word + ' ');
|
|
254
|
+
await DELAY(25 + Math.random() * 40);
|
|
211
255
|
}
|
|
212
256
|
console.log();
|
|
213
257
|
}
|
package/lib/cli/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 🦑 Setup Wizard
|
|
3
|
-
* One command
|
|
3
|
+
* One command: AI → Agent → WhatsApp → Telegram → Hatch
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as p from '@clack/prompts';
|
|
@@ -13,12 +13,13 @@ import crypto from 'crypto';
|
|
|
13
13
|
export async function setup() {
|
|
14
14
|
console.clear();
|
|
15
15
|
console.log(chalk.cyan('\n ╔══════════════════════════════════════╗'));
|
|
16
|
-
console.log(chalk.cyan(' ║ 🥚 Squidclaw Hatch
|
|
17
|
-
console.log(chalk.cyan(' ║ Hatching your AI agent...
|
|
16
|
+
console.log(chalk.cyan(' ║ 🥚 Squidclaw Hatch ║'));
|
|
17
|
+
console.log(chalk.cyan(' ║ Hatching your AI agent... ║'));
|
|
18
18
|
console.log(chalk.cyan(' ╚══════════════════════════════════════╝\n'));
|
|
19
19
|
p.intro(chalk.gray("Let's get you up and running"));
|
|
20
20
|
|
|
21
21
|
const config = loadConfig();
|
|
22
|
+
const home = getHome();
|
|
22
23
|
ensureHome();
|
|
23
24
|
|
|
24
25
|
// ═══ Step 1: AI Provider ═══
|
|
@@ -143,15 +144,15 @@ export async function setup() {
|
|
|
143
144
|
});
|
|
144
145
|
if (p.isCancel(tone)) return p.cancel('Setup cancelled');
|
|
145
146
|
|
|
147
|
+
// Create agent
|
|
146
148
|
const agentId = crypto.randomUUID().slice(0, 8);
|
|
147
|
-
const agentDir = join(
|
|
149
|
+
const agentDir = join(home, 'agents', agentId);
|
|
148
150
|
mkdirSync(join(agentDir, 'memory'), { recursive: true });
|
|
149
|
-
mkdirSync(join(agentDir, 'knowledge'), { recursive: true });
|
|
150
151
|
|
|
151
152
|
const langText = { bilingual: 'Bilingual — match the customer language', en: 'English', ar: 'Arabic', fr: 'French', tr: 'Turkish', ur: 'Urdu' };
|
|
152
153
|
const toneText = { 30: 'Formal and polished.', 50: 'Professional but warm.', 80: 'Casual and friendly.', 90: 'Fun and sarcastic.' };
|
|
153
154
|
|
|
154
|
-
const soul = '# ' + agentName + '\n\n## Who I Am\nI\'m ' + agentName + '. ' + (agentPurpose || 'A helpful AI assistant.') + '\n\n## How I Speak\n- Language: ' + (langText[language] || 'English') + '\n- Tone: ' + (toneText[tone] || toneText[50]) + '\n- Short messages, natural, human-like\n- Emojis: natural but not overdone\n\n## What I Do\n' + (agentPurpose || 'Help people.') + '\n\n## Never\n- Say "As an AI" or "I\'d be happy to help!"\n- Send walls of text\n- Make things up\n- Over-respond to "thanks" or "ok" — just react
|
|
155
|
+
const soul = '# ' + agentName + '\n\n## Who I Am\nI\'m ' + agentName + '. ' + (agentPurpose || 'A helpful AI assistant.') + '\n\n## How I Speak\n- Language: ' + (langText[language] || 'English') + '\n- Tone: ' + (toneText[tone] || toneText[50]) + '\n- Short messages, natural, human-like\n- Emojis: natural but not overdone\n\n## What I Do\n' + (agentPurpose || 'Help people.') + '\n\n## Never\n- Say "As an AI" or "I\'d be happy to help!"\n- Send walls of text\n- Make things up\n- Over-respond to "thanks" or "ok" — just react with a heart\n';
|
|
155
156
|
|
|
156
157
|
writeFileSync(join(agentDir, 'SOUL.md'), soul);
|
|
157
158
|
writeFileSync(join(agentDir, 'IDENTITY.md'), '# ' + agentName + '\n- Name: ' + agentName + '\n- Language: ' + language + '\n- Emoji: 🦑\n');
|
|
@@ -174,18 +175,38 @@ export async function setup() {
|
|
|
174
175
|
});
|
|
175
176
|
if (p.isCancel(connectWA)) return p.cancel('Setup cancelled');
|
|
176
177
|
|
|
178
|
+
let waConnected = false;
|
|
179
|
+
|
|
177
180
|
if (connectWA === 'pair') {
|
|
178
181
|
const waPhone = await p.text({
|
|
179
182
|
message: 'WhatsApp phone number (with country code):',
|
|
180
183
|
placeholder: '+966 5XX XXX XXXX',
|
|
181
184
|
validate: (v) => v.replace(/[^0-9+]/g, '').length < 8 ? 'Enter a valid phone number' : undefined,
|
|
182
185
|
});
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
if (p.isCancel(waPhone)) return p.cancel('Setup cancelled');
|
|
187
|
+
|
|
188
|
+
const cleanPhone = waPhone.replace(/[^0-9]/g, '');
|
|
189
|
+
manifest.whatsappNumber = cleanPhone;
|
|
190
|
+
|
|
191
|
+
// Actually connect WhatsApp NOW
|
|
192
|
+
console.log();
|
|
193
|
+
try {
|
|
194
|
+
const { connectWhatsApp } = await import('./wa-login.js');
|
|
195
|
+
waConnected = await connectWhatsApp(home, agentId, 'pair', cleanPhone);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.log(chalk.yellow(' WhatsApp connection failed: ' + err.message));
|
|
198
|
+
console.log(chalk.gray(' You can connect later: squidclaw channels login whatsapp'));
|
|
186
199
|
}
|
|
187
200
|
} else if (connectWA === 'qr') {
|
|
188
|
-
|
|
201
|
+
// Actually show QR code NOW
|
|
202
|
+
console.log();
|
|
203
|
+
try {
|
|
204
|
+
const { connectWhatsApp } = await import('./wa-login.js');
|
|
205
|
+
waConnected = await connectWhatsApp(home, agentId, 'qr', null);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.log(chalk.yellow(' WhatsApp connection failed: ' + err.message));
|
|
208
|
+
console.log(chalk.gray(' You can connect later: squidclaw channels login whatsapp'));
|
|
209
|
+
}
|
|
189
210
|
}
|
|
190
211
|
|
|
191
212
|
if (connectWA !== 'skip') {
|
|
@@ -219,16 +240,9 @@ export async function setup() {
|
|
|
219
240
|
writeFileSync(join(agentDir, 'agent.json'), JSON.stringify(manifest, null, 2));
|
|
220
241
|
|
|
221
242
|
// ═══ Summary ═══
|
|
222
|
-
const waStatus =
|
|
243
|
+
const waStatus = waConnected ? chalk.green('connected ✅') : connectWA !== 'skip' ? chalk.yellow('will connect on start') : 'skipped';
|
|
223
244
|
const tgStatus = connectTG ? 'bot ready' : 'skipped';
|
|
224
245
|
|
|
225
|
-
// ═══ Offer to Hatch ═══
|
|
226
|
-
const hatchNow = await p.confirm({
|
|
227
|
-
message: '🥚 Ready to hatch ' + agentName + '? (wake up and meet your agent)',
|
|
228
|
-
initialValue: true,
|
|
229
|
-
});
|
|
230
|
-
if (p.isCancel(hatchNow)) return p.cancel('Setup cancelled');
|
|
231
|
-
|
|
232
246
|
console.log(chalk.green('\n ════════════════════════════════════'));
|
|
233
247
|
console.log(chalk.green(' ✅ Setup Complete!'));
|
|
234
248
|
console.log(chalk.green(' ════════════════════════════════════\n'));
|
|
@@ -237,25 +251,23 @@ export async function setup() {
|
|
|
237
251
|
console.log(' ' + chalk.bold('Language:') + ' ' + (langText[language] || language));
|
|
238
252
|
console.log(' ' + chalk.bold('WhatsApp:') + ' ' + waStatus);
|
|
239
253
|
console.log(' ' + chalk.bold('Telegram:') + ' ' + tgStatus);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
console.log(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
console.log(' WhatsApp → ⚙️ → Linked Devices → Link a Device');
|
|
249
|
-
}
|
|
250
|
-
console.log('\n ' + chalk.bold('Other commands:\n'));
|
|
251
|
-
console.log(' ' + chalk.cyan('squidclaw status') + ' — check what\'s running');
|
|
252
|
-
console.log(' ' + chalk.cyan('squidclaw tui') + ' — chat in terminal');
|
|
253
|
-
console.log(' ' + chalk.cyan('squidclaw agent chat ' + agentId) + ' — test your agent');
|
|
254
|
-
console.log(' ' + chalk.cyan('squidclaw help') + ' — all commands');
|
|
255
|
-
console.log('\n ════════════════════════════════════\n');
|
|
254
|
+
|
|
255
|
+
// ═══ Hatch TUI ═══
|
|
256
|
+
console.log();
|
|
257
|
+
const hatchNow = await p.confirm({
|
|
258
|
+
message: '🥚 Ready to hatch ' + agentName + '? (chat with your newborn agent)',
|
|
259
|
+
initialValue: true,
|
|
260
|
+
});
|
|
261
|
+
if (p.isCancel(hatchNow)) return p.cancel('Setup cancelled');
|
|
256
262
|
|
|
257
263
|
if (hatchNow) {
|
|
258
264
|
const { hatchTUI } = await import('./hatch-tui.js');
|
|
259
265
|
await hatchTUI(agentId);
|
|
266
|
+
} else {
|
|
267
|
+
console.log('\n ' + chalk.bold('Start later:\n'));
|
|
268
|
+
console.log(' ' + chalk.cyan('squidclaw start'));
|
|
269
|
+
console.log(' ' + chalk.cyan('squidclaw tui'));
|
|
270
|
+
console.log(' ' + chalk.cyan('squidclaw wake'));
|
|
271
|
+
console.log();
|
|
260
272
|
}
|
|
261
273
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🦑 WhatsApp Login — inline during setup
|
|
3
|
+
* Shows QR code or pairing code right in the terminal
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } from '@whiskeysockets/baileys';
|
|
7
|
+
import qrcode from 'qrcode-terminal';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { mkdirSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
export async function connectWhatsApp(home, agentId, method, phoneNumber) {
|
|
13
|
+
const authDir = join(home, 'channels', 'whatsapp', agentId);
|
|
14
|
+
mkdirSync(authDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
|
17
|
+
const { version } = await fetchLatestBaileysVersion();
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let resolved = false;
|
|
21
|
+
let timeout = null;
|
|
22
|
+
|
|
23
|
+
const sock = makeWASocket({
|
|
24
|
+
version,
|
|
25
|
+
auth: state,
|
|
26
|
+
printQRInTerminal: false,
|
|
27
|
+
browser: ['Squidclaw', 'Chrome', '126.0'],
|
|
28
|
+
generateHighQualityLinkPreview: false,
|
|
29
|
+
syncFullHistory: false,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// QR code handler
|
|
33
|
+
sock.ev.on('connection.update', async (update) => {
|
|
34
|
+
const { connection, lastDisconnect, qr } = update;
|
|
35
|
+
|
|
36
|
+
if (qr && method === 'qr') {
|
|
37
|
+
console.log();
|
|
38
|
+
console.log(chalk.cyan(' 📱 Scan this QR code with WhatsApp:'));
|
|
39
|
+
console.log(chalk.gray(' WhatsApp → Settings → Linked Devices → Link a Device'));
|
|
40
|
+
console.log();
|
|
41
|
+
qrcode.generate(qr, { small: true }, (code) => {
|
|
42
|
+
// Indent each line
|
|
43
|
+
const lines = code.split('\n');
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
console.log(' ' + line);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(chalk.yellow(' Waiting for scan...'));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (qr && method === 'pair' && phoneNumber) {
|
|
53
|
+
try {
|
|
54
|
+
const code = await sock.requestPairingCode(phoneNumber);
|
|
55
|
+
console.log();
|
|
56
|
+
console.log(chalk.cyan(' ════════════════════════════════════'));
|
|
57
|
+
console.log(chalk.cyan(' 📱 Your pairing code:'));
|
|
58
|
+
console.log();
|
|
59
|
+
console.log(chalk.bold.green(' ' + code.match(/.{1,4}/g).join(' - ')));
|
|
60
|
+
console.log();
|
|
61
|
+
console.log(chalk.gray(' Enter this in WhatsApp:'));
|
|
62
|
+
console.log(chalk.gray(' Settings → Linked Devices → Link a Device'));
|
|
63
|
+
console.log(chalk.gray(' → Link with phone number'));
|
|
64
|
+
console.log(chalk.cyan(' ════════════════════════════════════'));
|
|
65
|
+
console.log();
|
|
66
|
+
console.log(chalk.yellow(' Waiting for confirmation...'));
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.log(chalk.red(' Failed to get pairing code: ' + err.message));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (connection === 'open') {
|
|
73
|
+
console.log();
|
|
74
|
+
console.log(chalk.green(' ✅ WhatsApp connected!'));
|
|
75
|
+
console.log();
|
|
76
|
+
resolved = true;
|
|
77
|
+
clearTimeout(timeout);
|
|
78
|
+
await saveCreds();
|
|
79
|
+
// Don't close — let the engine take over
|
|
80
|
+
try { sock.end(); } catch {}
|
|
81
|
+
resolve(true);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (connection === 'close') {
|
|
85
|
+
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
86
|
+
if (statusCode === DisconnectReason.loggedOut) {
|
|
87
|
+
console.log(chalk.red(' ✗ Logged out. Try again.'));
|
|
88
|
+
if (!resolved) resolve(false);
|
|
89
|
+
} else if (statusCode === 515) {
|
|
90
|
+
// Restart requested after pairing
|
|
91
|
+
console.log(chalk.green(' ✅ WhatsApp paired! Will connect on engine start.'));
|
|
92
|
+
await saveCreds();
|
|
93
|
+
if (!resolved) { resolved = true; resolve(true); }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
sock.ev.on('creds.update', saveCreds);
|
|
99
|
+
|
|
100
|
+
// Timeout after 2 minutes
|
|
101
|
+
timeout = setTimeout(() => {
|
|
102
|
+
if (!resolved) {
|
|
103
|
+
console.log(chalk.yellow(' ⏰ Timed out. You can connect later with: squidclaw channels login whatsapp'));
|
|
104
|
+
try { sock.end(); } catch {}
|
|
105
|
+
resolve(false);
|
|
106
|
+
}
|
|
107
|
+
}, 120000);
|
|
108
|
+
});
|
|
109
|
+
}
|