twinclaw 1.1.1 → 1.1.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/README.md +3 -3
- package/dist/api/handlers/agents.js +82 -0
- package/dist/api/handlers/debug.js +69 -0
- package/dist/api/handlers/devices.js +79 -0
- package/dist/api/handlers/jobs.js +99 -0
- package/dist/api/handlers/status.js +149 -0
- package/dist/api/router.js +272 -2
- package/dist/api/runtime-event-producer.js +20 -0
- package/dist/api/shared.js +34 -0
- package/dist/api/websocket-hub.js +18 -7
- package/dist/config/json-config.js +393 -1
- package/dist/core/heartbeat.js +304 -8
- package/dist/core/lane-executor.js +18 -0
- package/dist/core/onboarding.js +2 -2
- package/dist/core/self-improve-cli.js +212 -0
- package/dist/core/simplified-onboarding.js +158 -16
- package/dist/index.js +61 -33
- package/dist/interfaces/dispatcher.js +4 -2
- package/dist/interfaces/whatsapp_handler.js +243 -2
- package/dist/services/auto-configurer.js +591 -0
- package/dist/services/device-pairing.js +378 -0
- package/dist/services/hooks.js +130 -0
- package/dist/services/job-scheduler.js +133 -1
- package/dist/services/learning-system.js +226 -0
- package/dist/services/self-healing.js +267 -0
- package/dist/services/skill-acquisition/intent-parser.js +115 -0
- package/dist/services/skill-acquisition/research-engine.js +319 -0
- package/dist/services/skill-builder.js +235 -0
- package/dist/services/sub-agent-service.js +215 -0
- package/dist/services/web-service.js +70 -0
- package/dist/services/webhook-service.js +127 -0
- package/dist/skills/builtin.js +827 -0
- package/dist/tools/agent-improvement.js +208 -0
- package/dist/types/skill-acquisition.js +4 -0
- package/package.json +3 -1
|
@@ -1,15 +1,45 @@
|
|
|
1
1
|
import WAWebJS from 'whatsapp-web.js';
|
|
2
2
|
import qrcode from 'qrcode-terminal';
|
|
3
|
+
import axios from 'axios';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import fs from 'node:fs/promises';
|
|
6
7
|
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { logThought } from '../utils/logger.js';
|
|
7
9
|
const { Client, LocalAuth, MessageMedia } = WAWebJS;
|
|
8
10
|
const RATE_LIMIT_MS = 1500;
|
|
11
|
+
/**
|
|
12
|
+
* Create MessageMedia from URL
|
|
13
|
+
*/
|
|
14
|
+
async function MessageMediaFromUrl(url, caption, mimeType, filename) {
|
|
15
|
+
try {
|
|
16
|
+
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
|
17
|
+
const buffer = Buffer.from(response.data);
|
|
18
|
+
const mime = mimeType || response.headers['content-type'] || 'application/octet-stream';
|
|
19
|
+
const name = filename || url.split('/').pop() || 'file';
|
|
20
|
+
return new MessageMedia(mime, buffer.toString('base64'), name);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
console.error('[WhatsAppHandler] Failed to fetch media from URL:', err);
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
9
27
|
export class WhatsAppHandler {
|
|
10
28
|
#client;
|
|
11
29
|
#lastMessageAt = 0;
|
|
12
|
-
|
|
30
|
+
// Session recovery
|
|
31
|
+
#connectionState = 'disconnected';
|
|
32
|
+
#reconnectAttempts = 0;
|
|
33
|
+
#maxReconnectAttempts = 5;
|
|
34
|
+
#reconnectDelayMs = 1000;
|
|
35
|
+
#maxReconnectDelayMs = 60000;
|
|
36
|
+
// Message tracking for status updates
|
|
37
|
+
#trackedMessages = new Map();
|
|
38
|
+
// Event callbacks
|
|
39
|
+
onConnectionStateChange;
|
|
40
|
+
onMessageStatus;
|
|
41
|
+
onQrCode;
|
|
42
|
+
constructor(options = {}) {
|
|
13
43
|
const disableChromiumSandbox = process.env.WHATSAPP_DISABLE_CHROMIUM_SANDBOX === 'true';
|
|
14
44
|
const clientConfig = {
|
|
15
45
|
authStrategy: new LocalAuth({ dataPath: './memory/whatsapp_auth' }),
|
|
@@ -20,6 +50,10 @@ export class WhatsAppHandler {
|
|
|
20
50
|
};
|
|
21
51
|
}
|
|
22
52
|
this.#client = new Client(clientConfig);
|
|
53
|
+
// Configure reconnection options
|
|
54
|
+
this.#maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
55
|
+
this.#reconnectDelayMs = options.reconnectDelayMs ?? 1000;
|
|
56
|
+
this.#maxReconnectDelayMs = options.maxReconnectDelayMs ?? 60000;
|
|
23
57
|
this.#registerListeners();
|
|
24
58
|
}
|
|
25
59
|
onMessage;
|
|
@@ -31,13 +65,55 @@ export class WhatsAppHandler {
|
|
|
31
65
|
this.#lastMessageAt = Date.now();
|
|
32
66
|
}
|
|
33
67
|
#registerListeners() {
|
|
68
|
+
// QR Code generation
|
|
34
69
|
this.#client.on('qr', (qr) => {
|
|
35
70
|
console.log('[WhatsAppHandler] Scan this QR code to authenticate:');
|
|
36
71
|
qrcode.generate(qr, { small: true });
|
|
72
|
+
this.onQrCode?.(qr);
|
|
73
|
+
});
|
|
74
|
+
// Authentication success
|
|
75
|
+
this.#client.on('authenticated', () => {
|
|
76
|
+
console.log('[WhatsAppHandler] Session authenticated successfully');
|
|
77
|
+
this.#connectionState = 'connected';
|
|
78
|
+
this.#reconnectAttempts = 0;
|
|
79
|
+
this.onConnectionStateChange?.('connected');
|
|
37
80
|
});
|
|
81
|
+
// Client ready
|
|
38
82
|
this.#client.on('ready', () => {
|
|
39
83
|
console.log('[WhatsAppHandler] Client is ready!');
|
|
84
|
+
this.#connectionState = 'connected';
|
|
85
|
+
this.#reconnectAttempts = 0;
|
|
86
|
+
this.onConnectionStateChange?.('connected');
|
|
87
|
+
});
|
|
88
|
+
// Connection failure
|
|
89
|
+
this.#client.on('failure', (error) => {
|
|
90
|
+
console.error('[WhatsAppHandler] Client failed:', error);
|
|
91
|
+
this.#connectionState = 'failed';
|
|
92
|
+
this.onConnectionStateChange?.('failed');
|
|
93
|
+
this.#handleDisconnection();
|
|
94
|
+
});
|
|
95
|
+
// Disconnected event
|
|
96
|
+
this.#client.on('disconnected', (reason) => {
|
|
97
|
+
console.log('[WhatsAppHandler] Client disconnected:', reason);
|
|
98
|
+
this.#connectionState = 'disconnected';
|
|
99
|
+
this.onConnectionStateChange?.('disconnected');
|
|
100
|
+
this.#handleDisconnection();
|
|
101
|
+
});
|
|
102
|
+
// Connection state changes
|
|
103
|
+
this.#client.on('change_state', (state) => {
|
|
104
|
+
console.log('[WhatsAppHandler] Connection state changed:', state);
|
|
105
|
+
if (state === 'close' || state === 'disconnected' || state === 'open') {
|
|
106
|
+
// Only trigger disconnection for non-connected states
|
|
107
|
+
if (state !== 'open' && this.#connectionState === 'connected') {
|
|
108
|
+
this.#handleDisconnection();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
// Message acknowledgment (acks)
|
|
113
|
+
this.#client.on('message_ack', (message, ack) => {
|
|
114
|
+
this.#handleMessageAck(message, ack);
|
|
40
115
|
});
|
|
116
|
+
// Incoming messages
|
|
41
117
|
this.#client.on('message', async (msg) => {
|
|
42
118
|
await this.#applyRateLimit();
|
|
43
119
|
const base = {
|
|
@@ -81,10 +157,175 @@ export class WhatsAppHandler {
|
|
|
81
157
|
});
|
|
82
158
|
this.#client.initialize().catch(err => {
|
|
83
159
|
console.error('[WhatsAppHandler] Failed to initialize client:', err);
|
|
160
|
+
this.#connectionState = 'failed';
|
|
161
|
+
this.onConnectionStateChange?.('failed');
|
|
84
162
|
});
|
|
85
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Handle disconnection with exponential backoff reconnection
|
|
166
|
+
*/
|
|
167
|
+
async #handleDisconnection() {
|
|
168
|
+
if (this.#reconnectAttempts >= this.#maxReconnectAttempts) {
|
|
169
|
+
console.error('[WhatsAppHandler] Max reconnection attempts reached');
|
|
170
|
+
await logThought('[WhatsAppHandler] Failed to reconnect after multiple attempts. Please scan a new QR code.');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.#connectionState = 'connecting';
|
|
174
|
+
this.onConnectionStateChange?.('connecting');
|
|
175
|
+
const delay = Math.min(this.#reconnectDelayMs * Math.pow(2, this.#reconnectAttempts), this.#maxReconnectDelayMs);
|
|
176
|
+
console.log(`[WhatsAppHandler] Attempting to reconnect in ${delay}ms (attempt ${this.#reconnectAttempts + 1}/${this.#maxReconnectAttempts})`);
|
|
177
|
+
await logThought(`[WhatsAppHandler] Reconnecting... attempt ${this.#reconnectAttempts + 1}`);
|
|
178
|
+
this.#reconnectAttempts++;
|
|
179
|
+
setTimeout(async () => {
|
|
180
|
+
try {
|
|
181
|
+
await this.#client.initialize();
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
console.error('[WhatsAppHandler] Reconnection failed:', err);
|
|
185
|
+
// The failure event will trigger another attempt via the event handler
|
|
186
|
+
}
|
|
187
|
+
}, delay);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Handle message acknowledgment status updates
|
|
191
|
+
*/
|
|
192
|
+
#handleMessageAck(message, ack) {
|
|
193
|
+
// Use _serialized which is the reliable message ID in whatsapp-web.js
|
|
194
|
+
const messageKey = message.id._serialized;
|
|
195
|
+
const tracked = this.#trackedMessages.get(messageKey);
|
|
196
|
+
let status;
|
|
197
|
+
// Map WhatsApp ack values to our status
|
|
198
|
+
// -1: Error, 0: Pending, 1: Server, 2: Device, 3: Read, 4: Played
|
|
199
|
+
switch (ack) {
|
|
200
|
+
case -1:
|
|
201
|
+
status = 'failed';
|
|
202
|
+
break;
|
|
203
|
+
case 0:
|
|
204
|
+
status = 'queued';
|
|
205
|
+
break;
|
|
206
|
+
case 1:
|
|
207
|
+
case 2:
|
|
208
|
+
status = 'sent';
|
|
209
|
+
break;
|
|
210
|
+
case 3:
|
|
211
|
+
status = 'delivered';
|
|
212
|
+
break;
|
|
213
|
+
case 4:
|
|
214
|
+
status = 'read';
|
|
215
|
+
break;
|
|
216
|
+
default:
|
|
217
|
+
status = 'sent';
|
|
218
|
+
}
|
|
219
|
+
if (tracked) {
|
|
220
|
+
tracked.ackStatus = ack;
|
|
221
|
+
tracked.status = status;
|
|
222
|
+
this.onMessageStatus?.(messageKey, message.from, status);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** Get current connection state */
|
|
226
|
+
get connectionState() {
|
|
227
|
+
return this.#connectionState;
|
|
228
|
+
}
|
|
86
229
|
async sendText(chatId, text) {
|
|
87
|
-
await this.#client.sendMessage(chatId, text);
|
|
230
|
+
const message = await this.#client.sendMessage(chatId, text);
|
|
231
|
+
// Use _serialized which is the reliable message ID in whatsapp-web.js
|
|
232
|
+
const messageId = message.id._serialized;
|
|
233
|
+
// Track the message for status updates
|
|
234
|
+
this.#trackedMessages.set(messageId, {
|
|
235
|
+
id: messageId,
|
|
236
|
+
chatId,
|
|
237
|
+
status: 'sent',
|
|
238
|
+
sentAt: new Date(),
|
|
239
|
+
});
|
|
240
|
+
return messageId;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Send an image with optional caption
|
|
244
|
+
*/
|
|
245
|
+
async sendImage(chatId, imagePath, caption) {
|
|
246
|
+
const media = await MessageMedia.fromFilePath(imagePath);
|
|
247
|
+
const message = await this.#client.sendMessage(chatId, media, { caption });
|
|
248
|
+
const messageId = message.id._serialized;
|
|
249
|
+
this.#trackedMessages.set(messageId, {
|
|
250
|
+
id: messageId,
|
|
251
|
+
chatId,
|
|
252
|
+
status: 'sent',
|
|
253
|
+
sentAt: new Date(),
|
|
254
|
+
});
|
|
255
|
+
return messageId;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Send an image from URL
|
|
259
|
+
*/
|
|
260
|
+
async sendImageFromUrl(chatId, url, caption) {
|
|
261
|
+
const media = await MessageMediaFromUrl(url, caption);
|
|
262
|
+
const message = await this.#client.sendMessage(chatId, media);
|
|
263
|
+
const messageId = message.id._serialized;
|
|
264
|
+
this.#trackedMessages.set(messageId, {
|
|
265
|
+
id: messageId,
|
|
266
|
+
chatId,
|
|
267
|
+
status: 'sent',
|
|
268
|
+
sentAt: new Date(),
|
|
269
|
+
});
|
|
270
|
+
return messageId;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Send a video with optional caption
|
|
274
|
+
*/
|
|
275
|
+
async sendVideo(chatId, videoPath, caption) {
|
|
276
|
+
const media = await MessageMedia.fromFilePath(videoPath);
|
|
277
|
+
const message = await this.#client.sendMessage(chatId, media, { caption });
|
|
278
|
+
const messageId = message.id._serialized;
|
|
279
|
+
this.#trackedMessages.set(messageId, {
|
|
280
|
+
id: messageId,
|
|
281
|
+
chatId,
|
|
282
|
+
status: 'sent',
|
|
283
|
+
sentAt: new Date(),
|
|
284
|
+
});
|
|
285
|
+
return messageId;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Send a document/file
|
|
289
|
+
*/
|
|
290
|
+
async sendDocument(chatId, filePath, caption) {
|
|
291
|
+
const media = await MessageMedia.fromFilePath(filePath);
|
|
292
|
+
const message = await this.#client.sendMessage(chatId, media, { caption });
|
|
293
|
+
const messageId = message.id._serialized;
|
|
294
|
+
this.#trackedMessages.set(messageId, {
|
|
295
|
+
id: messageId,
|
|
296
|
+
chatId,
|
|
297
|
+
status: 'sent',
|
|
298
|
+
sentAt: new Date(),
|
|
299
|
+
});
|
|
300
|
+
return messageId;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Send a document from URL
|
|
304
|
+
*/
|
|
305
|
+
async sendDocumentFromUrl(chatId, url, filename, caption) {
|
|
306
|
+
const media = await MessageMediaFromUrl(url, caption, undefined, filename);
|
|
307
|
+
const message = await this.#client.sendMessage(chatId, media);
|
|
308
|
+
const messageId = message.id._serialized;
|
|
309
|
+
this.#trackedMessages.set(messageId, {
|
|
310
|
+
id: messageId,
|
|
311
|
+
chatId,
|
|
312
|
+
status: 'sent',
|
|
313
|
+
sentAt: new Date(),
|
|
314
|
+
});
|
|
315
|
+
return messageId;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Get message status
|
|
319
|
+
*/
|
|
320
|
+
getMessageStatus(messageId) {
|
|
321
|
+
return this.#trackedMessages.get(messageId)?.status;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Force reconnection
|
|
325
|
+
*/
|
|
326
|
+
async reconnect() {
|
|
327
|
+
this.#reconnectAttempts = 0;
|
|
328
|
+
await this.#client.initialize();
|
|
88
329
|
}
|
|
89
330
|
async sendVoice(chatId, audio) {
|
|
90
331
|
const media = new MessageMedia('audio/wav', audio.toString('base64'), 'response.wav');
|