levix-bot 3.3.0-rc.2 → 4.0.0-beta

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.
Files changed (118) hide show
  1. package/README.md +11 -7
  2. package/app.cjs +42 -45
  3. package/package.json +9 -1
  4. package/public/dashboard/assets/index-C1b7ijfu.js +391 -0
  5. package/public/dashboard/assets/index-pYaivHON.css +1 -0
  6. package/public/dashboard/index.html +19 -0
  7. package/public/dashboard.css +391 -66
  8. package/public/dashboard.js +593 -42
  9. package/scheduler.cjs +9 -14
  10. package/src/auth/use-database-auth-state.js +2 -4
  11. package/src/bootstrap/core.js +2 -5
  12. package/src/bootstrap/panel.js +10 -5
  13. package/src/cli.js +3 -5
  14. package/src/commands/autoschedule.cjs +1 -4
  15. package/src/commands/blacklist.cjs +22 -20
  16. package/src/commands/block.cjs +2 -4
  17. package/src/commands/calc.cjs +9 -16
  18. package/src/commands/digit.cjs +97 -0
  19. package/src/commands/gemini.cjs +147 -161
  20. package/src/commands/group/add.cjs +7 -16
  21. package/src/commands/group/antiSpam.cjs +5 -11
  22. package/src/commands/group/antilink.cjs +34 -28
  23. package/src/commands/group/approveall.cjs +3 -11
  24. package/src/commands/group/clearwarns.cjs +2 -5
  25. package/src/commands/group/demote.cjs +3 -8
  26. package/src/commands/group/kick.cjs +12 -15
  27. package/src/commands/group/media.cjs +17 -24
  28. package/src/commands/group/members.cjs +1 -2
  29. package/src/commands/group/promote.cjs +4 -12
  30. package/src/commands/group/removeall.cjs +2 -5
  31. package/src/commands/group/setpp.cjs +1 -1
  32. package/src/commands/group/setrules.cjs +2 -8
  33. package/src/commands/group/setwarn.cjs +4 -2
  34. package/src/commands/group/tagadmins.cjs +2 -5
  35. package/src/commands/group/warn.cjs +26 -24
  36. package/src/commands/group/warns.cjs +1 -2
  37. package/src/commands/group/welcome.cjs +13 -17
  38. package/src/commands/group.cjs +7 -10
  39. package/src/commands/help.cjs +6 -12
  40. package/src/commands/lang.cjs +104 -0
  41. package/src/commands/listschedules.cjs +2 -6
  42. package/src/commands/loop.cjs +6 -13
  43. package/src/commands/memory.cjs +19 -32
  44. package/src/commands/mod.cjs +97 -35
  45. package/src/commands/perm.cjs +18 -56
  46. package/src/commands/ping.cjs +1 -5
  47. package/src/commands/poll.cjs +1 -1
  48. package/src/commands/rand.cjs +2 -5
  49. package/src/commands/schedule.cjs +4 -8
  50. package/src/commands/score.cjs +8 -2
  51. package/src/commands/setprefix.cjs +2 -2
  52. package/src/commands/shortlink.cjs +7 -15
  53. package/src/commands/shutdown.cjs +1 -1
  54. package/src/commands/sticker.cjs +222 -0
  55. package/src/commands/stt.cjs +136 -68
  56. package/src/commands/todo.cjs +9 -15
  57. package/src/commands/tts.cjs +10 -16
  58. package/src/commands/unblock.cjs +2 -6
  59. package/src/commands/weather.cjs +5 -12
  60. package/src/config/ai-persona.md +5 -3
  61. package/src/config/available-models.json +1156 -0
  62. package/src/config/baileys.config.js +2 -1
  63. package/src/config/brand.esm.js +1 -10
  64. package/src/config/constants.js +1 -1
  65. package/src/config/defaults.cjs +66 -66
  66. package/src/config/lock.cjs +2 -2
  67. package/src/config/modelCapabilities.cjs +172 -0
  68. package/src/config/runtime-config.cjs +8 -13
  69. package/src/config/secrets.cjs +18 -10
  70. package/src/config/settings.cjs +94 -10
  71. package/src/config/vault.cjs +107 -0
  72. package/src/core/connection.js +13 -12
  73. package/src/core/events.js +41 -23
  74. package/src/core/proxy.js +7 -7
  75. package/src/core/session.js +91 -48
  76. package/src/core/socket.js +12 -15
  77. package/src/db/db.cjs +14 -6
  78. package/src/db/store.cjs +221 -87
  79. package/src/domain/caddy.js +1 -1
  80. package/src/domain/command.js +40 -19
  81. package/src/domain/detect.js +1 -1
  82. package/src/domain/system.js +2 -5
  83. package/src/handlers/command.handler.js +22 -43
  84. package/src/handlers/group.handler.js +13 -13
  85. package/src/handlers/message.handler.js +11 -21
  86. package/src/index.js +8 -4
  87. package/src/middleware/antispam.middleware.js +46 -23
  88. package/src/middleware/blacklist.middleware.js +14 -9
  89. package/src/middleware/forward-tracking.middleware.js +3 -2
  90. package/src/middleware/permissions.middleware.js +7 -10
  91. package/src/panel/login-throttle.cjs +43 -0
  92. package/src/panel/session-auth.cjs +17 -0
  93. package/src/routes/dashboard.api.esm.js +394 -72
  94. package/src/services/aiAgent.cjs +136 -49
  95. package/src/services/aiProviders.cjs +193 -47
  96. package/src/services/aiRouter.cjs +137 -0
  97. package/src/services/aiTools.cjs +305 -125
  98. package/src/services/llmDiscovery.cjs +481 -0
  99. package/src/services/modelRegistry.cjs +412 -0
  100. package/src/utils/datetime.cjs +2 -2
  101. package/src/utils/lid-helper.esm.js +7 -7
  102. package/src/utils/logger.cjs +1 -1
  103. package/src/utils/markdownParser.cjs +159 -0
  104. package/src/utils/memory.cjs +43 -13
  105. package/src/utils/messageContent.cjs +40 -0
  106. package/src/utils/normalizeJid.esm.js +7 -7
  107. package/src/utils/numerals.cjs +75 -0
  108. package/src/utils/permissions.esm.js +10 -15
  109. package/src/utils/providerUrl.cjs +281 -0
  110. package/src/utils/recurrence.cjs +7 -11
  111. package/src/utils/sendBotMessage.esm.js +4 -14
  112. package/src/utils/statusMessage.cjs +5 -13
  113. package/src/utils/storage.cjs +10 -0
  114. package/src/utils/storage.esm.js +11 -0
  115. package/src/utils/textDecode.cjs +4 -9
  116. package/src/utils/thumbnail.cjs +8 -14
  117. package/src/utils/typing.esm.js +12 -12
  118. package/views/dashboard.ejs +134 -106
