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.
@@ -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