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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// WA MCP — Baileys SQLite Auth State
|
|
3
|
+
// Implements Baileys' AuthenticationState using SQLite via
|
|
4
|
+
// Drizzle ORM, persisting Signal protocol keys in auth_keys.
|
|
5
|
+
// ============================================================
|
|
6
|
+
import { proto } from "@whiskeysockets/baileys";
|
|
7
|
+
import { initAuthCreds, BufferJSON } from "@whiskeysockets/baileys";
|
|
8
|
+
import { eq, and } from "drizzle-orm";
|
|
9
|
+
import { db } from "../../db/client.js";
|
|
10
|
+
import { authKeys } from "../../db/schema.js";
|
|
11
|
+
const CREDS_KEY = "creds";
|
|
12
|
+
/**
|
|
13
|
+
* Creates a Baileys-compatible AuthenticationState backed by SQLite.
|
|
14
|
+
* Equivalent to useMultiFileAuthState but stores everything in the auth_keys table.
|
|
15
|
+
*/
|
|
16
|
+
export async function useSqliteAuthState(instanceId) {
|
|
17
|
+
// ---- Helpers ----
|
|
18
|
+
const readData = async (key) => {
|
|
19
|
+
const row = db
|
|
20
|
+
.select({ keyData: authKeys.keyData })
|
|
21
|
+
.from(authKeys)
|
|
22
|
+
.where(and(eq(authKeys.instanceId, instanceId), eq(authKeys.keyId, key)))
|
|
23
|
+
.get();
|
|
24
|
+
return row?.keyData ?? null;
|
|
25
|
+
};
|
|
26
|
+
const writeData = async (key, data) => {
|
|
27
|
+
db.insert(authKeys)
|
|
28
|
+
.values({ instanceId, keyId: key, keyData: data })
|
|
29
|
+
.onConflictDoUpdate({
|
|
30
|
+
target: [authKeys.instanceId, authKeys.keyId],
|
|
31
|
+
set: { keyData: data },
|
|
32
|
+
})
|
|
33
|
+
.run();
|
|
34
|
+
};
|
|
35
|
+
const removeData = async (key) => {
|
|
36
|
+
db.delete(authKeys)
|
|
37
|
+
.where(and(eq(authKeys.instanceId, instanceId), eq(authKeys.keyId, key)))
|
|
38
|
+
.run();
|
|
39
|
+
};
|
|
40
|
+
// ---- Load or initialize creds ----
|
|
41
|
+
let creds;
|
|
42
|
+
const credsJson = await readData(CREDS_KEY);
|
|
43
|
+
if (credsJson) {
|
|
44
|
+
creds = JSON.parse(credsJson, BufferJSON.reviver);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
creds = initAuthCreds();
|
|
48
|
+
await writeData(CREDS_KEY, JSON.stringify(creds, BufferJSON.replacer));
|
|
49
|
+
}
|
|
50
|
+
// ---- Build keys interface ----
|
|
51
|
+
const state = {
|
|
52
|
+
creds,
|
|
53
|
+
keys: {
|
|
54
|
+
get: async (type, ids) => {
|
|
55
|
+
const result = {};
|
|
56
|
+
for (const id of ids) {
|
|
57
|
+
const keyId = `${type}-${id}`;
|
|
58
|
+
const raw = await readData(keyId);
|
|
59
|
+
if (raw) {
|
|
60
|
+
let parsed = JSON.parse(raw, BufferJSON.reviver);
|
|
61
|
+
if (type === "app-state-sync-key" && parsed) {
|
|
62
|
+
parsed = proto.Message.AppStateSyncKeyData.create(parsed);
|
|
63
|
+
}
|
|
64
|
+
result[id] = parsed;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
},
|
|
69
|
+
set: async (data) => {
|
|
70
|
+
for (const [type, entries] of Object.entries(data)) {
|
|
71
|
+
for (const [id, value] of Object.entries(entries)) {
|
|
72
|
+
const keyId = `${type}-${id}`;
|
|
73
|
+
if (value) {
|
|
74
|
+
await writeData(keyId, JSON.stringify(value, BufferJSON.replacer));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
await removeData(keyId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
state,
|
|
86
|
+
saveCreds: async () => {
|
|
87
|
+
await writeData(CREDS_KEY, JSON.stringify(creds, BufferJSON.replacer));
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Clears all auth state for an instance (used after logout).
|
|
93
|
+
*/
|
|
94
|
+
export async function clearAuthState(instanceId) {
|
|
95
|
+
db.delete(authKeys).where(eq(authKeys.instanceId, instanceId)).run();
|
|
96
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// WA MCP — Baileys Event Normalization
|
|
3
|
+
// Maps raw Baileys events to normalized ChannelEvent types.
|
|
4
|
+
// ============================================================
|
|
5
|
+
/**
|
|
6
|
+
* Extract the message type from a Baileys WAMessage.
|
|
7
|
+
*/
|
|
8
|
+
export function extractMessageType(msg) {
|
|
9
|
+
const m = msg.message;
|
|
10
|
+
if (!m)
|
|
11
|
+
return "text";
|
|
12
|
+
if (m.conversation || m.extendedTextMessage)
|
|
13
|
+
return "text";
|
|
14
|
+
if (m.imageMessage ||
|
|
15
|
+
m.viewOnceMessage?.message?.imageMessage ||
|
|
16
|
+
m.viewOnceMessageV2?.message?.imageMessage)
|
|
17
|
+
return "image";
|
|
18
|
+
if (m.videoMessage ||
|
|
19
|
+
m.viewOnceMessage?.message?.videoMessage ||
|
|
20
|
+
m.viewOnceMessageV2?.message?.videoMessage)
|
|
21
|
+
return "video";
|
|
22
|
+
if (m.audioMessage)
|
|
23
|
+
return "audio";
|
|
24
|
+
if (m.documentMessage || m.documentWithCaptionMessage)
|
|
25
|
+
return "document";
|
|
26
|
+
if (m.locationMessage || m.liveLocationMessage)
|
|
27
|
+
return "location";
|
|
28
|
+
if (m.contactMessage || m.contactsArrayMessage)
|
|
29
|
+
return "contact";
|
|
30
|
+
if (m.pollCreationMessage || m.pollCreationMessageV2 || m.pollCreationMessageV3)
|
|
31
|
+
return "poll";
|
|
32
|
+
if (m.reactionMessage)
|
|
33
|
+
return "reaction";
|
|
34
|
+
if (m.stickerMessage)
|
|
35
|
+
return "sticker";
|
|
36
|
+
return "text";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Extract text content from a Baileys WAMessage.
|
|
40
|
+
*/
|
|
41
|
+
export function extractTextContent(msg) {
|
|
42
|
+
const m = msg.message;
|
|
43
|
+
if (!m)
|
|
44
|
+
return null;
|
|
45
|
+
if (m.conversation)
|
|
46
|
+
return m.conversation;
|
|
47
|
+
if (m.extendedTextMessage?.text)
|
|
48
|
+
return m.extendedTextMessage.text;
|
|
49
|
+
if (m.imageMessage?.caption)
|
|
50
|
+
return m.imageMessage.caption;
|
|
51
|
+
if (m.videoMessage?.caption)
|
|
52
|
+
return m.videoMessage.caption;
|
|
53
|
+
if (m.documentMessage?.caption)
|
|
54
|
+
return m.documentMessage.caption;
|
|
55
|
+
if (m.documentWithCaptionMessage?.message?.documentMessage?.caption) {
|
|
56
|
+
return m.documentWithCaptionMessage.message.documentMessage.caption;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Extract quoted message ID from a Baileys WAMessage.
|
|
62
|
+
*/
|
|
63
|
+
export function extractQuotedMessageId(msg) {
|
|
64
|
+
const ctx = msg.message?.extendedTextMessage?.contextInfo ??
|
|
65
|
+
msg.message?.imageMessage?.contextInfo ??
|
|
66
|
+
msg.message?.videoMessage?.contextInfo ??
|
|
67
|
+
msg.message?.audioMessage?.contextInfo ??
|
|
68
|
+
msg.message?.documentMessage?.contextInfo;
|
|
69
|
+
return ctx?.stanzaId ?? null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Swap LID/PN in message keys: when remoteJid is a LID and remoteJidAlt has the PN, use the PN.
|
|
73
|
+
* This follows the Evolution API pattern for Baileys v7.
|
|
74
|
+
*/
|
|
75
|
+
export function normalizeLidInMessage(msg) {
|
|
76
|
+
const key = msg.key;
|
|
77
|
+
if (key.remoteJid &&
|
|
78
|
+
typeof key.remoteJid === "string" &&
|
|
79
|
+
key.remoteJid.endsWith("@lid") &&
|
|
80
|
+
key.remoteJidAlt) {
|
|
81
|
+
const lid = key.remoteJid;
|
|
82
|
+
key.remoteJid = key.remoteJidAlt;
|
|
83
|
+
key.remoteJidAlt = lid;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get the chat JID for a message (group or individual).
|
|
88
|
+
*/
|
|
89
|
+
export function getChatJid(msg) {
|
|
90
|
+
return msg.key.remoteJid ?? "";
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the sender JID for a message.
|
|
94
|
+
*/
|
|
95
|
+
export function getSenderJid(msg) {
|
|
96
|
+
// In group chats, participant holds the actual sender
|
|
97
|
+
if (msg.key.participant)
|
|
98
|
+
return msg.key.participant;
|
|
99
|
+
// In individual chats, remoteJid is the sender (if not from me)
|
|
100
|
+
if (!msg.key.fromMe)
|
|
101
|
+
return msg.key.remoteJid ?? "";
|
|
102
|
+
return "me";
|
|
103
|
+
}
|
|
104
|
+
// ---- Normalizers ----
|
|
105
|
+
/**
|
|
106
|
+
* Normalize a Baileys messages.upsert event to NormalizedMessageEvent[].
|
|
107
|
+
*/
|
|
108
|
+
export function normalizeMessagesUpsert(instanceId, data) {
|
|
109
|
+
// Only process notify-type messages (not history sync)
|
|
110
|
+
if (data.type !== "notify")
|
|
111
|
+
return [];
|
|
112
|
+
return data.messages.map((msg) => {
|
|
113
|
+
normalizeLidInMessage(msg);
|
|
114
|
+
return {
|
|
115
|
+
instanceId,
|
|
116
|
+
chatId: getChatJid(msg),
|
|
117
|
+
message: {
|
|
118
|
+
id: msg.key.id ?? "",
|
|
119
|
+
sender: getSenderJid(msg),
|
|
120
|
+
timestamp: typeof msg.messageTimestamp === "number"
|
|
121
|
+
? msg.messageTimestamp
|
|
122
|
+
: Number(msg.messageTimestamp ?? 0),
|
|
123
|
+
type: extractMessageType(msg),
|
|
124
|
+
content: extractTextContent(msg),
|
|
125
|
+
mediaUrl: null,
|
|
126
|
+
quotedMessageId: extractQuotedMessageId(msg),
|
|
127
|
+
isFromMe: msg.key.fromMe ?? false,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Normalize a Baileys messages.update event.
|
|
134
|
+
*/
|
|
135
|
+
export function normalizeMessagesUpdate(instanceId, updates) {
|
|
136
|
+
return updates
|
|
137
|
+
.filter((u) => u.update.status !== undefined)
|
|
138
|
+
.map((u) => {
|
|
139
|
+
const statusMap = {
|
|
140
|
+
0: "received",
|
|
141
|
+
1: "sent",
|
|
142
|
+
2: "delivered",
|
|
143
|
+
3: "read",
|
|
144
|
+
4: "played",
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
instanceId,
|
|
148
|
+
chatId: u.key.remoteJid ?? "",
|
|
149
|
+
messageId: u.key.id ?? "",
|
|
150
|
+
status: statusMap[u.update.status ?? 0] ?? "received",
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Normalize a Baileys messages.delete event.
|
|
156
|
+
*/
|
|
157
|
+
export function normalizeMessagesDelete(instanceId, data) {
|
|
158
|
+
if ("keys" in data) {
|
|
159
|
+
return data.keys.map((key) => ({
|
|
160
|
+
instanceId,
|
|
161
|
+
chatId: key.remoteJid ?? "",
|
|
162
|
+
messageId: key.id ?? "",
|
|
163
|
+
deletedBy: key.participant ?? key.remoteJid ?? "",
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Normalize a Baileys messages.reaction event.
|
|
170
|
+
*/
|
|
171
|
+
export function normalizeMessagesReaction(instanceId, reactions) {
|
|
172
|
+
return reactions.map((r) => ({
|
|
173
|
+
instanceId,
|
|
174
|
+
chatId: r.key.remoteJid ?? "",
|
|
175
|
+
messageId: r.key.id ?? "",
|
|
176
|
+
emoji: r.reaction?.text ?? "",
|
|
177
|
+
reactedBy: r.reaction?.key?.participant ?? r.reaction?.key?.remoteJid ?? "",
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Normalize a Baileys message edit event.
|
|
182
|
+
*/
|
|
183
|
+
export function normalizeMessageEdit(instanceId, msg, chatId) {
|
|
184
|
+
const editedContent = msg.message?.protocolMessage?.editedMessage?.conversation ??
|
|
185
|
+
msg.message?.protocolMessage?.editedMessage?.extendedTextMessage?.text ??
|
|
186
|
+
"";
|
|
187
|
+
return {
|
|
188
|
+
instanceId,
|
|
189
|
+
chatId,
|
|
190
|
+
messageId: msg.message?.protocolMessage?.key?.id ?? msg.key.id ?? "",
|
|
191
|
+
newContent: editedContent,
|
|
192
|
+
editedAt: typeof msg.messageTimestamp === "number"
|
|
193
|
+
? msg.messageTimestamp
|
|
194
|
+
: Number(msg.messageTimestamp ?? Date.now() / 1000),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Normalize a Baileys presence.update event.
|
|
199
|
+
*/
|
|
200
|
+
export function normalizePresenceUpdate(instanceId, data) {
|
|
201
|
+
const results = [];
|
|
202
|
+
const chatId = data.id;
|
|
203
|
+
for (const [participant, presence] of Object.entries(data.presences)) {
|
|
204
|
+
const statusMap = {
|
|
205
|
+
composing: "composing",
|
|
206
|
+
recording: "recording",
|
|
207
|
+
paused: "paused",
|
|
208
|
+
available: "available",
|
|
209
|
+
unavailable: "unavailable",
|
|
210
|
+
};
|
|
211
|
+
results.push({
|
|
212
|
+
instanceId,
|
|
213
|
+
chatId,
|
|
214
|
+
participant,
|
|
215
|
+
status: statusMap[presence.lastKnownPresence] ?? "unavailable",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return results;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Normalize a Baileys groups.update event.
|
|
222
|
+
*/
|
|
223
|
+
export function normalizeGroupsUpdate(instanceId, updates) {
|
|
224
|
+
return updates.map((update) => ({
|
|
225
|
+
instanceId,
|
|
226
|
+
groupId: update.id ?? "",
|
|
227
|
+
changes: { ...update },
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Normalize a Baileys group-participants.update event.
|
|
232
|
+
*/
|
|
233
|
+
export function normalizeGroupParticipantsUpdate(instanceId, data) {
|
|
234
|
+
const actionMap = {
|
|
235
|
+
add: "add",
|
|
236
|
+
remove: "remove",
|
|
237
|
+
promote: "promote",
|
|
238
|
+
demote: "demote",
|
|
239
|
+
};
|
|
240
|
+
return {
|
|
241
|
+
instanceId,
|
|
242
|
+
groupId: data.id,
|
|
243
|
+
action: actionMap[data.action] ?? "add",
|
|
244
|
+
participants: data.participants.map((p) => (typeof p === "string" ? p : p.id)),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Normalize a Baileys contacts.update event.
|
|
249
|
+
*/
|
|
250
|
+
export function normalizeContactsUpdate(instanceId, contacts) {
|
|
251
|
+
return contacts.map((contact) => ({
|
|
252
|
+
instanceId,
|
|
253
|
+
contactId: contact.id ?? "",
|
|
254
|
+
changes: { ...contact },
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Normalize a Baileys connection.update event.
|
|
259
|
+
*/
|
|
260
|
+
export function normalizeConnectionUpdate(instanceId, data) {
|
|
261
|
+
if (data.connection === undefined && data.qr === undefined)
|
|
262
|
+
return null;
|
|
263
|
+
const statusMap = {
|
|
264
|
+
open: "open",
|
|
265
|
+
close: "close",
|
|
266
|
+
connecting: "connecting",
|
|
267
|
+
};
|
|
268
|
+
return {
|
|
269
|
+
instanceId,
|
|
270
|
+
status: data.connection ? (statusMap[data.connection] ?? "connecting") : "connecting",
|
|
271
|
+
qrCode: data.qr,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Normalize a Baileys call event.
|
|
276
|
+
*/
|
|
277
|
+
export function normalizeCallEvent(instanceId, calls) {
|
|
278
|
+
return calls.map((call) => ({
|
|
279
|
+
instanceId,
|
|
280
|
+
callerId: call.from ?? "",
|
|
281
|
+
isVideo: call.isVideo ?? false,
|
|
282
|
+
callId: call.id ?? "",
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// WA MCP — Baileys WA Web Version Management
|
|
3
|
+
// Fetches and caches the latest WhatsApp Web version.
|
|
4
|
+
// Falls back to bundled version on failure.
|
|
5
|
+
// ============================================================
|
|
6
|
+
import { fetchLatestBaileysVersion } from "@whiskeysockets/baileys";
|
|
7
|
+
import { eq } from "drizzle-orm";
|
|
8
|
+
import { db } from "../../db/client.js";
|
|
9
|
+
import { waVersionCache } from "../../db/schema.js";
|
|
10
|
+
import { createChildLogger } from "../../utils/logger.js";
|
|
11
|
+
const logger = createChildLogger({ service: "baileys-version" });
|
|
12
|
+
// Cache duration: 24 hours
|
|
13
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
14
|
+
/**
|
|
15
|
+
* Get the WhatsApp Web version, using cached value when fresh.
|
|
16
|
+
* Falls back to bundled version on network failure.
|
|
17
|
+
*/
|
|
18
|
+
export async function getWaVersion() {
|
|
19
|
+
// Check cache first
|
|
20
|
+
const cached = db.select().from(waVersionCache).where(eq(waVersionCache.id, 1)).get();
|
|
21
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
22
|
+
return {
|
|
23
|
+
version: JSON.parse(cached.versionJson),
|
|
24
|
+
isLatest: cached.isLatest === 1,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Fetch from network
|
|
28
|
+
try {
|
|
29
|
+
const { version, isLatest } = await fetchLatestBaileysVersion();
|
|
30
|
+
// Upsert cache
|
|
31
|
+
db.insert(waVersionCache)
|
|
32
|
+
.values({
|
|
33
|
+
id: 1,
|
|
34
|
+
versionJson: JSON.stringify(version),
|
|
35
|
+
fetchedAt: Date.now(),
|
|
36
|
+
isLatest: isLatest ? 1 : 0,
|
|
37
|
+
})
|
|
38
|
+
.onConflictDoUpdate({
|
|
39
|
+
target: waVersionCache.id,
|
|
40
|
+
set: {
|
|
41
|
+
versionJson: JSON.stringify(version),
|
|
42
|
+
fetchedAt: Date.now(),
|
|
43
|
+
isLatest: isLatest ? 1 : 0,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
.run();
|
|
47
|
+
logger.info({ version, isLatest }, "Fetched latest WA Web version");
|
|
48
|
+
return { version, isLatest };
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
logger.warn({ err }, "Failed to fetch WA version, using cached/bundled");
|
|
52
|
+
// Return cached if available, even if stale
|
|
53
|
+
if (cached) {
|
|
54
|
+
return {
|
|
55
|
+
version: JSON.parse(cached.versionJson),
|
|
56
|
+
isLatest: false,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Final fallback: let Baileys use its bundled version
|
|
60
|
+
const { version, isLatest } = await fetchLatestBaileysVersion();
|
|
61
|
+
return { version, isLatest };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Force refresh the version cache (used by maintenance job).
|
|
66
|
+
*/
|
|
67
|
+
export async function refreshWaVersion() {
|
|
68
|
+
const { version, isLatest } = await fetchLatestBaileysVersion();
|
|
69
|
+
db.insert(waVersionCache)
|
|
70
|
+
.values({
|
|
71
|
+
id: 1,
|
|
72
|
+
versionJson: JSON.stringify(version),
|
|
73
|
+
fetchedAt: Date.now(),
|
|
74
|
+
isLatest: isLatest ? 1 : 0,
|
|
75
|
+
})
|
|
76
|
+
.onConflictDoUpdate({
|
|
77
|
+
target: waVersionCache.id,
|
|
78
|
+
set: {
|
|
79
|
+
versionJson: JSON.stringify(version),
|
|
80
|
+
fetchedAt: Date.now(),
|
|
81
|
+
isLatest: isLatest ? 1 : 0,
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
.run();
|
|
85
|
+
return { version, isLatest };
|
|
86
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// WA MCP — Channel Adapter Interface
|
|
3
|
+
// All channel implementations (Baileys, Cloud API) must
|
|
4
|
+
// implement this interface to provide a unified API surface.
|
|
5
|
+
// ============================================================
|
|
6
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
+
export function getConfigDir() {
|
|
7
|
+
if (process.env.WAGENT_HOME)
|
|
8
|
+
return process.env.WAGENT_HOME;
|
|
9
|
+
if (process.platform === "win32")
|
|
10
|
+
return join(process.env.APPDATA ?? homedir(), "wagent");
|
|
11
|
+
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "wagent");
|
|
12
|
+
}
|
|
13
|
+
export function ensureConfigDir() {
|
|
14
|
+
const dir = getConfigDir();
|
|
15
|
+
mkdirSync(dir, { recursive: true });
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
export function getConfigPath() {
|
|
19
|
+
return join(ensureConfigDir(), "config.json");
|
|
20
|
+
}
|
|
21
|
+
export function defaultPolicy() {
|
|
22
|
+
return {
|
|
23
|
+
autoReplyEnabled: false,
|
|
24
|
+
readPolicy: "self_only",
|
|
25
|
+
replyPolicy: "self_only",
|
|
26
|
+
sendPolicy: "everyone_except_blacklist",
|
|
27
|
+
allowGroups: false,
|
|
28
|
+
whitelistedContacts: [],
|
|
29
|
+
blacklistedContacts: [],
|
|
30
|
+
whitelistedGroups: [],
|
|
31
|
+
mentionBypass: "@wagent",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function defaultAgentState() {
|
|
35
|
+
return {
|
|
36
|
+
mode: "bootstrap",
|
|
37
|
+
bootstrapStarted: false,
|
|
38
|
+
bootstrapNotes: [],
|
|
39
|
+
policy: defaultPolicy(),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function defaultConfig() {
|
|
43
|
+
return {
|
|
44
|
+
provider: "gemini",
|
|
45
|
+
apiKey: "",
|
|
46
|
+
model: "gemini-3.1-flash-lite",
|
|
47
|
+
fallbackModels: ["gemma-4-31b", "gemma-4-27b"],
|
|
48
|
+
replyDelayMinMs: 2000,
|
|
49
|
+
replyDelayMaxMs: 8000,
|
|
50
|
+
agent: defaultAgentState(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function loadConfig() {
|
|
54
|
+
const path = getConfigPath();
|
|
55
|
+
const fromDisk = existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : {};
|
|
56
|
+
const merged = {
|
|
57
|
+
...defaultConfig(),
|
|
58
|
+
...fromDisk,
|
|
59
|
+
agent: {
|
|
60
|
+
...defaultAgentState(),
|
|
61
|
+
...(fromDisk.agent ?? {}),
|
|
62
|
+
policy: { ...defaultPolicy(), ...(fromDisk.agent?.policy ?? {}) },
|
|
63
|
+
bootstrapNotes: fromDisk.agent?.bootstrapNotes ?? [],
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
if (process.env.GROQ_API_KEY) {
|
|
67
|
+
merged.provider = "groq";
|
|
68
|
+
merged.apiKey = process.env.GROQ_API_KEY;
|
|
69
|
+
merged.baseUrl = process.env.GROQ_BASE_URL ?? merged.baseUrl;
|
|
70
|
+
merged.model = process.env.GROQ_MODEL ?? "openai/gpt-oss-120b";
|
|
71
|
+
merged.fallbackModels = ["llama-3.3-70b-versatile", "llama-3.1-8b-instant"];
|
|
72
|
+
}
|
|
73
|
+
return merged;
|
|
74
|
+
}
|
|
75
|
+
export function saveConfig(config) {
|
|
76
|
+
writeFileSync(getConfigPath(), `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
77
|
+
}
|
|
78
|
+
function mask(value) {
|
|
79
|
+
if (value.length <= 8)
|
|
80
|
+
return "********";
|
|
81
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
82
|
+
}
|
|
83
|
+
export async function ensureAiConfig(config) {
|
|
84
|
+
if (config.apiKey)
|
|
85
|
+
return config;
|
|
86
|
+
const rl = readline.createInterface({ input, output });
|
|
87
|
+
try {
|
|
88
|
+
console.log("\nChoose AI provider:");
|
|
89
|
+
console.log("1. Google Gemini + Web Search (recommended)");
|
|
90
|
+
console.log("2. Groq");
|
|
91
|
+
console.log("3. Other OpenAI-compatible endpoint");
|
|
92
|
+
const choice = (await rl.question("Provider [1]: ")).trim() || "1";
|
|
93
|
+
if (choice === "2") {
|
|
94
|
+
config.provider = "groq";
|
|
95
|
+
config.baseUrl = "https://api.groq.com/openai/v1";
|
|
96
|
+
config.model = "openai/gpt-oss-120b";
|
|
97
|
+
config.fallbackModels = ["llama-3.3-70b-versatile", "llama-3.1-8b-instant"];
|
|
98
|
+
}
|
|
99
|
+
else if (choice === "3") {
|
|
100
|
+
config.provider = "openai-compatible";
|
|
101
|
+
config.baseUrl = (await rl.question("Base URL: ")).trim();
|
|
102
|
+
config.model = (await rl.question("Model: ")).trim();
|
|
103
|
+
config.fallbackModels = [];
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
config.provider = "gemini";
|
|
107
|
+
config.model = "gemini-3.1-flash-lite";
|
|
108
|
+
config.fallbackModels = ["gemma-4-31b", "gemma-4-27b"];
|
|
109
|
+
}
|
|
110
|
+
config.apiKey = (await rl.question("API key: ")).trim();
|
|
111
|
+
if (!config.apiKey)
|
|
112
|
+
throw new Error("API key is required");
|
|
113
|
+
saveConfig(config);
|
|
114
|
+
console.log(`Saved ${config.provider} config (${mask(config.apiKey)}).`);
|
|
115
|
+
return config;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
rl.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// WA MCP — Constants & Defaults
|
|
3
|
+
// ============================================================
|
|
4
|
+
export const VERSION = "1.0.0";
|
|
5
|
+
export const SERVER_NAME = "wa-mcp";
|
|
6
|
+
// Transport
|
|
7
|
+
export const DEFAULT_TRANSPORT = "http";
|
|
8
|
+
export const DEFAULT_PORT = 3000;
|
|
9
|
+
// Redis
|
|
10
|
+
export const DEFAULT_REDIS_URL = "redis://localhost:6379";
|
|
11
|
+
// Logging
|
|
12
|
+
export const DEFAULT_LOG_LEVEL = "warn";
|
|
13
|
+
// Rate Limits (messages per minute)
|
|
14
|
+
export const DEFAULT_BAILEYS_RATE_LIMIT = 20;
|
|
15
|
+
export const DEFAULT_CLOUD_RATE_LIMIT = 80;
|
|
16
|
+
// Message Retention
|
|
17
|
+
export const DEFAULT_MESSAGE_RETENTION_DAYS = 30;
|
|
18
|
+
// Media
|
|
19
|
+
export const DEFAULT_MEDIA_CACHE_MAX_MB = 500;
|
|
20
|
+
// Cloud API Webhook
|
|
21
|
+
export const DEFAULT_CLOUD_WEBHOOK_PORT = 3001;
|
|
22
|
+
// Auto-reconnect
|
|
23
|
+
export const DEFAULT_AUTO_RECONNECT = true;
|
|
24
|
+
// Version check
|
|
25
|
+
export const DEFAULT_VERSION_CHECK = true;
|
|
26
|
+
// Queue settings
|
|
27
|
+
export const QUEUE_RETRY_ATTEMPTS = 3;
|
|
28
|
+
export const QUEUE_RETRY_DELAY_MS = 2000;
|
|
29
|
+
export const QUEUE_COMPLETED_AGE_S = 3600; // 1 hour
|
|
30
|
+
export const QUEUE_FAILED_AGE_S = 86400; // 24 hours
|
|
31
|
+
export const DEFAULT_JOB_PRIORITY = 5;
|
|
32
|
+
// Reconnect settings
|
|
33
|
+
export const RECONNECT_INITIAL_DELAY_MS = 1000;
|
|
34
|
+
export const RECONNECT_MAX_DELAY_MS = 30000;
|
|
35
|
+
export const RECONNECT_MAX_ATTEMPTS = 10;
|
|
36
|
+
// Dedup
|
|
37
|
+
export const DEDUP_TTL_HOURS = 24;
|
|
38
|
+
// Maintenance schedules
|
|
39
|
+
export const PRUNE_MESSAGES_CRON = "0 3 * * *"; // Daily at 3 AM
|
|
40
|
+
export const PRUNE_DEDUP_CRON = "0 * * * *"; // Hourly
|
|
41
|
+
export const CHECK_VERSION_CRON = "0 6 * * *"; // Daily at 6 AM
|
|
42
|
+
export const HEALTH_CHECK_INTERVAL_MS = 300_000; // 5 minutes
|
|
43
|
+
// Instance ID prefix
|
|
44
|
+
export const INSTANCE_ID_PREFIX = "inst_";
|
|
45
|
+
// Message limits
|
|
46
|
+
export const MAX_TEXT_LENGTH = 65536;
|
|
47
|
+
// Media size limits (bytes)
|
|
48
|
+
export const MEDIA_LIMITS = {
|
|
49
|
+
image: 16 * 1024 * 1024, // 16 MB
|
|
50
|
+
video: 16 * 1024 * 1024, // 16 MB
|
|
51
|
+
audio: 16 * 1024 * 1024, // 16 MB
|
|
52
|
+
document: 100 * 1024 * 1024, // 100 MB
|
|
53
|
+
sticker: 500 * 1024, // 500 KB
|
|
54
|
+
};
|
|
55
|
+
// MCP endpoint
|
|
56
|
+
export const MCP_ENDPOINT = "/mcp";
|
|
57
|
+
export const HEALTH_ENDPOINT = "/health";
|
|
58
|
+
// Max base64 media size (bytes)
|
|
59
|
+
export const MAX_BASE64_MEDIA_BYTES = 100 * 1024 * 1024; // 100 MB
|