@@ -0,0 +1,412 @@
1
+ // Centralized LLM provider model registry and seed catalog.
2
+ //
3
+ // Source of truth rule:
4
+ // - The seeded catalog is ONLY for recommended models, display names,
5
+ // capability fallback, and the empty-key UI.
6
+ // - Seed models are NEVER presented as "available for your account" unless
7
+ // the provider's authenticated models endpoint returned that id.
8
+
9
+ const crypto = require("node:crypto");
10
+ const {
11
+ officialCapabilities,
12
+ isGeminiRecommended,
13
+ isConversationalLlm,
14
+ } = require("../config/modelCapabilities.cjs");
15
+
16
+ const DEFAULT_CAPABILITIES = Object.freeze({
17
+ textInput: true,
18
+ textOutput: true,
19
+ vision: false,
20
+ audioInput: false,
21
+ stt: false,
22
+ videoInput: false,
23
+ pdfInput: false,
24
+ });
25
+
26
+ function normalizeCapabilities(caps) {
27
+ const c = caps || {};
28
+ return {
29
+ textInput: Boolean(c.textInput ?? DEFAULT_CAPABILITIES.textInput),
30
+ textOutput: Boolean(c.textOutput ?? DEFAULT_CAPABILITIES.textOutput),
31
+ vision: Boolean(c.vision ?? DEFAULT_CAPABILITIES.vision),
32
+ audioInput: Boolean(c.audioInput ?? DEFAULT_CAPABILITIES.audioInput),
33
+ stt: Boolean(c.stt ?? DEFAULT_CAPABILITIES.stt),
34
+ videoInput: Boolean(c.videoInput ?? DEFAULT_CAPABILITIES.videoInput),
35
+ pdfInput: Boolean(c.pdfInput ?? DEFAULT_CAPABILITIES.pdfInput),
36
+ };
37
+ }
38
+
39
+ function cleanModelId(value) {
40
+ return String(value || "")
41
+ .trim()
42
+ .replace(/^models\//, "");
43
+ }
44
+
45
+ function flattenRawModels(modelsList) {
46
+ const result = [];
47
+ if (!Array.isArray(modelsList)) return result;
48
+ for (const item of modelsList) {
49
+ if (!item) continue;
50
+ if (Array.isArray(item.models)) {
51
+ result.push(...flattenRawModels(item.models));
52
+ continue;
53
+ }
54
+ result.push(item);
55
+ }
56
+ return result;
57
+ }
58
+
59
+ function normalizeSeedEntry(item, provider) {
60
+ const rawId = cleanModelId(item.id || item.name);
61
+ if (!rawId) return null;
62
+
63
+ const methods = item.supportedGenerationMethods || item.supportedActions || [];
64
+ if (!isConversationalLlm(provider, rawId, { methods, type: item.type })) {
65
+ return null;
66
+ }
67
+
68
+ let caps = item.capabilities;
69
+ let capabilitySource = item.capabilitySource || (caps ? "official-docs" : "unknown");
70
+ if (!caps) {
71
+ const official = officialCapabilities(provider, rawId);
72
+ if (official) {
73
+ caps = official;
74
+ capabilitySource = "official-docs";
75
+ } else {
76
+ caps = DEFAULT_CAPABILITIES;
77
+ capabilitySource = "unknown";
78
+ }
79
+ }
80
+
81
+ const recommended =
82
+ provider === "gemini"
83
+ ? Boolean(item.recommended) || isGeminiRecommended(rawId)
84
+ : Boolean(item.recommended);
85
+
86
+ return {
87
+ id: rawId,
88
+ name: item.name || (provider === "gemini" ? `models/${rawId}` : rawId),
89
+ displayName: item.displayName || rawId,
90
+ recommended,
91
+ status: item.status || (provider === "gemini" ? "stable" : "active"),
92
+ aliases: Array.isArray(item.aliases) ? item.aliases : [],
93
+ capabilitySource,
94
+ capabilities: normalizeCapabilities(caps),
95
+ supportedGenerationMethods: methods,
96
+ };
97
+ }
98
+
99
+ function ensureBaselineModels(models, provider) {
100
+ const existingIds = new Set(models.map((m) => m.id.toLowerCase()));
101
+ const baselines = {
102
+ openai: [
103
+ {
104
+ id: "gpt-4o",
105
+ displayName: "GPT-4o",
106
+ recommended: false,
107
+ status: "active",
108
+ aliases: ["chatgpt-4o-latest", "gpt-4o-2024-08-06", "gpt-4o-2024-05-13"],
109
+ capabilitySource: "official-docs",
110
+ capabilities: {
111
+ textInput: true,
112
+ textOutput: true,
113
+ vision: true,
114
+ audioInput: false,
115
+ stt: false,
116
+ videoInput: false,
117
+ pdfInput: true,
118
+ },
119
+ },
120
+ {
121
+ id: "gpt-4o-mini",
122
+ displayName: "GPT-4o Mini",
123
+ recommended: false,
124
+ status: "active",
125
+ aliases: ["gpt-4o-mini-2024-07-18"],
126
+ capabilitySource: "official-docs",
127
+ capabilities: {
128
+ textInput: true,
129
+ textOutput: true,
130
+ vision: true,
131
+ audioInput: false,
132
+ stt: false,
133
+ videoInput: false,
134
+ pdfInput: true,
135
+ },
136
+ },
137
+ ],
138
+ anthropic: [
139
+ {
140
+ id: "claude-3-5-sonnet",
141
+ displayName: "Claude 3.5 Sonnet",
142
+ recommended: false,
143
+ status: "active",
144
+ aliases: ["claude-3-5-sonnet-20241022", "claude-3-5-sonnet-latest", "claude-3-sonnet"],
145
+ capabilitySource: "official-docs",
146
+ capabilities: {
147
+ textInput: true,
148
+ textOutput: true,
149
+ vision: true,
150
+ audioInput: false,
151
+ stt: false,
152
+ videoInput: false,
153
+ pdfInput: true,
154
+ },
155
+ },
156
+ {
157
+ id: "claude-3-5-haiku",
158
+ displayName: "Claude 3.5 Haiku",
159
+ recommended: false,
160
+ status: "active",
161
+ aliases: ["claude-3-5-haiku-20241022", "claude-3-haiku"],
162
+ capabilitySource: "official-docs",
163
+ capabilities: {
164
+ textInput: true,
165
+ textOutput: true,
166
+ vision: true,
167
+ audioInput: false,
168
+ stt: false,
169
+ videoInput: false,
170
+ pdfInput: true,
171
+ },
172
+ },
173
+ ],
174
+ gemini: [
175
+ {
176
+ id: "gemini-3.7-flash",
177
+ displayName: "Gemini 3.7 Flash",
178
+ recommended: true,
179
+ status: "stable",
180
+ aliases: ["models/gemini-3.7-flash"],
181
+ capabilitySource: "official-docs",
182
+ capabilities: officialCapabilities("gemini", "gemini-3.7-flash"),
183
+ },
184
+ {
185
+ id: "gemini-1.5-flash",
186
+ displayName: "Gemini 1.5 Flash",
187
+ recommended: false,
188
+ status: "stable",
189
+ aliases: ["models/gemini-1.5-flash"],
190
+ capabilitySource: "official-docs",
191
+ capabilities: officialCapabilities("gemini", "gemini-1.5-flash"),
192
+ },
193
+ ],
194
+ };
195
+
196
+ for (const b of baselines[provider] || []) {
197
+ if (!existingIds.has(b.id.toLowerCase())) {
198
+ models.push({
199
+ ...b,
200
+ capabilities: normalizeCapabilities(b.capabilities),
201
+ });
202
+ existingIds.add(b.id.toLowerCase());
203
+ }
204
+ }
205
+ }
206
+
207
+ function processCatalog(rawCatalog) {
208
+ if (!rawCatalog || !rawCatalog.providers) return rawCatalog;
209
+ for (const p of Object.keys(rawCatalog.providers)) {
210
+ const provObj = rawCatalog.providers[p];
211
+ if (!provObj || !Array.isArray(provObj.models)) continue;
212
+ const flat = flattenRawModels(provObj.models)
213
+ .map((item) => normalizeSeedEntry(item, p))
214
+ .filter(Boolean);
215
+ ensureBaselineModels(flat, p);
216
+ provObj.models = flat;
217
+ }
218
+ return rawCatalog;
219
+ }
220
+
221
+ let cachedSeedData = null;
222
+
223
+ function loadRawCatalog() {
224
+ try {
225
+ return require("../config/available-models.json");
226
+ } catch {
227
+ return null;
228
+ }
229
+ }
230
+
231
+ function loadSeedCatalog() {
232
+ if (cachedSeedData) return cachedSeedData;
233
+ const parsed = loadRawCatalog();
234
+ if (parsed && parsed.providers) {
235
+ cachedSeedData = processCatalog(parsed);
236
+ return cachedSeedData;
237
+ }
238
+
239
+ cachedSeedData = processCatalog({
240
+ schemaVersion: 1,
241
+ updatedAt: "2026-09-18",
242
+ providers: {
243
+ openai: { requiresApiKey: true, models: [] },
244
+ anthropic: { requiresApiKey: true, models: [] },
245
+ gemini: { requiresApiKey: true, models: [] },
246
+ },
247
+ });
248
+ return cachedSeedData;
249
+ }
250
+
251
+ function getSeedModels(provider) {
252
+ const catalog = loadSeedCatalog();
253
+ const provKey = String(provider || "").toLowerCase();
254
+ const provData = catalog.providers?.[provKey];
255
+ if (!provData || !Array.isArray(provData.models)) return [];
256
+
257
+ return provData.models.map((m) => ({
258
+ id: m.id,
259
+ displayName: m.displayName || m.id,
260
+ provider: provKey,
261
+ recommended: Boolean(m.recommended),
262
+ capabilities: normalizeCapabilities(m.capabilities),
263
+ capabilitySource: "seed",
264
+ status: m.status || "active",
265
+ isRecommendedSeed: true,
266
+ live: false,
267
+ }));
268
+ }
269
+
270
+ function findSeedModel(provider, modelId) {
271
+ if (!modelId) return null;
272
+ const catalog = loadSeedCatalog();
273
+ const provKey = String(provider || "").toLowerCase();
274
+ const provData = catalog.providers?.[provKey];
275
+ if (!provData || !Array.isArray(provData.models)) return null;
276
+
277
+ const rawTarget = String(modelId).trim().toLowerCase();
278
+ const cleanTarget = cleanModelId(rawTarget);
279
+
280
+ for (const m of provData.models) {
281
+ if (!m) continue;
282
+ const mid = String(m.id || "").toLowerCase();
283
+ const mname = cleanModelId(m.name || "").toLowerCase();
284
+ if (mid === cleanTarget || mid === rawTarget || mname === cleanTarget || mname === rawTarget) {
285
+ return m;
286
+ }
287
+ if (Array.isArray(m.aliases)) {
288
+ const hit = m.aliases.some((alias) => {
289
+ const a = String(alias).toLowerCase();
290
+ return a === cleanTarget || a === rawTarget || cleanModelId(a) === cleanTarget;
291
+ });
292
+ if (hit) return m;
293
+ }
294
+ }
295
+ return null;
296
+ }
297
+
298
+ function resolveCapabilities(provider, modelId, providerCaps = null) {
299
+ const seed = findSeedModel(provider, modelId);
300
+ if (providerCaps && typeof providerCaps === "object") {
301
+ return {
302
+ capabilities: normalizeCapabilities({
303
+ ...(seed?.capabilities || DEFAULT_CAPABILITIES),
304
+ ...providerCaps,
305
+ }),
306
+ capabilitySource: "provider",
307
+ displayName: seed?.displayName || null,
308
+ recommended: Boolean(seed?.recommended),
309
+ status: seed?.status || "active",
310
+ };
311
+ }
312
+ if (seed) {
313
+ return {
314
+ capabilities: normalizeCapabilities(seed.capabilities),
315
+ capabilitySource: "seed",
316
+ displayName: seed.displayName || null,
317
+ recommended: Boolean(seed.recommended),
318
+ status: seed.status || "active",
319
+ };
320
+ }
321
+ return {
322
+ capabilities: { ...DEFAULT_CAPABILITIES },
323
+ capabilitySource: "unknown",
324
+ displayName: null,
325
+ recommended: false,
326
+ status: "active",
327
+ };
328
+ }
329
+
330
+ /**
331
+ * Independent vision / STT guards. Unsupported features are forced off;
332
+ * supported features keep the operator's existing choice.
333
+ */
334
+ function applyCapabilityGuards({ visionEnabled, sttEnabled, capabilities }) {
335
+ const caps = normalizeCapabilities(capabilities);
336
+ return {
337
+ visionEnabled: caps.vision ? Boolean(visionEnabled) : false,
338
+ sttEnabled: caps.stt ? Boolean(sttEnabled) : false,
339
+ visionLocked: !caps.vision,
340
+ sttLocked: !caps.stt,
341
+ };
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // Discovery cache: provider + SHA-256 of the credential. Never stores the key.
346
+ // ---------------------------------------------------------------------------
347
+
348
+ const CACHE_TTL_MS = 5 * 60 * 1000;
349
+ const discoveryCache = new Map();
350
+
351
+ function computeCacheKey(provider, apiKey, baseUrl = "") {
352
+ const prov = String(provider || "").toLowerCase();
353
+ const keyHash = crypto
354
+ .createHash("sha256")
355
+ .update(String(apiKey || ""))
356
+ .digest("hex");
357
+ const normalizedUrl = String(baseUrl || "").replace(/\/+$/, "");
358
+ return `${prov}:${keyHash}:${normalizedUrl}`;
359
+ }
360
+
361
+ function getCachedDiscovery(provider, apiKey, baseUrl = "") {
362
+ if (!apiKey) return null;
363
+ const key = computeCacheKey(provider, apiKey, baseUrl);
364
+ const entry = discoveryCache.get(key);
365
+ if (!entry) return null;
366
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
367
+ discoveryCache.delete(key);
368
+ return null;
369
+ }
370
+ return entry.models;
371
+ }
372
+
373
+ function setCachedDiscovery(provider, apiKey, baseUrl = "", models = []) {
374
+ if (!apiKey) return;
375
+ const key = computeCacheKey(provider, apiKey, baseUrl);
376
+ discoveryCache.set(key, {
377
+ timestamp: Date.now(),
378
+ models: Object.freeze([...models]),
379
+ });
380
+ }
381
+
382
+ function invalidateDiscoveryCache(provider = null) {
383
+ if (!provider) {
384
+ discoveryCache.clear();
385
+ return;
386
+ }
387
+ const prefix = `${String(provider).toLowerCase()}:`;
388
+ for (const k of discoveryCache.keys()) {
389
+ if (k.startsWith(prefix)) discoveryCache.delete(k);
390
+ }
391
+ }
392
+
393
+ function getDiscoveryCacheSize() {
394
+ return discoveryCache.size;
395
+ }
396
+
397
+ module.exports = {
398
+ DEFAULT_CAPABILITIES,
399
+ loadSeedCatalog,
400
+ getSeedModels,
401
+ findSeedModel,
402
+ normalizeCapabilities,
403
+ resolveCapabilities,
404
+ applyCapabilityGuards,
405
+ isConversationalLlm,
406
+ computeCacheKey,
407
+ getCachedDiscovery,
408
+ setCachedDiscovery,
409
+ invalidateDiscoveryCache,
410
+ getDiscoveryCacheSize,
411
+ cleanModelId,
412
+ };
@@ -34,7 +34,7 @@ function zoneOffsetMs(date, timeZone = defaultTimezone()) {
34
34
  Number(parts.day),
35
35
  Number(parts.hour),
36
36
  Number(parts.minute),
37
- Number(parts.second)
37
+ Number(parts.second),
38
38
  );
39
39
 
40
40
  // بنشيل الملي ثانية من الطرفين عشان الفرق يطلع نظيف.
@@ -44,7 +44,7 @@ function zoneOffsetMs(date, timeZone = defaultTimezone()) {
44
44
  // ساعة حائط في منطقة زمنية -> لحظة حقيقية (Date بـ UTC جوّاها).
45
45
  function zonedTimeToDate(
46
46
  { year, month, day, hour = 0, minute = 0, second = 0 },
47
- timeZone = defaultTimezone()
47
+ timeZone = defaultTimezone(),
48
48
  ) {
49
49
  const asUtc = Date.UTC(year, month - 1, day, hour, minute, second);
50
50
  if (!Number.isFinite(asUtc)) return new Date(NaN);
@@ -1,7 +1,7 @@
1
1
  // Baileys v7 LID Helper Functions
2
2
  // These functions help work with the new LID system
3
3
 
4
- import { getLidForPn, getPnForLid } from '../utils/storage.esm.js';
4
+ import { getLidForPn, getPnForLid } from "../utils/storage.esm.js";
5
5
 
6
6
  /**
7
7
  * Get the preferred identifier for a user (LID if available, otherwise PN)
@@ -10,7 +10,7 @@ import { getLidForPn, getPnForLid } from '../utils/storage.esm.js';
10
10
  * @returns {Promise<string>} - LID if available, otherwise the original PN
11
11
  */
12
12
  export async function getPreferredJid(sock, pn) {
13
- if (!pn || !pn.includes('@s.whatsapp.net')) {
13
+ if (!pn || !pn.includes("@s.whatsapp.net")) {
14
14
  return pn; // Not a phone number, return as-is
15
15
  }
16
16
 
@@ -44,7 +44,7 @@ export async function getPreferredJid(sock, pn) {
44
44
  * @returns {Promise<string>} - PN if found, otherwise the original LID
45
45
  */
46
46
  export async function lidToPn(sock, lid) {
47
- if (!lid || !lid.includes('@lid')) {
47
+ if (!lid || !lid.includes("@lid")) {
48
48
  return lid; // Not a LID, return as-is
49
49
  }
50
50
 
@@ -78,7 +78,7 @@ export async function lidToPn(sock, lid) {
78
78
  * @returns {Promise<string>} - User identifier (LID or PN)
79
79
  */
80
80
  export async function getUserIdFromKey(sock, msgKey) {
81
- const isGroup = msgKey.remoteJid?.endsWith('@g.us');
81
+ const isGroup = msgKey.remoteJid?.endsWith("@g.us");
82
82
 
83
83
  if (isGroup) {
84
84
  // In groups, use participant field
@@ -114,11 +114,11 @@ export async function getUserIdFromKey(sock, msgKey) {
114
114
  */
115
115
  export async function formatUserMention(sock, jid) {
116
116
  // Extract the number/ID part for display
117
- const displayId = jid.split('@')[0];
117
+ const displayId = jid.split("@")[0];
118
118
 
119
119
  return {
120
120
  text: `@${displayId}`,
121
- mentions: [jid]
121
+ mentions: [jid],
122
122
  };
123
123
  }
124
124
 
@@ -132,7 +132,7 @@ export async function resolveJids(sock, jids) {
132
132
  const resolved = [];
133
133
 
134
134
  for (const jid of jids) {
135
- if (jid.includes('@s.whatsapp.net')) {
135
+ if (jid.includes("@s.whatsapp.net")) {
136
136
  resolved.push(await getPreferredJid(sock, jid));
137
137
  } else {
138
138
  resolved.push(jid);
@@ -51,7 +51,7 @@ const logger = useInProcessLogs()
51
51
  { level: "trace", stream: pino.destination({ dest: combined, mkdir: true }) },
52
52
  { level: "error", stream: pino.destination({ dest: errors, mkdir: true }) },
53
53
  { level: "trace", stream: require("pino-pretty")(PRETTY) },
54
- ])
54
+ ]),
55
55
  )
56
56
  : pino({
57
57
  ...base,
@@ -0,0 +1,159 @@
1
+ // file: src/utils/markdownParser.cjs
2
+ //
3
+ // Converts standard Markdown output (especially from LLMs) into human-readable
4
+ // WhatsApp formatting:
5
+ // - **bold** -> *bold*
6
+ // - ~~strike~~ -> ~strike~
7
+ // - __italic__ -> _italic_
8
+ // - # Headings -> *Headings*
9
+ // - Markdown tables (| col1 | col2 |) -> clean, itemized WhatsApp cards
10
+
11
+ /**
12
+ * Parses markdown table text into structured data:
13
+ * { headers: string[], rows: string[][] }
14
+ */
15
+ function parseMarkdownTable(tableLines) {
16
+ const cleanCells = (line) => {
17
+ return line
18
+ .trim()
19
+ .replace(/^\|/, "")
20
+ .replace(/\|$/, "")
21
+ .split("|")
22
+ .map((c) => c.trim());
23
+ };
24
+
25
+ if (tableLines.length < 2) return null;
26
+
27
+ const headers = cleanCells(tableLines[0]);
28
+ // Line 1 is usually the separator: |---|---|
29
+ const separatorLine = tableLines[1];
30
+ if (!/^[\s|:-]+$/.test(separatorLine)) return null;
31
+
32
+ const rows = [];
33
+ for (let i = 2; i < tableLines.length; i++) {
34
+ const line = tableLines[i].trim();
35
+ if (!line) continue;
36
+ rows.push(cleanCells(line));
37
+ }
38
+
39
+ return { headers, rows };
40
+ }
41
+
42
+ /**
43
+ * Formats parsed table data into a clean WhatsApp-friendly list of cards.
44
+ */
45
+ function formatTableForWhatsApp({ headers, rows }) {
46
+ if (!headers.length || !rows.length) return "";
47
+
48
+ const cards = [];
49
+
50
+ rows.forEach((row, rowIndex) => {
51
+ const primaryLabel = row[0] || `عنصر ${rowIndex + 1}`;
52
+ const fields = [];
53
+
54
+ // If there's more than 1 column, list the remaining fields with bullet points
55
+ if (headers.length > 1) {
56
+ for (let c = 0; c < headers.length; c++) {
57
+ const headerName = headers[c] || `عمود ${c + 1}`;
58
+ const cellValue = row[c] || "—";
59
+ fields.push(` • *${headerName}:* ${cellValue}`);
60
+ }
61
+ } else {
62
+ fields.push(` • ${primaryLabel}`);
63
+ }
64
+
65
+ cards.push(`📋 *${primaryLabel}*\n${fields.join("\n")}`);
66
+ });
67
+
68
+ return cards.join("\n\n");
69
+ }
70
+
71
+ /**
72
+ * Replaces markdown table blocks with WhatsApp-friendly formatted cards.
73
+ */
74
+ function convertMarkdownTables(text) {
75
+ const lines = text.split("\n");
76
+ const output = [];
77
+ let currentTable = [];
78
+
79
+ const flushTable = () => {
80
+ if (currentTable.length >= 2) {
81
+ const parsed = parseMarkdownTable(currentTable);
82
+ if (parsed) {
83
+ output.push(formatTableForWhatsApp(parsed));
84
+ } else {
85
+ output.push(...currentTable);
86
+ }
87
+ } else {
88
+ output.push(...currentTable);
89
+ }
90
+ currentTable = [];
91
+ };
92
+
93
+ for (const line of lines) {
94
+ if (line.trim().startsWith("|") && line.trim().endsWith("|")) {
95
+ currentTable.push(line);
96
+ } else {
97
+ flushTable();
98
+ output.push(line);
99
+ }
100
+ }
101
+ flushTable();
102
+
103
+ return output.join("\n");
104
+ }
105
+
106
+ /**
107
+ * Main parser: converts standard Markdown to WhatsApp formatting.
108
+ */
109
+ function formatMarkdownForWhatsApp(content) {
110
+ if (!content || typeof content !== "string") return content;
111
+
112
+ // 1. Protect code blocks and inline code using null-byte tokens
113
+ const codeBlocks = [];
114
+ let masked = content.replace(/```([\s\S]*?)```/g, (_, code) => {
115
+ const placeholder = `\x00CODEBLOCK${codeBlocks.length}\x00`;
116
+ codeBlocks.push(`\`\`\`${code}\`\`\``);
117
+ return placeholder;
118
+ });
119
+
120
+ const inlineCodes = [];
121
+ masked = masked.replace(/`([^`]+)`/g, (_, code) => {
122
+ const placeholder = `\x00INLINECODE${inlineCodes.length}\x00`;
123
+ inlineCodes.push(`\`${code}\``);
124
+ return placeholder;
125
+ });
126
+
127
+ // 2. Convert markdown tables into WhatsApp cards
128
+ masked = convertMarkdownTables(masked);
129
+
130
+ // 3. Convert Headings: # Heading -> *Heading*
131
+ masked = masked.replace(/(^|\n|\s)(#{1,6})\s+([^\n]+)/g, (match, lead, hashes, title) => {
132
+ return `${lead}*${title.trim()}*`;
133
+ });
134
+
135
+ // 4. Convert Strikethrough: ~~text~~ -> ~text~
136
+ masked = masked.replace(/~~([^~\n]+)~~/g, "~$1~");
137
+
138
+ // 5. Convert Bold: **text** -> *text*
139
+ masked = masked.replace(/\*\*([^*\n]+)\*\*/g, "*$1*");
140
+
141
+ // 6. Convert Italic: __text__ -> _text_
142
+ masked = masked.replace(/__([^_\n]+)__/g, "_$1_");
143
+
144
+ // 7. Restore inline code and code blocks safely
145
+ inlineCodes.forEach((code, idx) => {
146
+ masked = masked.split(`\x00INLINECODE${idx}\x00`).join(code);
147
+ });
148
+ codeBlocks.forEach((block, idx) => {
149
+ masked = masked.split(`\x00CODEBLOCK${idx}\x00`).join(block);
150
+ });
151
+
152
+ return masked;
153
+ }
154
+
155
+ module.exports = {
156
+ formatMarkdownForWhatsApp,
157
+ convertMarkdownTables,
158
+ parseMarkdownTable,
159
+ };