wagent 1.0.0
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 +103 -0
- package/dist/channels/baileys/baileys.adapter.js +992 -0
- package/dist/channels/baileys/baileys.auth.js +96 -0
- package/dist/channels/baileys/baileys.events.js +284 -0
- package/dist/channels/baileys/baileys.version.js +86 -0
- package/dist/channels/channel.interface.js +6 -0
- package/dist/config.js +120 -0
- package/dist/constants.js +59 -0
- package/dist/db/client.js +160 -0
- package/dist/db/schema.js +189 -0
- package/dist/index.js +873 -0
- package/dist/services/instance-manager.js +185 -0
- package/dist/types/channel.types.js +4 -0
- package/dist/types/db.types.js +4 -0
- package/dist/utils/logger.js +39 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { InstanceManager } from "./services/instance-manager.js";
|
|
3
|
+
import { createChildLogger } from "./utils/logger.js";
|
|
4
|
+
import { generateText, tool } from "ai";
|
|
5
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import qrcode from "qrcode-terminal";
|
|
8
|
+
import { ensureAiConfig, loadConfig, saveConfig } from "./config.js";
|
|
9
|
+
const logger = createChildLogger({ service: "wagent" });
|
|
10
|
+
const chatHistories = new Map();
|
|
11
|
+
const MAX_HISTORY = 30;
|
|
12
|
+
const processedMessages = new Set();
|
|
13
|
+
const agentSentMessages = new Set();
|
|
14
|
+
const typingUntil = new Map();
|
|
15
|
+
function getOwnJid(adapter) {
|
|
16
|
+
return adapter.getMyJid();
|
|
17
|
+
}
|
|
18
|
+
function getContactName(adapter, jid) {
|
|
19
|
+
return adapter.getContacts(jid).then((c) => {
|
|
20
|
+
const match = c.find((x) => x.jid === jid);
|
|
21
|
+
return match?.name ?? match?.notifyName ?? jid.split("@")[0] ?? null;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function getGroupName(adapter, jid) {
|
|
25
|
+
return adapter.getGroupMetadata(jid).then((g) => g.subject).catch(() => jid.split("@")[0]);
|
|
26
|
+
}
|
|
27
|
+
function getModelNames(config) {
|
|
28
|
+
return [config.model, ...config.fallbackModels].filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
async function getLanguageModel(config, modelName) {
|
|
31
|
+
if (config.provider === "gemini") {
|
|
32
|
+
const mod = await import("@ai-sdk/google");
|
|
33
|
+
const createGoogleGenerativeAI = mod.createGoogleGenerativeAI;
|
|
34
|
+
const google = createGoogleGenerativeAI({ apiKey: config.apiKey });
|
|
35
|
+
return google(modelName);
|
|
36
|
+
}
|
|
37
|
+
const provider = createOpenAICompatible({
|
|
38
|
+
name: config.provider,
|
|
39
|
+
baseURL: config.baseUrl ?? "https://api.groq.com/openai/v1",
|
|
40
|
+
apiKey: config.apiKey,
|
|
41
|
+
});
|
|
42
|
+
return provider.chatModel(modelName);
|
|
43
|
+
}
|
|
44
|
+
async function generateWithFallback(config, options) {
|
|
45
|
+
let lastError;
|
|
46
|
+
for (const modelName of getModelNames(config)) {
|
|
47
|
+
try {
|
|
48
|
+
return await generateText({ ...options, model: await getLanguageModel(config, modelName) });
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
lastError = err;
|
|
52
|
+
logger.warn({ err, model: modelName }, "Model failed, trying fallback");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw lastError;
|
|
56
|
+
}
|
|
57
|
+
function randomBetween(min, max) {
|
|
58
|
+
return Math.floor(min + Math.random() * (max - min + 1));
|
|
59
|
+
}
|
|
60
|
+
async function delayBeforeReply(config) {
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, randomBetween(config.replyDelayMinMs, config.replyDelayMaxMs)));
|
|
62
|
+
}
|
|
63
|
+
async function waitForTypingToStop(chatId) {
|
|
64
|
+
const started = Date.now();
|
|
65
|
+
while ((typingUntil.get(chatId) ?? 0) > Date.now() && Date.now() - started < 30_000) {
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, 750));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function rememberAgentMessage(messageId) {
|
|
70
|
+
if (!messageId)
|
|
71
|
+
return;
|
|
72
|
+
agentSentMessages.add(messageId);
|
|
73
|
+
if (agentSentMessages.size > 1000) {
|
|
74
|
+
const toDelete = [...agentSentMessages].slice(0, 500);
|
|
75
|
+
for (const id of toDelete)
|
|
76
|
+
agentSentMessages.delete(id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function normalizePhone(input) {
|
|
80
|
+
const digits = input.replace(/[^0-9]/g, "");
|
|
81
|
+
return digits.length >= 8 ? `${digits}@s.whatsapp.net` : null;
|
|
82
|
+
}
|
|
83
|
+
function normalizeName(input) {
|
|
84
|
+
return input.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
85
|
+
}
|
|
86
|
+
function editDistance(a, b) {
|
|
87
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
|
|
88
|
+
for (let i = 1; i <= a.length; i++) {
|
|
89
|
+
for (let j = 1; j <= b.length; j++) {
|
|
90
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return dp[a.length][b.length];
|
|
94
|
+
}
|
|
95
|
+
async function resolveTarget(adapter, to, ctx) {
|
|
96
|
+
const raw = to?.trim();
|
|
97
|
+
if (!raw)
|
|
98
|
+
return ctx.currentChatId;
|
|
99
|
+
if (raw.includes("@")) {
|
|
100
|
+
if (raw.startsWith("@"))
|
|
101
|
+
throw new Error(`Invalid target JID: ${raw}`);
|
|
102
|
+
return raw;
|
|
103
|
+
}
|
|
104
|
+
const phoneJid = normalizePhone(raw);
|
|
105
|
+
if (phoneJid)
|
|
106
|
+
return phoneJid;
|
|
107
|
+
let contacts = await adapter.getContacts(raw);
|
|
108
|
+
if (contacts.length === 0) {
|
|
109
|
+
const needle = normalizeName(raw);
|
|
110
|
+
const fuzzy = (await adapter.getContacts())
|
|
111
|
+
.map((contact) => {
|
|
112
|
+
const names = [contact.name, contact.notifyName].filter(Boolean).map((name) => normalizeName(name));
|
|
113
|
+
const distance = Math.min(...names.map((name) => editDistance(needle, name)), Number.POSITIVE_INFINITY);
|
|
114
|
+
return { contact, distance };
|
|
115
|
+
})
|
|
116
|
+
.filter(({ distance }) => distance <= 2)
|
|
117
|
+
.sort((a, b) => a.distance - b.distance);
|
|
118
|
+
if (fuzzy.length > 0)
|
|
119
|
+
contacts = [fuzzy[0].contact];
|
|
120
|
+
}
|
|
121
|
+
const query = raw.toLowerCase();
|
|
122
|
+
const exact = contacts.find((c) => [c.name, c.notifyName, c.phone]
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.some((v) => v.toLowerCase() === query));
|
|
125
|
+
const match = exact ?? contacts[0];
|
|
126
|
+
if (!match)
|
|
127
|
+
throw new Error(`Could not resolve contact "${raw}". Try a phone number or exact contact name.`);
|
|
128
|
+
if (contacts.length > 1 && !exact) {
|
|
129
|
+
const names = contacts.slice(0, 5).map((c) => c.name ?? c.notifyName ?? c.phone ?? c.jid).join(", ");
|
|
130
|
+
throw new Error(`Multiple contacts match "${raw}": ${names}. Use a more specific name or phone number.`);
|
|
131
|
+
}
|
|
132
|
+
const resolved = match.phone ? `${match.phone}@s.whatsapp.net` : match.jid;
|
|
133
|
+
if (!resolved || resolved.startsWith("@"))
|
|
134
|
+
throw new Error(`Resolved invalid target for "${raw}": ${resolved}`);
|
|
135
|
+
return resolved;
|
|
136
|
+
}
|
|
137
|
+
function buildTools(adapter) {
|
|
138
|
+
return {
|
|
139
|
+
sendText: tool({
|
|
140
|
+
description: "Send a text message to a phone number or JID. Omit 'to' to send to the current conversation.",
|
|
141
|
+
inputSchema: z.object({
|
|
142
|
+
to: z.string().optional().describe("Phone/JID. Defaults to current chat."),
|
|
143
|
+
text: z.string().describe("Message content"),
|
|
144
|
+
quotedMessageId: z.string().optional().describe("Reply to a specific message ID"),
|
|
145
|
+
}),
|
|
146
|
+
execute: async ({ to, text, quotedMessageId }, { experimental_context }) => {
|
|
147
|
+
const ctx = experimental_context;
|
|
148
|
+
const target = await resolveTarget(adapter, to, ctx);
|
|
149
|
+
const content = { type: "text", text };
|
|
150
|
+
if (quotedMessageId)
|
|
151
|
+
content.quotedMessageId = quotedMessageId;
|
|
152
|
+
await delayBeforeReply(ctx.config);
|
|
153
|
+
const res = await adapter.sendMessage(target, content);
|
|
154
|
+
rememberAgentMessage(res.messageId);
|
|
155
|
+
return `Sent to ${target}. ID: ${res.messageId}`;
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
sendImage: tool({
|
|
159
|
+
description: "Send an image from a public HTTPS URL. Omit 'to' to send to the current conversation.",
|
|
160
|
+
inputSchema: z.object({
|
|
161
|
+
to: z.string().optional().describe("Phone/JID. Defaults to current chat."),
|
|
162
|
+
imageUrl: z.string().url().describe("Public HTTPS URL of the image"),
|
|
163
|
+
caption: z.string().optional(),
|
|
164
|
+
}),
|
|
165
|
+
execute: async ({ to, imageUrl, caption }, { experimental_context }) => {
|
|
166
|
+
const ctx = experimental_context;
|
|
167
|
+
const target = await resolveTarget(adapter, to, ctx);
|
|
168
|
+
await delayBeforeReply(ctx.config);
|
|
169
|
+
const res = await adapter.sendMessage(target, { type: "image", image: imageUrl, caption });
|
|
170
|
+
rememberAgentMessage(res.messageId);
|
|
171
|
+
return `Image sent. ID: ${res.messageId}`;
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
sendVideo: tool({
|
|
175
|
+
description: "Send a video from a public HTTPS URL. Omit 'to' to send to the current conversation.",
|
|
176
|
+
inputSchema: z.object({
|
|
177
|
+
to: z.string().optional().describe("Phone/JID. Defaults to current chat."),
|
|
178
|
+
videoUrl: z.string().url(),
|
|
179
|
+
caption: z.string().optional(),
|
|
180
|
+
}),
|
|
181
|
+
execute: async ({ to, videoUrl, caption }, { experimental_context }) => {
|
|
182
|
+
const ctx = experimental_context;
|
|
183
|
+
const target = await resolveTarget(adapter, to, ctx);
|
|
184
|
+
await delayBeforeReply(ctx.config);
|
|
185
|
+
const res = await adapter.sendMessage(target, { type: "video", video: videoUrl, caption });
|
|
186
|
+
rememberAgentMessage(res.messageId);
|
|
187
|
+
return `Video sent. ID: ${res.messageId}`;
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
sendAudio: tool({
|
|
191
|
+
description: "Send an audio file or voice note from a URL. Omit 'to' to send to the current conversation.",
|
|
192
|
+
inputSchema: z.object({
|
|
193
|
+
to: z.string().optional().describe("Phone/JID. Defaults to current chat."),
|
|
194
|
+
audioUrl: z.string().url(),
|
|
195
|
+
asVoiceNote: z.boolean().optional().default(false),
|
|
196
|
+
}),
|
|
197
|
+
execute: async ({ to, audioUrl, asVoiceNote }, { experimental_context }) => {
|
|
198
|
+
const ctx = experimental_context;
|
|
199
|
+
const target = await resolveTarget(adapter, to, ctx);
|
|
200
|
+
await delayBeforeReply(ctx.config);
|
|
201
|
+
const res = await adapter.sendMessage(target, { type: "audio", audio: audioUrl, ptt: asVoiceNote });
|
|
202
|
+
rememberAgentMessage(res.messageId);
|
|
203
|
+
return `Audio sent. ID: ${res.messageId}`;
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
sendDocument: tool({
|
|
207
|
+
description: "Send a document/file from a URL. Omit 'to' to send to the current conversation.",
|
|
208
|
+
inputSchema: z.object({
|
|
209
|
+
to: z.string().optional().describe("Phone/JID. Defaults to current chat."),
|
|
210
|
+
documentUrl: z.string().url(),
|
|
211
|
+
fileName: z.string().describe("Display filename e.g. report.pdf"),
|
|
212
|
+
mimeType: z.string().describe("MIME type e.g. application/pdf"),
|
|
213
|
+
}),
|
|
214
|
+
execute: async ({ to, documentUrl, fileName, mimeType }, { experimental_context }) => {
|
|
215
|
+
const ctx = experimental_context;
|
|
216
|
+
const target = await resolveTarget(adapter, to, ctx);
|
|
217
|
+
await delayBeforeReply(ctx.config);
|
|
218
|
+
const res = await adapter.sendMessage(target, { type: "document", document: documentUrl, fileName, mimeType });
|
|
219
|
+
rememberAgentMessage(res.messageId);
|
|
220
|
+
return `Document sent. ID: ${res.messageId}`;
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
sendLocation: tool({
|
|
224
|
+
description: "Send a GPS location pin. Omit 'to' to send to the current conversation.",
|
|
225
|
+
inputSchema: z.object({
|
|
226
|
+
to: z.string().optional().describe("Phone/JID. Defaults to current chat."),
|
|
227
|
+
latitude: z.number(),
|
|
228
|
+
longitude: z.number(),
|
|
229
|
+
name: z.string().optional(),
|
|
230
|
+
address: z.string().optional(),
|
|
231
|
+
}),
|
|
232
|
+
execute: async ({ to, latitude, longitude, name, address }, { experimental_context }) => {
|
|
233
|
+
const ctx = experimental_context;
|
|
234
|
+
const target = await resolveTarget(adapter, to, ctx);
|
|
235
|
+
await delayBeforeReply(ctx.config);
|
|
236
|
+
const res = await adapter.sendMessage(target, { type: "location", latitude, longitude, name, address });
|
|
237
|
+
rememberAgentMessage(res.messageId);
|
|
238
|
+
return `Location sent. ID: ${res.messageId}`;
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
sendContact: tool({
|
|
242
|
+
description: "Send a contact (vCard). Omit 'to' to send to the current conversation.",
|
|
243
|
+
inputSchema: z.object({
|
|
244
|
+
to: z.string().optional().describe("Phone/JID. Defaults to current chat."),
|
|
245
|
+
contactName: z.string(),
|
|
246
|
+
contactPhone: z.string(),
|
|
247
|
+
}),
|
|
248
|
+
execute: async ({ to, contactName, contactPhone }, { experimental_context }) => {
|
|
249
|
+
const ctx = experimental_context;
|
|
250
|
+
const target = await resolveTarget(adapter, to, ctx);
|
|
251
|
+
await delayBeforeReply(ctx.config);
|
|
252
|
+
const res = await adapter.sendMessage(target, { type: "contact", contactName, contactPhone });
|
|
253
|
+
rememberAgentMessage(res.messageId);
|
|
254
|
+
return `Contact sent. ID: ${res.messageId}`;
|
|
255
|
+
},
|
|
256
|
+
}),
|
|
257
|
+
sendPoll: tool({
|
|
258
|
+
description: "Create and send a poll to a group or chat. Omit 'to' to send to the current conversation.",
|
|
259
|
+
inputSchema: z.object({
|
|
260
|
+
to: z.string().optional().describe("Phone/JID. Defaults to current chat."),
|
|
261
|
+
question: z.string(),
|
|
262
|
+
options: z.array(z.string()).min(1).max(12),
|
|
263
|
+
multiSelect: z.boolean().optional().default(false),
|
|
264
|
+
}),
|
|
265
|
+
execute: async ({ to, question, options, multiSelect }, { experimental_context }) => {
|
|
266
|
+
const ctx = experimental_context;
|
|
267
|
+
const target = await resolveTarget(adapter, to, ctx);
|
|
268
|
+
await delayBeforeReply(ctx.config);
|
|
269
|
+
const res = await adapter.sendMessage(target, { type: "poll", question, options, multiSelect });
|
|
270
|
+
rememberAgentMessage(res.messageId);
|
|
271
|
+
return `Poll sent. ID: ${res.messageId}`;
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
sendReaction: tool({
|
|
275
|
+
description: "React to a message with an emoji",
|
|
276
|
+
inputSchema: z.object({
|
|
277
|
+
chatId: z.string(),
|
|
278
|
+
messageId: z.string(),
|
|
279
|
+
emoji: z.string().describe("Single emoji character"),
|
|
280
|
+
}),
|
|
281
|
+
execute: async ({ chatId, messageId, emoji }) => {
|
|
282
|
+
await adapter.sendReaction(chatId, messageId, emoji);
|
|
283
|
+
return `Reacted with ${emoji}`;
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
forwardMessage: tool({
|
|
287
|
+
description: "Forward a message from one chat to another",
|
|
288
|
+
inputSchema: z.object({
|
|
289
|
+
to: z.string(),
|
|
290
|
+
messageId: z.string(),
|
|
291
|
+
fromChatId: z.string(),
|
|
292
|
+
}),
|
|
293
|
+
execute: async ({ to, messageId, fromChatId }) => {
|
|
294
|
+
const res = await adapter.forwardMessage(to, messageId, fromChatId);
|
|
295
|
+
return `Forwarded. ID: ${res.messageId}`;
|
|
296
|
+
},
|
|
297
|
+
}),
|
|
298
|
+
editMessage: tool({
|
|
299
|
+
description: "Edit a message you sent (works on messages less than ~48h old)",
|
|
300
|
+
inputSchema: z.object({
|
|
301
|
+
chatId: z.string(),
|
|
302
|
+
messageId: z.string(),
|
|
303
|
+
newText: z.string(),
|
|
304
|
+
}),
|
|
305
|
+
execute: async ({ chatId, messageId, newText }) => {
|
|
306
|
+
await adapter.editMessage(chatId, messageId, newText);
|
|
307
|
+
return "Message edited";
|
|
308
|
+
},
|
|
309
|
+
}),
|
|
310
|
+
deleteMessage: tool({
|
|
311
|
+
description: "Delete a message for everyone",
|
|
312
|
+
inputSchema: z.object({
|
|
313
|
+
chatId: z.string(),
|
|
314
|
+
messageId: z.string(),
|
|
315
|
+
}),
|
|
316
|
+
execute: async ({ chatId, messageId }) => {
|
|
317
|
+
await adapter.deleteMessage(chatId, messageId);
|
|
318
|
+
return "Message deleted";
|
|
319
|
+
},
|
|
320
|
+
}),
|
|
321
|
+
pinMessage: tool({
|
|
322
|
+
description: "Pin or unpin a message in a chat",
|
|
323
|
+
inputSchema: z.object({
|
|
324
|
+
chatId: z.string(),
|
|
325
|
+
messageId: z.string(),
|
|
326
|
+
pin: z.boolean(),
|
|
327
|
+
}),
|
|
328
|
+
execute: async ({ chatId, messageId, pin }) => {
|
|
329
|
+
await adapter.pinMessage(chatId, messageId, pin);
|
|
330
|
+
return pin ? "Message pinned" : "Message unpinned";
|
|
331
|
+
},
|
|
332
|
+
}),
|
|
333
|
+
sendPresence: tool({
|
|
334
|
+
description: "Show typing indicator or recording indicator in a chat",
|
|
335
|
+
inputSchema: z.object({
|
|
336
|
+
chatId: z.string(),
|
|
337
|
+
status: z.enum(["composing", "recording", "paused", "available", "unavailable"]),
|
|
338
|
+
}),
|
|
339
|
+
execute: async ({ chatId, status }) => {
|
|
340
|
+
await adapter.sendPresence(chatId, status);
|
|
341
|
+
return `Presence: ${status}`;
|
|
342
|
+
},
|
|
343
|
+
}),
|
|
344
|
+
markRead: tool({
|
|
345
|
+
description: "Mark messages as read in a chat",
|
|
346
|
+
inputSchema: z.object({
|
|
347
|
+
chatId: z.string(),
|
|
348
|
+
messageIds: z.array(z.string()),
|
|
349
|
+
}),
|
|
350
|
+
execute: async ({ chatId, messageIds }) => {
|
|
351
|
+
await adapter.markRead(chatId, messageIds);
|
|
352
|
+
return "Marked as read";
|
|
353
|
+
},
|
|
354
|
+
}),
|
|
355
|
+
getMessages: tool({
|
|
356
|
+
description: "Get recent messages from a chat or group",
|
|
357
|
+
inputSchema: z.object({
|
|
358
|
+
chatId: z.string(),
|
|
359
|
+
limit: z.number().min(1).max(100).default(20),
|
|
360
|
+
}),
|
|
361
|
+
execute: async ({ chatId, limit }) => {
|
|
362
|
+
const msgs = await adapter.getMessages(chatId, limit);
|
|
363
|
+
return JSON.stringify(msgs.map((m) => ({
|
|
364
|
+
from: m.isFromMe ? "me" : m.senderId,
|
|
365
|
+
text: m.content,
|
|
366
|
+
time: new Date(m.timestamp * 1000).toISOString(),
|
|
367
|
+
type: m.type,
|
|
368
|
+
})));
|
|
369
|
+
},
|
|
370
|
+
}),
|
|
371
|
+
searchContact: tool({
|
|
372
|
+
description: "Search contacts by name or phone number",
|
|
373
|
+
inputSchema: z.object({
|
|
374
|
+
query: z.string().describe("Name or partial phone number"),
|
|
375
|
+
}),
|
|
376
|
+
execute: async ({ query }) => {
|
|
377
|
+
const contacts = await adapter.getContacts(query);
|
|
378
|
+
return JSON.stringify(contacts.map((c) => ({
|
|
379
|
+
jid: c.jid,
|
|
380
|
+
name: c.name ?? c.notifyName,
|
|
381
|
+
phone: c.phone,
|
|
382
|
+
isBusiness: c.isBusiness,
|
|
383
|
+
})));
|
|
384
|
+
},
|
|
385
|
+
}),
|
|
386
|
+
checkNumber: tool({
|
|
387
|
+
description: "Check if a phone number exists on WhatsApp",
|
|
388
|
+
inputSchema: z.object({
|
|
389
|
+
phone: z.string().describe("Phone number with country code"),
|
|
390
|
+
}),
|
|
391
|
+
execute: async ({ phone }) => {
|
|
392
|
+
const result = await adapter.checkNumberExists(phone);
|
|
393
|
+
return JSON.stringify(result);
|
|
394
|
+
},
|
|
395
|
+
}),
|
|
396
|
+
blockContact: tool({
|
|
397
|
+
description: "Block a contact by JID",
|
|
398
|
+
inputSchema: z.object({ jid: z.string() }),
|
|
399
|
+
execute: async ({ jid }) => {
|
|
400
|
+
await adapter.blockContact(jid);
|
|
401
|
+
return "Contact blocked";
|
|
402
|
+
},
|
|
403
|
+
}),
|
|
404
|
+
unblockContact: tool({
|
|
405
|
+
description: "Unblock a contact by JID",
|
|
406
|
+
inputSchema: z.object({ jid: z.string() }),
|
|
407
|
+
execute: async ({ jid }) => {
|
|
408
|
+
await adapter.unblockContact(jid);
|
|
409
|
+
return "Contact unblocked";
|
|
410
|
+
},
|
|
411
|
+
}),
|
|
412
|
+
createGroup: tool({
|
|
413
|
+
description: "Create a new WhatsApp group",
|
|
414
|
+
inputSchema: z.object({
|
|
415
|
+
name: z.string(),
|
|
416
|
+
participants: z.array(z.string()).describe("Array of phone numbers or JIDs to add"),
|
|
417
|
+
}),
|
|
418
|
+
execute: async ({ name, participants }) => {
|
|
419
|
+
const res = await adapter.createGroup(name, participants);
|
|
420
|
+
return `Group created. ID: ${res.groupId}`;
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
groupAddParticipants: tool({
|
|
424
|
+
description: "Add participants to a group",
|
|
425
|
+
inputSchema: z.object({
|
|
426
|
+
groupId: z.string(),
|
|
427
|
+
participants: z.array(z.string()),
|
|
428
|
+
}),
|
|
429
|
+
execute: async ({ groupId, participants }) => {
|
|
430
|
+
await adapter.modifyParticipants(groupId, participants, "add");
|
|
431
|
+
return "Participants added";
|
|
432
|
+
},
|
|
433
|
+
}),
|
|
434
|
+
groupRemoveParticipants: tool({
|
|
435
|
+
description: "Remove participants from a group",
|
|
436
|
+
inputSchema: z.object({
|
|
437
|
+
groupId: z.string(),
|
|
438
|
+
participants: z.array(z.string()),
|
|
439
|
+
}),
|
|
440
|
+
execute: async ({ groupId, participants }) => {
|
|
441
|
+
await adapter.modifyParticipants(groupId, participants, "remove");
|
|
442
|
+
return "Participants removed";
|
|
443
|
+
},
|
|
444
|
+
}),
|
|
445
|
+
groupPromote: tool({
|
|
446
|
+
description: "Promote participants to admin in a group",
|
|
447
|
+
inputSchema: z.object({
|
|
448
|
+
groupId: z.string(),
|
|
449
|
+
participants: z.array(z.string()),
|
|
450
|
+
}),
|
|
451
|
+
execute: async ({ groupId, participants }) => {
|
|
452
|
+
await adapter.modifyParticipants(groupId, participants, "promote");
|
|
453
|
+
return "Participants promoted";
|
|
454
|
+
},
|
|
455
|
+
}),
|
|
456
|
+
groupDemote: tool({
|
|
457
|
+
description: "Demote participants from admin in a group",
|
|
458
|
+
inputSchema: z.object({
|
|
459
|
+
groupId: z.string(),
|
|
460
|
+
participants: z.array(z.string()),
|
|
461
|
+
}),
|
|
462
|
+
execute: async ({ groupId, participants }) => {
|
|
463
|
+
await adapter.modifyParticipants(groupId, participants, "demote");
|
|
464
|
+
return "Participants demoted";
|
|
465
|
+
},
|
|
466
|
+
}),
|
|
467
|
+
groupUpdateSubject: tool({
|
|
468
|
+
description: "Change group name/subject",
|
|
469
|
+
inputSchema: z.object({
|
|
470
|
+
groupId: z.string(),
|
|
471
|
+
subject: z.string(),
|
|
472
|
+
}),
|
|
473
|
+
execute: async ({ groupId, subject }) => {
|
|
474
|
+
await adapter.modifyGroup(groupId, { action: "updateSubject", value: subject });
|
|
475
|
+
return "Group name updated";
|
|
476
|
+
},
|
|
477
|
+
}),
|
|
478
|
+
groupUpdateDescription: tool({
|
|
479
|
+
description: "Change group description",
|
|
480
|
+
inputSchema: z.object({
|
|
481
|
+
groupId: z.string(),
|
|
482
|
+
description: z.string(),
|
|
483
|
+
}),
|
|
484
|
+
execute: async ({ groupId, description }) => {
|
|
485
|
+
await adapter.modifyGroup(groupId, { action: "updateDescription", value: description });
|
|
486
|
+
return "Group description updated";
|
|
487
|
+
},
|
|
488
|
+
}),
|
|
489
|
+
groupUpdateSettings: tool({
|
|
490
|
+
description: "Toggle between 'announcement' (admins only) and 'all' (everyone) for group messaging",
|
|
491
|
+
inputSchema: z.object({
|
|
492
|
+
groupId: z.string(),
|
|
493
|
+
mode: z.enum(["announcement", "all"]),
|
|
494
|
+
}),
|
|
495
|
+
execute: async ({ groupId, mode }) => {
|
|
496
|
+
await adapter.modifyGroup(groupId, { action: "updateSettings", value: mode });
|
|
497
|
+
return `Group settings updated to ${mode}`;
|
|
498
|
+
},
|
|
499
|
+
}),
|
|
500
|
+
groupLeave: tool({
|
|
501
|
+
description: "Leave a WhatsApp group",
|
|
502
|
+
inputSchema: z.object({ groupId: z.string() }),
|
|
503
|
+
execute: async ({ groupId }) => {
|
|
504
|
+
await adapter.modifyGroup(groupId, { action: "leave" });
|
|
505
|
+
return "Left the group";
|
|
506
|
+
},
|
|
507
|
+
}),
|
|
508
|
+
groupGetInviteCode: tool({
|
|
509
|
+
description: "Get the invite link/code for a group",
|
|
510
|
+
inputSchema: z.object({ groupId: z.string() }),
|
|
511
|
+
execute: async ({ groupId }) => {
|
|
512
|
+
const code = await adapter.getGroupInviteCode(groupId);
|
|
513
|
+
return `Invite code: ${code}`;
|
|
514
|
+
},
|
|
515
|
+
}),
|
|
516
|
+
groupJoin: tool({
|
|
517
|
+
description: "Join a group using an invite code",
|
|
518
|
+
inputSchema: z.object({ inviteCode: z.string() }),
|
|
519
|
+
execute: async ({ inviteCode }) => {
|
|
520
|
+
const gid = await adapter.joinGroup(inviteCode);
|
|
521
|
+
return `Joined group: ${gid}`;
|
|
522
|
+
},
|
|
523
|
+
}),
|
|
524
|
+
getGroupMetadata: tool({
|
|
525
|
+
description: "Get detailed metadata about a group (members, admins, settings)",
|
|
526
|
+
inputSchema: z.object({ groupId: z.string() }),
|
|
527
|
+
execute: async ({ groupId }) => {
|
|
528
|
+
const meta = await adapter.getGroupMetadata(groupId);
|
|
529
|
+
return JSON.stringify({
|
|
530
|
+
subject: meta.subject,
|
|
531
|
+
description: meta.description,
|
|
532
|
+
participantCount: meta.participantCount,
|
|
533
|
+
isAnnounce: meta.isAnnounce,
|
|
534
|
+
admins: meta.participants.filter((p) => p.isAdmin).map((p) => p.jid),
|
|
535
|
+
});
|
|
536
|
+
},
|
|
537
|
+
}),
|
|
538
|
+
updateProfileName: tool({
|
|
539
|
+
description: "Change the WhatsApp profile/display name",
|
|
540
|
+
inputSchema: z.object({ name: z.string() }),
|
|
541
|
+
execute: async ({ name }) => {
|
|
542
|
+
await adapter.updateProfileName(name);
|
|
543
|
+
return `Profile name changed to: ${name}`;
|
|
544
|
+
},
|
|
545
|
+
}),
|
|
546
|
+
updateProfileStatus: tool({
|
|
547
|
+
description: "Change the WhatsApp profile status/about text",
|
|
548
|
+
inputSchema: z.object({ status: z.string() }),
|
|
549
|
+
execute: async ({ status }) => {
|
|
550
|
+
await adapter.updateProfileStatus(status);
|
|
551
|
+
return "Profile status updated";
|
|
552
|
+
},
|
|
553
|
+
}),
|
|
554
|
+
getProfileInfo: tool({
|
|
555
|
+
description: "Get the logged-in user's profile info",
|
|
556
|
+
inputSchema: z.object({}),
|
|
557
|
+
execute: async () => {
|
|
558
|
+
const info = await adapter.getProfileInfo();
|
|
559
|
+
return JSON.stringify(info);
|
|
560
|
+
},
|
|
561
|
+
}),
|
|
562
|
+
sendTextStatus: tool({
|
|
563
|
+
description: "Post a text status/story update",
|
|
564
|
+
inputSchema: z.object({
|
|
565
|
+
text: z.string(),
|
|
566
|
+
backgroundColor: z.string().optional().default("#000000"),
|
|
567
|
+
font: z.number().optional().default(1),
|
|
568
|
+
}),
|
|
569
|
+
execute: async ({ text, backgroundColor, font }) => {
|
|
570
|
+
await adapter.sendStatus({ type: "text", text, backgroundColor, font });
|
|
571
|
+
return "Status posted";
|
|
572
|
+
},
|
|
573
|
+
}),
|
|
574
|
+
sendImageStatus: tool({
|
|
575
|
+
description: "Post an image as status/story update",
|
|
576
|
+
inputSchema: z.object({
|
|
577
|
+
imageUrl: z.string().url(),
|
|
578
|
+
caption: z.string().optional(),
|
|
579
|
+
}),
|
|
580
|
+
execute: async ({ imageUrl, caption }) => {
|
|
581
|
+
await adapter.sendStatus({ type: "image", media: imageUrl, caption });
|
|
582
|
+
return "Image status posted";
|
|
583
|
+
},
|
|
584
|
+
}),
|
|
585
|
+
archiveChat: tool({
|
|
586
|
+
description: "Archive or unarchive a chat",
|
|
587
|
+
inputSchema: z.object({
|
|
588
|
+
chatId: z.string(),
|
|
589
|
+
archive: z.boolean(),
|
|
590
|
+
}),
|
|
591
|
+
execute: async ({ chatId, archive }) => {
|
|
592
|
+
await adapter.modifyChat(chatId, { action: archive ? "archive" : "unarchive" });
|
|
593
|
+
return archive ? "Chat archived" : "Chat unarchived";
|
|
594
|
+
},
|
|
595
|
+
}),
|
|
596
|
+
pinChat: tool({
|
|
597
|
+
description: "Pin or unpin a chat",
|
|
598
|
+
inputSchema: z.object({
|
|
599
|
+
chatId: z.string(),
|
|
600
|
+
pin: z.boolean(),
|
|
601
|
+
}),
|
|
602
|
+
execute: async ({ chatId, pin }) => {
|
|
603
|
+
await adapter.modifyChat(chatId, { action: pin ? "pin" : "unpin" });
|
|
604
|
+
return pin ? "Chat pinned" : "Chat unpinned";
|
|
605
|
+
},
|
|
606
|
+
}),
|
|
607
|
+
muteChat: tool({
|
|
608
|
+
description: "Mute or unmute a chat",
|
|
609
|
+
inputSchema: z.object({
|
|
610
|
+
chatId: z.string(),
|
|
611
|
+
mute: z.boolean(),
|
|
612
|
+
}),
|
|
613
|
+
execute: async ({ chatId, mute }) => {
|
|
614
|
+
await adapter.modifyChat(chatId, { action: mute ? "mute" : "unmute" });
|
|
615
|
+
return mute ? "Chat muted" : "Chat unmuted";
|
|
616
|
+
},
|
|
617
|
+
}),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const SYSTEM_PROMPT = `You are a WhatsApp assistant called "wagent". You have full access to all WhatsApp features.
|
|
621
|
+
|
|
622
|
+
CAPABILITIES:
|
|
623
|
+
- Send and receive text, images, videos, audio, documents, locations, contacts, polls
|
|
624
|
+
- Create and manage groups (add/remove/promote/demote members, change settings)
|
|
625
|
+
- Search contacts, check if numbers are on WhatsApp
|
|
626
|
+
- Forward, edit, delete, pin messages
|
|
627
|
+
- React to messages with emojis
|
|
628
|
+
- Show typing indicators and mark messages as read
|
|
629
|
+
- Manage profile (name, status)
|
|
630
|
+
- Post status/story updates
|
|
631
|
+
- Archive, pin, mute chats
|
|
632
|
+
|
|
633
|
+
CRITICAL RULES:
|
|
634
|
+
- Your response TEXT is automatically sent back to the conversation. NEVER call sendText/sendImage/etc to reply in the current conversation. Only use send tools to message OTHER people.
|
|
635
|
+
- Send tools accept contact names, phone numbers, or JIDs. If the user says "message Bharghav", call sendText with to="Bharghav"; the app resolves it safely.
|
|
636
|
+
- Use searchContact only when the user asks to look up contacts or when you need to disambiguate before answering.
|
|
637
|
+
|
|
638
|
+
BEHAVIOR:
|
|
639
|
+
- When the sender is YOU (self-message): treat it as a command. Execute the requested actions.
|
|
640
|
+
- When the sender is someone else: respond helpfully based on context.
|
|
641
|
+
- Unless instructed "silently", summarize what you did in a brief text response.
|
|
642
|
+
- You can chain multiple tool calls in sequence.
|
|
643
|
+
- Keep responses concise. Be direct and practical.`;
|
|
644
|
+
function getSystemPrompt(config) {
|
|
645
|
+
const identity = config.agent.systemPrompt ? `\n\nAGENT-WRITTEN IDENTITY:\n${config.agent.systemPrompt}` : "";
|
|
646
|
+
const memory = config.agent.memorySummary ? `\n\nCOMPACTED MEMORY:\n${config.agent.memorySummary}` : "";
|
|
647
|
+
return `${SYSTEM_PROMPT}${identity}${memory}`;
|
|
648
|
+
}
|
|
649
|
+
function hasMentionBypass(text, config) {
|
|
650
|
+
return text.toLowerCase().includes(config.agent.policy.mentionBypass.toLowerCase());
|
|
651
|
+
}
|
|
652
|
+
function isListed(value, list) {
|
|
653
|
+
const lower = value.toLowerCase();
|
|
654
|
+
return list.some((item) => lower.includes(item.toLowerCase()) || item.toLowerCase() === lower);
|
|
655
|
+
}
|
|
656
|
+
function messageCanReachAgent(config, event, isSelf, text) {
|
|
657
|
+
if (hasMentionBypass(text, config))
|
|
658
|
+
return true;
|
|
659
|
+
if (isSelf)
|
|
660
|
+
return true;
|
|
661
|
+
if (config.agent.mode === "bootstrap")
|
|
662
|
+
return false;
|
|
663
|
+
if (!config.agent.policy.autoReplyEnabled)
|
|
664
|
+
return false;
|
|
665
|
+
const isGroup = event.chatId.endsWith("@g.us");
|
|
666
|
+
if (isGroup && !config.agent.policy.allowGroups && !config.agent.policy.whitelistedGroups.includes(event.chatId))
|
|
667
|
+
return false;
|
|
668
|
+
const sender = event.message.sender;
|
|
669
|
+
if (isListed(sender, config.agent.policy.blacklistedContacts))
|
|
670
|
+
return false;
|
|
671
|
+
if (config.agent.policy.readPolicy === "everyone_except_blacklist")
|
|
672
|
+
return true;
|
|
673
|
+
if (config.agent.policy.readPolicy === "whitelist")
|
|
674
|
+
return isListed(sender, config.agent.policy.whitelistedContacts);
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
function isConfirmation(text) {
|
|
678
|
+
return /\b(confirm|confirmed|approve|approved|yes|ship it|looks good)\b/i.test(text);
|
|
679
|
+
}
|
|
680
|
+
async function sendTracked(adapter, to, text) {
|
|
681
|
+
const res = await adapter.sendMessage(to, { type: "text", text });
|
|
682
|
+
rememberAgentMessage(res.messageId);
|
|
683
|
+
}
|
|
684
|
+
async function startBootstrap(adapter, config, myJid) {
|
|
685
|
+
if (config.agent.bootstrapStarted || config.agent.mode !== "bootstrap")
|
|
686
|
+
return config;
|
|
687
|
+
await sendTracked(adapter, myJid, "I'm online. I don't know who I am yet.");
|
|
688
|
+
await sendTracked(adapter, myJid, "Tell me my purpose, your relationship to me, my vibe, and who I can read or reply to.");
|
|
689
|
+
await sendTracked(adapter, myJid, "I can message contacts, manage WhatsApp tools, remember context, compact memory, and enforce read/reply permissions.");
|
|
690
|
+
config.agent.bootstrapStarted = true;
|
|
691
|
+
saveConfig(config);
|
|
692
|
+
return config;
|
|
693
|
+
}
|
|
694
|
+
async function handleBootstrapMessage(adapter, config, chatId, text) {
|
|
695
|
+
if (config.agent.pendingSystemPrompt && isConfirmation(text)) {
|
|
696
|
+
config.agent.systemPrompt = config.agent.pendingSystemPrompt;
|
|
697
|
+
config.agent.pendingSystemPrompt = undefined;
|
|
698
|
+
config.agent.memorySummary = config.agent.bootstrapNotes.join("\n").slice(-6000);
|
|
699
|
+
config.agent.mode = "configured";
|
|
700
|
+
saveConfig(config);
|
|
701
|
+
await sendTracked(adapter, chatId, "Confirmed. I compacted setup context and will follow this system prompt from now on.");
|
|
702
|
+
return config;
|
|
703
|
+
}
|
|
704
|
+
config.agent.bootstrapNotes.push(text);
|
|
705
|
+
const draft = await generateWithFallback(config, {
|
|
706
|
+
system: "Draft a concise first-person system prompt for a WhatsApp agent from the user's setup notes. Include identity, purpose, vibe, read/reply boundaries, and tool behavior. Return only the prompt.",
|
|
707
|
+
messages: [{ role: "user", content: config.agent.bootstrapNotes.join("\n\n") }],
|
|
708
|
+
temperature: 0.3,
|
|
709
|
+
});
|
|
710
|
+
config.agent.pendingSystemPrompt = draft.text.trim();
|
|
711
|
+
saveConfig(config);
|
|
712
|
+
await sendTracked(adapter, chatId, `Here's my proposed system prompt:\n\n${config.agent.pendingSystemPrompt}\n\nReply confirm if this is right, or tell me what to change.`);
|
|
713
|
+
return config;
|
|
714
|
+
}
|
|
715
|
+
async function processMessage(adapter, config, instId, event, text, isSelf, senderName, chatName, tools) {
|
|
716
|
+
const { chatId } = event;
|
|
717
|
+
const chatKey = `${instId}:${chatId}`;
|
|
718
|
+
const history = chatHistories.get(chatKey) ?? [];
|
|
719
|
+
const roleInfo = isSelf
|
|
720
|
+
? `[COMMAND from ${senderName ?? "you"} (self-message) | chat: ${chatId}]`
|
|
721
|
+
: `[MESSAGE from ${senderName ?? event.message.sender} in ${chatName ?? chatId} | chat: ${chatId}]`;
|
|
722
|
+
const userMsg = { role: "user", content: `${roleInfo}\n\n${text}` };
|
|
723
|
+
const messages = [
|
|
724
|
+
...history.slice(-MAX_HISTORY),
|
|
725
|
+
userMsg,
|
|
726
|
+
];
|
|
727
|
+
try {
|
|
728
|
+
logger.info({ chatId, isSelf, text: text.substring(0, 80) }, "Processing with AI");
|
|
729
|
+
const result = await generateWithFallback(config, {
|
|
730
|
+
system: getSystemPrompt(config),
|
|
731
|
+
messages,
|
|
732
|
+
tools,
|
|
733
|
+
temperature: 0.7,
|
|
734
|
+
experimental_context: { currentChatId: chatId, config },
|
|
735
|
+
});
|
|
736
|
+
const responseText = result.text?.trim();
|
|
737
|
+
const allToolCalls = result.steps?.flatMap((s) => s.toolCalls ?? []) ?? [];
|
|
738
|
+
if (allToolCalls.length > 0 || responseText) {
|
|
739
|
+
history.push(userMsg);
|
|
740
|
+
history.push({ role: "assistant", content: responseText || "(done)" });
|
|
741
|
+
chatHistories.set(chatKey, history);
|
|
742
|
+
}
|
|
743
|
+
if (responseText) {
|
|
744
|
+
await adapter.sendPresence(chatId, "composing");
|
|
745
|
+
await delayBeforeReply(config);
|
|
746
|
+
const content = { type: "text", text: responseText };
|
|
747
|
+
const res = await adapter.sendMessage(chatId, content);
|
|
748
|
+
rememberAgentMessage(res.messageId);
|
|
749
|
+
await adapter.sendPresence(chatId, "paused");
|
|
750
|
+
logger.info({ chatId, response: responseText.substring(0, 80) }, "Response sent");
|
|
751
|
+
}
|
|
752
|
+
else if (allToolCalls.length > 0) {
|
|
753
|
+
logger.info({ chatId, toolCount: allToolCalls.length }, "Tools executed silently");
|
|
754
|
+
const res = await adapter.sendMessage(chatId, { type: "text", text: "Done." });
|
|
755
|
+
rememberAgentMessage(res.messageId);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
catch (err) {
|
|
759
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
760
|
+
logger.error({ err, chatId }, "Agent error");
|
|
761
|
+
try {
|
|
762
|
+
const fallback = "Sorry, I hit an error processing that. Please try again.";
|
|
763
|
+
const res = await adapter.sendMessage(chatId, { type: "text", text: fallback });
|
|
764
|
+
rememberAgentMessage(res.messageId);
|
|
765
|
+
}
|
|
766
|
+
catch (_) { }
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async function main() {
|
|
770
|
+
const args = process.argv.slice(2);
|
|
771
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
772
|
+
console.log(`wagent\n\nUsage:\n wagent Start agent (pairs WhatsApp first if needed)\n wagent start Start agent\n wagent setup Re-run AI provider setup after WhatsApp pairing\n wagent config Show config path and provider\n wagent --verbose Show lifecycle logs\n wagent --debug Show debug logs\n`);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
let config = loadConfig();
|
|
776
|
+
if (args[0] === "config") {
|
|
777
|
+
const { getConfigPath } = await import("./config.js");
|
|
778
|
+
console.log(JSON.stringify({ path: getConfigPath(), provider: config.provider, model: config.model, configured: Boolean(config.apiKey), agentMode: config.agent.mode }, null, 2));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (args[0] === "setup") {
|
|
782
|
+
config.apiKey = "";
|
|
783
|
+
saveConfig(config);
|
|
784
|
+
}
|
|
785
|
+
console.log("wagent starting...");
|
|
786
|
+
const instanceManager = new InstanceManager();
|
|
787
|
+
let instances = instanceManager.getAllInstances();
|
|
788
|
+
let instanceId;
|
|
789
|
+
if (instances.length === 0) {
|
|
790
|
+
const inst = await instanceManager.createInstance("wagent", "baileys");
|
|
791
|
+
instanceId = inst.id;
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
instanceId = instances[0].id;
|
|
795
|
+
}
|
|
796
|
+
let myJid = null;
|
|
797
|
+
instanceManager.onAnyEvent((event, instId, payload) => {
|
|
798
|
+
if (event === "connection.changed" && instId === instanceId) {
|
|
799
|
+
const conn = payload;
|
|
800
|
+
if (conn.qrCode) {
|
|
801
|
+
console.log("\nScan this QR in WhatsApp > Linked Devices:");
|
|
802
|
+
qrcode.generate(conn.qrCode, { small: true });
|
|
803
|
+
}
|
|
804
|
+
if (conn.status === "open") {
|
|
805
|
+
const adapter = instanceManager.getAdapter(instId);
|
|
806
|
+
myJid = getOwnJid(adapter);
|
|
807
|
+
console.log(`\nWhatsApp connected as ${myJid}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
await instanceManager.connectInstance(instanceId);
|
|
812
|
+
const adapter = instanceManager.getAdapter(instanceId);
|
|
813
|
+
for (let i = 0; i < 60; i++) {
|
|
814
|
+
myJid = getOwnJid(adapter);
|
|
815
|
+
if (myJid)
|
|
816
|
+
break;
|
|
817
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
818
|
+
}
|
|
819
|
+
if (!myJid) {
|
|
820
|
+
console.error("WhatsApp did not connect. Check QR/session and try again.");
|
|
821
|
+
process.exit(1);
|
|
822
|
+
}
|
|
823
|
+
config = await ensureAiConfig(config);
|
|
824
|
+
const tools = buildTools(adapter);
|
|
825
|
+
config = await startBootstrap(adapter, config, myJid);
|
|
826
|
+
console.log("Agent ready. Message yourself to configure or command it.");
|
|
827
|
+
instanceManager.onAnyEvent(async (event, instId, payload) => {
|
|
828
|
+
if (event === "presence.updated" && instId === instanceId) {
|
|
829
|
+
const presence = payload;
|
|
830
|
+
if (presence.status === "composing" || presence.status === "recording") {
|
|
831
|
+
typingUntil.set(presence.chatId, Date.now() + 30_000);
|
|
832
|
+
}
|
|
833
|
+
else if (presence.status === "paused" || presence.status === "unavailable") {
|
|
834
|
+
typingUntil.delete(presence.chatId);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (event === "message.received" && instId === instanceId) {
|
|
838
|
+
const msg = payload;
|
|
839
|
+
const text = msg.message.content;
|
|
840
|
+
if (!text)
|
|
841
|
+
return;
|
|
842
|
+
if (agentSentMessages.delete(msg.message.id))
|
|
843
|
+
return;
|
|
844
|
+
const dedupKey = `${msg.chatId}:${msg.message.id}:${msg.message.timestamp}`;
|
|
845
|
+
if (processedMessages.has(dedupKey))
|
|
846
|
+
return;
|
|
847
|
+
processedMessages.add(dedupKey);
|
|
848
|
+
if (processedMessages.size > 1000) {
|
|
849
|
+
const toDelete = [...processedMessages].slice(0, 500);
|
|
850
|
+
for (const k of toDelete)
|
|
851
|
+
processedMessages.delete(k);
|
|
852
|
+
}
|
|
853
|
+
const isSelf = msg.message.sender === myJid || msg.message.isFromMe;
|
|
854
|
+
if (!messageCanReachAgent(config, msg, isSelf, text))
|
|
855
|
+
return;
|
|
856
|
+
await waitForTypingToStop(msg.chatId);
|
|
857
|
+
if (config.agent.mode === "bootstrap" && isSelf) {
|
|
858
|
+
config = await handleBootstrapMessage(adapter, config, msg.chatId, text);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const senderName = await getContactName(adapter, msg.message.sender);
|
|
862
|
+
const chatName = msg.chatId.endsWith("@g.us")
|
|
863
|
+
? await getGroupName(adapter, msg.chatId)
|
|
864
|
+
: senderName;
|
|
865
|
+
processMessage(adapter, config, instanceId, msg, text, isSelf, senderName, chatName, tools)
|
|
866
|
+
.catch((err) => logger.error({ err }, "processMessage error"));
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
main().catch((err) => {
|
|
871
|
+
console.error("Fatal error:", err);
|
|
872
|
+
process.exit(1);
|
|
873
|
+
});
|