@peopl-health/nexus 1.7.6 → 1.7.8

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.
@@ -19,6 +19,12 @@ async function startServer() {
19
19
  accountSid: process.env.TWILIO_ACCOUNT_SID,
20
20
  authToken: process.env.TWILIO_AUTH_TOKEN,
21
21
  whatsappNumber: process.env.TWILIO_WHATSAPP_NUMBER
22
+ },
23
+
24
+ // Media configuration for AWS S3 upload
25
+ media: {
26
+ bucketName: process.env.AWS_S3_BUCKET_NAME,
27
+ region: process.env.AWS_REGION || 'us-east-1'
22
28
  }
23
29
  });
24
30
 
@@ -28,7 +34,7 @@ async function startServer() {
28
34
  // Add webhook endpoint for incoming messages
29
35
  app.post('/webhook', async (req, res) => {
30
36
  try {
31
- await nexus.messaging.processIncomingMessage(req.body);
37
+ await nexus.processMessage(req.body);
32
38
  res.status(200).send('OK');
33
39
  } catch (error) {
34
40
  console.error('Webhook error:', error);
@@ -42,7 +48,12 @@ async function startServer() {
42
48
  connected: nexus.messaging.isConnected(),
43
49
  provider: 'twilio',
44
50
  mongodb: nexus.messaging.mongodb?.readyState === 1,
45
- airtable: !!nexus.messaging.airtable
51
+ airtable: !!nexus.messaging.airtable,
52
+ aws: {
53
+ configured: !!(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY),
54
+ bucket: process.env.AWS_S3_BUCKET_NAME || 'not configured',
55
+ region: process.env.AWS_REGION || 'us-east-1'
56
+ }
46
57
  });
47
58
  });
48
59
 
@@ -64,7 +75,14 @@ async function startServer() {
64
75
  console.log('- POST /webhook - Webhook for incoming messages');
65
76
  console.log('- GET /status - Connection status');
66
77
  console.log('- GET /airtable-test - Test Airtable connection');
78
+ console.log('- GET /media-test - Test AWS media upload configuration');
67
79
  console.log('- All Nexus API routes under /api/*');
80
+ console.log('');
81
+ console.log('📸 Media Upload Setup:');
82
+ console.log(' Add these to your .env file for media upload:');
83
+ console.log(' - AWS_ACCESS_KEY_ID=your_access_key');
84
+ console.log(' - AWS_SECRET_ACCESS_KEY=your_secret_key');
85
+ console.log(' - AWS_S3_BUCKET_NAME=your-bucket-name');
68
86
  });
69
87
  }
70
88
 
@@ -84,19 +84,19 @@ class BaileysProvider extends MessageProvider {
84
84
  throw new Error('Baileys provider not connected');
85
85
  }
86
86
 
87
- const { code, message, fileUrl, fileType, hidePreview = false } = messageData;
87
+ const { code, body, fileUrl, fileType, hidePreview = false } = messageData;
88
88
 
89
89
  if (!code) {
90
90
  throw new Error('Recipient is required');
91
91
  }
92
92
 
93
- if (!message && !fileUrl) {
93
+ if (!body && !fileUrl) {
94
94
  throw new Error('Message or file URL is required');
95
95
  }
96
96
 
97
97
  const formattedCode = this.formatCode(code);
98
98
  let sendOptions = {};
99
- let formattedMessage = this.formatMessage(message || '');
99
+ let formattedMessage = this.formatMessage(body || '');
100
100
 
101
101
  // Handle mentions
102
102
  const regex = /@\d+/g;
@@ -44,7 +44,7 @@ class TwilioProvider extends MessageProvider {
44
44
  throw new Error('Twilio provider not initialized');
45
45
  }
46
46
 
47
- const { code, message, fileUrl, fileType, variables, contentSid } = messageData;
47
+ const { code, body, fileUrl, fileType, variables, contentSid } = messageData;
48
48
 
49
49
  const formattedFrom = this.ensureWhatsAppFormat(this.whatsappNumber);
50
50
  const formattedCode = this.ensureWhatsAppFormat(code);
@@ -77,8 +77,8 @@ class TwilioProvider extends MessageProvider {
77
77
  });
78
78
  messageParams.contentVariables = JSON.stringify(formattedVariables);
79
79
  }
80
- } else if (message) {
81
- messageParams.body = message;
80
+ } else if (body) {
81
+ messageParams.body = body;
82
82
  }
83
83
 
84
84
  // Handle media messages
@@ -218,6 +218,11 @@ class TwilioProvider extends MessageProvider {
218
218
  // Convert Mongoose document to plain object if needed
219
219
  const payload = scheduledMessage.toObject ? scheduledMessage.toObject() : { ...scheduledMessage };
220
220
  delete payload.__nexusSend;
221
+
222
+ // Map message field to body for consistency (scheduled messages use 'message' field)
223
+ if (payload.message && !payload.body) {
224
+ payload.body = payload.message;
225
+ }
221
226
  console.log('[TwilioProvider] Timer fired', {
222
227
  code: payload.code,
223
228
  hasMessage: Boolean(payload.message || payload.body),
@@ -314,7 +319,7 @@ class TwilioProvider extends MessageProvider {
314
319
  mediaType,
315
320
  fileName: existingFileName || `${sanitizedBase}.${extension}`,
316
321
  fileSize: buffer.length,
317
- caption: messageData.message || '',
322
+ caption: messageData.body || '',
318
323
  metadata: {
319
324
  originalUrl: fileUrl,
320
325
  uploadedAt: new Date().toISOString()
@@ -25,7 +25,7 @@ const addInsAssistantController = async (req, res) => {
25
25
 
26
26
  try {
27
27
  const ans = await addInsAssistant(code, instruction);
28
- if (ans) await sendMessage({code, message: ans, fileType: 'text'});
28
+ if (ans) await sendMessage({code, body: ans, fileType: 'text'});
29
29
  return res.status(200).send({ message: 'Add instruction to the assistant' });
30
30
  } catch (error) {
31
31
  console.log(error);
@@ -38,7 +38,7 @@ const addMsgAssistantController = async (req, res) => {
38
38
 
39
39
  try {
40
40
  const ans = await addMsgAssistant(code, messages, reply);
41
- if (ans) await sendMessage({code, message: ans, fileType: 'text'});
41
+ if (ans) await sendMessage({code, body: ans, fileType: 'text'});
42
42
  return res.status(200).send({ message: 'Add message to the assistant' });
43
43
  } catch (error) {
44
44
  console.log(error);
@@ -66,7 +66,7 @@ const createAssistantController = async (req, res) => {
66
66
  console.log('messages', messages);
67
67
  for (const message of messages) {
68
68
  console.log('message', message);
69
- await sendMessage({code, message, fileType: 'text'});
69
+ await sendMessage({code, body: message, fileType: 'text'});
70
70
  }
71
71
  }
72
72
  res.status(200).send({ message: 'Create the assistant' });
@@ -260,12 +260,12 @@ const getConversationReplyController = async (req, res) => {
260
260
  }
261
261
 
262
262
  if (message && message.trim() !== '') {
263
- messageData.message = message;
263
+ messageData.body = message;
264
264
  }
265
265
  }
266
266
  // Handle text message
267
267
  else if (message) {
268
- messageData.message = message;
268
+ messageData.body = message;
269
269
  }
270
270
 
271
271
  console.log('Sending message with data:', JSON.stringify(messageData));
@@ -120,7 +120,10 @@ const sendMessageController = async (req, res) => {
120
120
  const scheduledRecord = await persistScheduledMessage(ScheduledMessageModel, payload);
121
121
  const result = hasScheduler
122
122
  ? await dependencies.sendScheduledMessage(scheduledRecord)
123
- : await dependencies.sendMessage(payload);
123
+ : await dependencies.sendMessage({
124
+ ...payload,
125
+ body: payload.message
126
+ });
124
127
 
125
128
  res.status(200).json({
126
129
  status: 200,
@@ -185,7 +188,10 @@ const sendBulkMessageController = async (req, res) => {
185
188
  const savedMessage = await persistScheduledMessage(ScheduledMessageModel, payload);
186
189
  const result = hasScheduler
187
190
  ? await dependencies.sendScheduledMessage(savedMessage)
188
- : await dependencies.sendMessage(payload);
191
+ : await dependencies.sendMessage({
192
+ ...payload,
193
+ body: payload.message
194
+ });
189
195
  return { result, scheduled: savedMessage };
190
196
  })
191
197
  );
@@ -289,7 +295,10 @@ const sendBulkMessageAirtableController = async (req, res) => {
289
295
  const savedMessage = await persistScheduledMessage(ScheduledMessageModel, payload);
290
296
  const result = hasScheduler
291
297
  ? await dependencies.sendScheduledMessage(savedMessage)
292
- : await dependencies.sendMessage(payload);
298
+ : await dependencies.sendMessage({
299
+ ...payload,
300
+ body: payload.message
301
+ });
293
302
  return { result, scheduled: savedMessage };
294
303
  })
295
304
  );
@@ -28,7 +28,7 @@ class MessageProvider {
28
28
  * Send a message
29
29
  * @param {Object} messageData - Message data
30
30
  * @param {string} messageData.code - Recipient
31
- * @param {string} messageData.message - Message text
31
+ * @param {string} messageData.body - Message text
32
32
  * @param {string} messageData.fileUrl - Optional file URL
33
33
  * @param {string} messageData.fileType - File type (text, image, document, audio)
34
34
  * @param {Object} messageData.variables - Template variables
@@ -364,7 +364,7 @@ class NexusMessaging {
364
364
 
365
365
  _extractAssistantInputs(messageData) {
366
366
  if (!messageData || typeof messageData !== 'object') {
367
- return { from: null, message: null };
367
+ return { from: null, body: null };
368
368
  }
369
369
 
370
370
  const from = [
@@ -380,7 +380,7 @@ class NexusMessaging {
380
380
  ].find((value) => typeof value === 'string' && value.trim().length > 0) || null;
381
381
 
382
382
  const message = [
383
- typeof messageData.message === 'string' ? messageData.message : null,
383
+ typeof messageData.body === 'string' ? messageData.body : null, // Unified body field for all message types
384
384
  typeof messageData.message?.conversation === 'string' ? messageData.message.conversation : null,
385
385
  typeof messageData.Body === 'string' ? messageData.Body : null,
386
386
  typeof messageData.raw?.message?.conversation === 'string' ? messageData.raw.message.conversation : null,
@@ -392,7 +392,7 @@ class NexusMessaging {
392
392
  : null
393
393
  ].find((value) => typeof value === 'string' && value.trim().length > 0) || null;
394
394
 
395
- return { from, message };
395
+ return { from, body: message };
396
396
  }
397
397
 
398
398
  async handleMessage(messageData) {
@@ -403,19 +403,24 @@ class NexusMessaging {
403
403
 
404
404
  async handleMessageWithAssistant(messageData) {
405
405
  try {
406
- const { from, message } = this._extractAssistantInputs(messageData);
406
+ const { from, body } = this._extractAssistantInputs(messageData);
407
407
 
408
- if (!from || !message) {
408
+ // Skip assistant processing for all interactive messages to avoid conflicts with predefined flows
409
+ if (messageData.isInteractive) {
410
+ return;
411
+ }
412
+
413
+ if (!from || !body) {
409
414
  console.warn('Unable to resolve assistant inputs from message, skipping automatic reply.');
410
415
  return;
411
416
  }
412
417
 
413
- const response = await replyAssistant(from, message);
418
+ const response = await replyAssistant(from, body);
414
419
 
415
420
  if (response) {
416
421
  await this.sendMessage({
417
422
  code: from,
418
- message: response,
423
+ body: response,
419
424
  processed: true
420
425
  });
421
426
  }
@@ -428,7 +433,24 @@ class NexusMessaging {
428
433
  if (this.messageStorage) {
429
434
  await this.messageStorage.saveInteractive(messageData);
430
435
  }
431
- return await this._handleWithPipeline('interactive', 'onInteractive', messageData);
436
+
437
+ this.events.emit('interactive:received', messageData);
438
+
439
+ // Run interactive handler if available
440
+ const result = await this._runPipeline('interactive', messageData, async (ctx) => {
441
+ const handler = this.handlers.onInteractive;
442
+ if (handler) {
443
+ return await handler(ctx, this);
444
+ }
445
+
446
+ // Interactive messages are handled by specific flows on other servers
447
+ // No assistant processing to avoid conflicts with predefined flows
448
+
449
+ return null;
450
+ });
451
+
452
+ this.events.emit('interactive:handled', messageData);
453
+ return result;
432
454
  }
433
455
 
434
456
  async handleMedia(messageData) {
@@ -441,7 +463,7 @@ class NexusMessaging {
441
463
 
442
464
  async handleMediaWithAssistant(messageData) {
443
465
  try {
444
- const { from, message } = this._extractAssistantInputs(messageData);
466
+ const { from, body } = this._extractAssistantInputs(messageData);
445
467
 
446
468
  if (!from) {
447
469
  console.warn('Unable to resolve sender for media message, skipping automatic reply.');
@@ -457,8 +479,8 @@ class NexusMessaging {
457
479
  return null;
458
480
  })();
459
481
 
460
- const fallbackMessage = message && message.trim().length > 0
461
- ? message
482
+ const fallbackMessage = body && body.trim().length > 0
483
+ ? body
462
484
  : `Media received (${mediaDescriptor || 'attachment'})`;
463
485
 
464
486
  const response = await replyAssistant(from, fallbackMessage);
@@ -466,7 +488,7 @@ class NexusMessaging {
466
488
  if (response) {
467
489
  await this.sendMessage({
468
490
  code: from,
469
- message: response
491
+ body: response
470
492
  });
471
493
  }
472
494
  } catch (error) {
@@ -508,8 +530,8 @@ class NexusMessaging {
508
530
  messageData.fileUrl = messageData.fileUrl || mediaPayload.metadata?.presignedUrl || null;
509
531
  messageData.fileType = messageData.fileType || mediaPayload.mediaType;
510
532
  messageData.caption = messageData.caption || primary.caption;
511
- if (!messageData.message && messageData.caption) {
512
- messageData.message = messageData.caption;
533
+ if (!messageData.body && messageData.caption) {
534
+ messageData.body = messageData.caption;
513
535
  }
514
536
  messageData.isMedia = true;
515
537
 
@@ -520,7 +542,7 @@ class NexusMessaging {
520
542
  const messageObj = convertTwilioToInternalFormat(raw);
521
543
  const values = getMessageValues(
522
544
  messageObj,
523
- messageData.message || messageData.caption || '',
545
+ messageData.body || messageData.caption || '',
524
546
  null,
525
547
  true
526
548
  );
@@ -605,7 +627,7 @@ class NexusMessaging {
605
627
  if (botResponse && this.provider) {
606
628
  await this.provider.sendMessage({
607
629
  to: chatId,
608
- message: botResponse,
630
+ body: botResponse,
609
631
  type: 'text',
610
632
  processed: true
611
633
  });
package/lib/index.js CHANGED
@@ -212,7 +212,7 @@ class Nexus {
212
212
  * Send a message
213
213
  * @param {Object} messageData - Message data
214
214
  * @param {string} messageData.code - Recipient phone number
215
- * @param {string} messageData.message - Message text
215
+ * @param {string} messageData.body - Message text
216
216
  * @param {string} [messageData.fileUrl] - Optional file URL
217
217
  * @param {string} [messageData.fileType] - File type
218
218
  * @param {Object} [messageData.variables] - Template variables
@@ -230,7 +230,7 @@ class Nexus {
230
230
  * Send a scheduled message
231
231
  * @param {Object} scheduledMessage - Scheduled message data
232
232
  * @param {string} scheduledMessage.code - Recipient phone number
233
- * @param {string} scheduledMessage.message - Message text
233
+ * @param {string} scheduledMessage.body - Message text
234
234
  * @param {Date|string} scheduledMessage.sendAt - When to send the message
235
235
  * @returns {Promise<Object>} Scheduled message result
236
236
  */
@@ -10,6 +10,12 @@ const messageSchema = new mongoose.Schema({
10
10
  message_id: { type: String, required: true },
11
11
  is_group: { type: Boolean, required: true },
12
12
  is_media: { type: Boolean, required: true },
13
+ is_interactive: { type: Boolean, default: false },
14
+ interaction_type: {
15
+ type: String,
16
+ enum: ['button', 'list', 'flow', 'quick_reply'],
17
+ default: null
18
+ },
13
19
  group_id: { type: String, default: null },
14
20
  reply_id: { type: String, default: null },
15
21
  processed: { type: Boolean, default: false },
@@ -64,6 +70,8 @@ async function insertMessage(values) {
64
70
  message_id: values.message_id,
65
71
  is_group: values.is_group,
66
72
  is_media: values.is_media,
73
+ is_interactive: values.is_interactive || false,
74
+ interaction_type: values.interaction_type || null,
67
75
  group_id: values.group_id,
68
76
  reply_id: values.reply_id,
69
77
  from_me: values.from_me,
@@ -339,7 +339,7 @@ const getThread = async (code, message = null) => {
339
339
 
340
340
  const getThreadInfo = async (code) => {
341
341
  try {
342
- let thread = await Thread.findOne({ code: code, active: true });
342
+ let thread = await Thread.findOne({ code: code });
343
343
  return thread;
344
344
  } catch (error) {
345
345
  console.log(error);
@@ -21,7 +21,7 @@ class MongoStorage {
21
21
  const interactionSchema = new mongoose.Schema({
22
22
  messageId: String,
23
23
  numero: String,
24
- interactionType: String, // 'button', 'list', 'flow'
24
+ interaction_type: String, // 'button', 'list', 'flow'
25
25
  payload: mongoose.Schema.Types.Mixed,
26
26
  timestamp: String,
27
27
  createdAt: { type: Date, default: Date.now }
@@ -67,7 +67,9 @@ class MongoStorage {
67
67
  provider: messageData?.provider || 'unknown',
68
68
  hasRaw: Boolean(messageData?.raw),
69
69
  hasMedia: Boolean(messageData?.media || messageData?.fileUrl),
70
- hasContentSid: Boolean(messageData?.contentSid)
70
+ hasContentSid: Boolean(messageData?.contentSid),
71
+ is_interactive: messageData.isInteractive,
72
+ interaction_type: messageData.interactionType
71
73
  });
72
74
  const enrichedMessage = await this._enrichTwilioMedia(messageData);
73
75
  const values = this.buildMessageValues(enrichedMessage);
@@ -135,7 +137,7 @@ class MongoStorage {
135
137
  fileUrl: undefined,
136
138
  fileType: mediaPayload.mediaType || messageData.fileType,
137
139
  isMedia: true,
138
- message: messageData.message || rawMessage.Body || primary.caption || '',
140
+ body: messageData.body || rawMessage.Body || primary.caption || '',
139
141
  caption: primary.caption || messageData.caption
140
142
  };
141
143
  } catch (error) {
@@ -179,7 +181,7 @@ class MongoStorage {
179
181
  const nombre = messageData.nombre_whatsapp || messageData.author || messageData.fromName || runtimeConfig.get('USER_DB_MONGO') || process.env.USER_DB_MONGO || 'Nexus';
180
182
 
181
183
  // Use message body directly (template rendering is now handled by the provider)
182
- let textBody = messageData.message || messageData.body;
184
+ let textBody = messageData.body;
183
185
 
184
186
  if (!textBody && isMedia) {
185
187
  textBody = `[Media:${messageData.fileType || 'attachment'}]`;
@@ -205,6 +207,8 @@ class MongoStorage {
205
207
  message_id: providerId,
206
208
  is_group: isGroup,
207
209
  is_media: isMedia,
210
+ is_interactive: messageData.isInteractive || false,
211
+ interaction_type: messageData.interactionType || null,
208
212
  group_id: isGroup ? normalizedNumero : null,
209
213
  reply_id: messageData.reply_id || messageData.replyId || null,
210
214
  from_me: messageData.fromMe !== undefined ? messageData.fromMe : true,
@@ -219,7 +223,7 @@ class MongoStorage {
219
223
  const interaction = new this.schemas.Interaction({
220
224
  messageId: interactionData.messageId,
221
225
  numero: interactionData.from,
222
- interactionType: interactionData.type, // 'button', 'list', 'flow'
226
+ interaction_type: interactionData.type, // 'button', 'list', 'flow'
223
227
  payload: interactionData.payload,
224
228
  timestamp: this.formatTimestamp(interactionData.timestamp)
225
229
  });
@@ -24,20 +24,34 @@ class MessageParser {
24
24
 
25
25
  // Check for interactive messages (buttons, lists, flows)
26
26
  if (this.isInteractiveMessage(rawMessage)) {
27
+ const interactive = this.parseInteractive(rawMessage);
28
+ const interactionText = this.extractInteractionText(interactive);
29
+
27
30
  return {
28
31
  ...messageData,
32
+ body: interactionText,
29
33
  type: 'interactive',
30
- interactive: this.parseInteractive(rawMessage)
34
+ interactive: interactive,
35
+ isInteractive: true, // Flag to indicate this is an interactive message
36
+ interactionType: interactive.type // Store the specific interaction type
31
37
  };
32
38
  }
33
39
 
34
40
  // Check for media messages
35
41
  if (this.isMediaMessage(rawMessage)) {
36
- return {
42
+ const mediaResult = {
37
43
  ...messageData,
38
44
  type: 'media',
39
45
  media: this.parseMedia(rawMessage)
40
46
  };
47
+
48
+ // Check if media message also has text content (caption)
49
+ const textContent = this.extractTextContent(rawMessage);
50
+ if (textContent) {
51
+ mediaResult.body = textContent;
52
+ }
53
+
54
+ return mediaResult;
41
55
  }
42
56
 
43
57
  // Parse text content
@@ -46,7 +60,7 @@ class MessageParser {
46
60
  return { ...messageData, type: 'unknown' };
47
61
  }
48
62
 
49
- messageData.message = textContent;
63
+ messageData.body = textContent;
50
64
 
51
65
  // Check for commands
52
66
  if (this.isCommand(textContent)) {
@@ -117,12 +131,19 @@ class MessageParser {
117
131
  }
118
132
 
119
133
  isInteractiveMessage(rawMessage) {
120
- // Twilio interactive messages
121
- return !!(rawMessage.ButtonPayload || rawMessage.ListId || rawMessage.FlowData || rawMessage.InteractiveData);
134
+ // Twilio interactive messages - check for any interactive indicators
135
+ return !!(
136
+ rawMessage.ButtonPayload ||
137
+ rawMessage.ListId ||
138
+ rawMessage.FlowData ||
139
+ rawMessage.InteractiveData ||
140
+ rawMessage.ButtonText
141
+ );
122
142
  }
123
143
 
124
144
  parseInteractive(rawMessage) {
125
- if (rawMessage.ButtonPayload) {
145
+ if (rawMessage.ButtonPayload !== undefined && rawMessage.ButtonPayload !== null && rawMessage.ButtonPayload.trim() !== '') {
146
+ // Non-empty payload = regular button
126
147
  return {
127
148
  type: 'button',
128
149
  payload: rawMessage.ButtonPayload,
@@ -130,6 +151,14 @@ class MessageParser {
130
151
  };
131
152
  }
132
153
 
154
+ if (rawMessage.ButtonText) {
155
+ return {
156
+ type: 'quick_reply',
157
+ title: rawMessage.ButtonText,
158
+ payload: rawMessage.ButtonText
159
+ };
160
+ }
161
+
133
162
  if (rawMessage.ListId) {
134
163
  return {
135
164
  type: 'list',
@@ -149,6 +178,33 @@ class MessageParser {
149
178
  return null;
150
179
  }
151
180
 
181
+ /**
182
+ * Extract interaction text for body field from interactive message
183
+ * @param {Object} interactive - Parsed interactive data
184
+ * @returns {string} Human-readable interaction text
185
+ */
186
+ extractInteractionText(interactive) {
187
+ if (!interactive) return '';
188
+
189
+ switch (interactive.type) {
190
+ case 'button':
191
+ return interactive.title || interactive.payload || '[Button clicked]';
192
+
193
+ case 'quick_reply':
194
+ return interactive.title || interactive.payload || '[Quick reply]';
195
+
196
+ case 'list':
197
+ return interactive.title || interactive.description || '[List item selected]';
198
+
199
+ case 'flow':
200
+ // Flows contain complex JSON data that doesn't need assistant processing
201
+ return '';
202
+
203
+ default:
204
+ return '[Interactive message]';
205
+ }
206
+ }
207
+
152
208
  isMediaMessage(rawMessage) {
153
209
  // Twilio format
154
210
  if (rawMessage.NumMedia && parseInt(rawMessage.NumMedia) > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "1.7.6",
3
+ "version": "1.7.8",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",