@peopl-health/nexus 1.2.0 → 1.3.1

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.
@@ -1,8 +1,11 @@
1
- const { TwilioProvider } = require('../adapters/TwilioProvider');
2
- const { BaileysProvider } = require('../adapters/BaileysProvider');
3
- const mongoose = require('mongoose');
4
1
  const { airtable, getBase } = require('../config/airtableConfig');
2
+ const { convertTwilioToInternalFormat } = require('../helpers/twilioHelper');
3
+ const { replyAssistant } = require('../services/assistantService');
4
+ const { createProvider } = require('../adapters/registry');
5
+
6
+ const mongoose = require('mongoose');
5
7
  const OpenAI = require('openai');
8
+ const EventEmitter = require('events');
6
9
 
7
10
  /**
8
11
  * Core messaging class that manages providers and message handling
@@ -23,6 +26,8 @@ class NexusMessaging {
23
26
  onKeyword: null,
24
27
  onFlow: null
25
28
  };
29
+ this.events = new EventEmitter();
30
+ this.middleware = { any: [], message: [], interactive: [], media: [], command: [], keyword: [], flow: [] };
26
31
  }
27
32
 
28
33
  /**
@@ -79,6 +84,54 @@ class NexusMessaging {
79
84
  return this.llmProvider;
80
85
  }
81
86
 
87
+ /**
88
+ * Get the underlying messaging provider instance
89
+ */
90
+ getProvider() {
91
+ return this.provider;
92
+ }
93
+
94
+ /**
95
+ * Access the internal event bus (EventEmitter)
96
+ */
97
+ getEventBus() {
98
+ return this.events;
99
+ }
100
+
101
+ /**
102
+ * Register middleware. Usage: use(type, fn) or use(fn) for global.
103
+ * Middleware signature: async (messageData, nexus, next) => {}
104
+ */
105
+ use(typeOrFn, maybeFn) {
106
+ if (typeof typeOrFn === 'function') {
107
+ this.middleware.any.push(typeOrFn);
108
+ return this;
109
+ }
110
+ const type = String(typeOrFn || '').toLowerCase();
111
+ const fn = maybeFn;
112
+ if (!this.middleware[type]) this.middleware[type] = [];
113
+ this.middleware[type].push(fn);
114
+ return this;
115
+ }
116
+
117
+ /**
118
+ * Internal helper to run middleware pipeline and the final handler
119
+ */
120
+ async _runPipeline(type, messageData, finalHandler) {
121
+ const chain = [...(this.middleware.any || []), ...(this.middleware[type] || []), async (ctx) => { return finalHandler(ctx); }];
122
+ let idx = -1;
123
+ const runner = async (i) => {
124
+ if (i <= idx) throw new Error('next() called multiple times');
125
+ idx = i;
126
+ const fn = chain[i];
127
+ if (!fn) return;
128
+ return await fn(messageData, this, () => runner(i+1));
129
+ };
130
+ return await runner(0);
131
+ }
132
+
133
+
134
+
82
135
  /**
83
136
  * Initialize Nexus with all services
84
137
  * @param {Object} options - Configuration options
@@ -122,17 +175,7 @@ class NexusMessaging {
122
175
  * @param {Object} providerConfig - Provider-specific configuration
123
176
  */
124
177
  async initializeProvider(providerType, providerConfig) {
125
- switch (providerType.toLowerCase()) {
126
- case 'twilio':
127
- this.provider = new TwilioProvider(providerConfig);
128
- break;
129
- case 'baileys':
130
- this.provider = new BaileysProvider(providerConfig);
131
- break;
132
- default:
133
- throw new Error(`Unsupported provider: ${providerType}`);
134
- }
135
-
178
+ this.provider = createProvider(providerType, providerConfig);
136
179
  await this.provider.initialize();
137
180
  }
138
181
 
@@ -209,12 +252,18 @@ class NexusMessaging {
209
252
  throw new Error('No provider initialized');
210
253
  }
211
254
 
212
- const result = await this.provider.sendMessage(messageData);
255
+ // Backward compatibility: accept `code` as destination
256
+ const normalized = { ...messageData };
257
+ if (!normalized.to && normalized.code) {
258
+ normalized.to = normalized.code;
259
+ }
260
+
261
+ const result = await this.provider.sendMessage(normalized);
213
262
 
214
263
  // Store message if storage is configured
215
264
  if (this.messageStorage) {
216
265
  await this.messageStorage.saveMessage({
217
- ...messageData,
266
+ ...normalized,
218
267
  messageId: result.messageId,
219
268
  provider: result.provider,
220
269
  timestamp: new Date(),
@@ -268,44 +317,111 @@ class NexusMessaging {
268
317
  }
269
318
 
270
319
  async handleMessage(messageData) {
271
- if (this.handlers.onMessage) {
272
- return await this.handlers.onMessage(messageData, this);
320
+ this.events.emit && this.events.emit('message:received', messageData);
321
+ const final = async (ctx) => {
322
+ if (this.handlers.onMessage) {
323
+ return await this.handlers.onMessage(ctx, this);
324
+ } else {
325
+ return await this.handleMessageWithAssistant(ctx);
326
+ }
327
+ };
328
+ const result = await this._runPipeline('message', messageData, final);
329
+ this.events.emit && this.events.emit('message:handled', messageData);
330
+ return result;
331
+ }
332
+
333
+ async handleMessageWithAssistant(messageData) {
334
+ try {
335
+ // Convert Twilio format to internal format if needed
336
+ const internalMessage = this.provider.constructor.name === 'TwilioProvider'
337
+ ? convertTwilioToInternalFormat(messageData)
338
+ : messageData;
339
+
340
+ // Extract standardized data
341
+ const extractedData = {
342
+ from: internalMessage.key?.remoteJid || '',
343
+ message: internalMessage.message?.conversation || '',
344
+ messageId: internalMessage.key?.id || '',
345
+ fromMe: internalMessage.key?.fromMe || false
346
+ };
347
+
348
+ const response = await replyAssistant(
349
+ extractedData.from,
350
+ extractedData.message
351
+ );
352
+
353
+ if (response) {
354
+ await this.sendMessage({
355
+ to: extractedData.from,
356
+ message: response
357
+ });
358
+ }
359
+ } catch (error) {
360
+ console.error('Error in handleMessageWithAssistant:', error);
273
361
  }
274
362
  }
275
363
 
276
364
  async handleInteractive(messageData) {
277
- // Store interactive message
278
365
  if (this.messageStorage) {
279
366
  await this.messageStorage.saveInteractive(messageData);
280
367
  }
281
-
282
- if (this.handlers.onInteractive) {
283
- return await this.handlers.onInteractive(messageData, this);
284
- }
368
+ this.events.emit && this.events.emit('interactive:received', messageData);
369
+ const final = async (ctx) => {
370
+ if (this.handlers.onInteractive) {
371
+ return await this.handlers.onInteractive(ctx, this);
372
+ }
373
+ };
374
+ const result = await this._runPipeline('interactive', messageData, final);
375
+ this.events.emit && this.events.emit('interactive:handled', messageData);
376
+ return result;
285
377
  }
286
378
 
287
379
  async handleMedia(messageData) {
288
- if (this.handlers.onMedia) {
289
- return await this.handlers.onMedia(messageData, this);
290
- }
380
+ this.events.emit && this.events.emit('media:received', messageData);
381
+ const final = async (ctx) => {
382
+ if (this.handlers.onMedia) {
383
+ return await this.handlers.onMedia(ctx, this);
384
+ }
385
+ };
386
+ const result = await this._runPipeline('media', messageData, final);
387
+ this.events.emit && this.events.emit('media:handled', messageData);
388
+ return result;
291
389
  }
292
390
 
293
391
  async handleCommand(messageData) {
294
- if (this.handlers.onCommand) {
295
- return await this.handlers.onCommand(messageData, this);
296
- }
392
+ this.events.emit && this.events.emit('command:received', messageData);
393
+ const final = async (ctx) => {
394
+ if (this.handlers.onCommand) {
395
+ return await this.handlers.onCommand(ctx, this);
396
+ }
397
+ };
398
+ const result = await this._runPipeline('command', messageData, final);
399
+ this.events.emit && this.events.emit('command:handled', messageData);
400
+ return result;
297
401
  }
298
402
 
299
403
  async handleKeyword(messageData) {
300
- if (this.handlers.onKeyword) {
301
- return await this.handlers.onKeyword(messageData, this);
302
- }
404
+ this.events.emit && this.events.emit('keyword:received', messageData);
405
+ const final = async (ctx) => {
406
+ if (this.handlers.onKeyword) {
407
+ return await this.handlers.onKeyword(ctx, this);
408
+ }
409
+ };
410
+ const result = await this._runPipeline('keyword', messageData, final);
411
+ this.events.emit && this.events.emit('keyword:handled', messageData);
412
+ return result;
303
413
  }
304
414
 
305
415
  async handleFlow(messageData) {
306
- if (this.handlers.onFlow) {
307
- return await this.handlers.onFlow(messageData, this);
308
- }
416
+ this.events.emit && this.events.emit('flow:received', messageData);
417
+ const final = async (ctx) => {
418
+ if (this.handlers.onFlow) {
419
+ return await this.handlers.onFlow(ctx, this);
420
+ }
421
+ };
422
+ const result = await this._runPipeline('flow', messageData, final);
423
+ this.events.emit && this.events.emit('flow:handled', messageData);
424
+ return result;
309
425
  }
310
426
 
311
427
  /**
@@ -1,7 +1,7 @@
1
1
  const { downloadFileFromS3, generatePresignedUrl } = require('../config/awsConfig.js');
2
2
  const { openaiClient } = require('../config/llmConfig.js');
3
3
 
4
- const { LegacyMessage } = require('../models/messageModel.js');
4
+ const { Message } = require('../models/messageModel.js');
5
5
 
6
6
  const { convertPdfToImages } = require('./filesHelper.js');
7
7
  const { analyzeImage } = require('../helpers/llmsHelper.js');
@@ -78,7 +78,7 @@ async function getLastMessages(code) {
78
78
  query.is_group = false;
79
79
  }
80
80
 
81
- const lastMessages = await LegacyMessage.find(query).sort({ timestamp: -1 });
81
+ const lastMessages = await Message.find(query).sort({ timestamp: -1 });
82
82
  console.log('[getLastMessages] lastMessages', lastMessages.map(msg => msg.body).join('\n\n'));
83
83
 
84
84
  if (lastMessages.length === 0) return [];
@@ -86,7 +86,7 @@ async function getLastMessages(code) {
86
86
  let patientReply = [];
87
87
  for (const message of lastMessages) {
88
88
  patientReply.push(message);
89
- await LegacyMessage.updateOne(
89
+ await Message.updateOne(
90
90
  { message_id: message.message_id, timestamp: message.timestamp },
91
91
  { $set: { processed: true } }
92
92
  );
@@ -101,7 +101,7 @@ async function getLastMessages(code) {
101
101
 
102
102
  async function getLastNMessages(code, n) {
103
103
  try {
104
- const lastMessages = await LegacyMessage.find({ numero: code })
104
+ const lastMessages = await Message.find({ numero: code })
105
105
  .sort({ timestamp: -1 })
106
106
  .limit(n);
107
107
 
@@ -149,7 +149,7 @@ function formatMessage(reply) {
149
149
  }
150
150
 
151
151
  async function downloadMediaAndCreateFile(code, reply) {
152
- const resultMedia = await LegacyMessage.findOne({
152
+ const resultMedia = await Message.findOne({
153
153
  message_id: reply.message_id,
154
154
  timestamp: reply.timestamp,
155
155
  media: { $ne: null }
@@ -259,7 +259,7 @@ async function processMessage(code, reply, thread) {
259
259
 
260
260
  console.log('Formatted message:', formattedMessage);
261
261
 
262
- await LegacyMessage.updateOne(
262
+ await Message.updateOne(
263
263
  { message_id: reply.message_id, timestamp: reply.timestamp },
264
264
  { $set: { assistant_id: thread.assistant_id, thread_id: thread.thread_id } }
265
265
  );
@@ -1,4 +1,4 @@
1
- const { LegacyMessage, insertMessage, getMessageValues } = require('../models/messageModel.js');
1
+ const { Message, insertMessage, getMessageValues } = require('../models/messageModel.js');
2
2
  const { uploadMediaToS3 } = require('./mediaHelper.js');
3
3
  const { downloadMediaMessage } = require('baileys');
4
4
 
@@ -103,7 +103,7 @@ function extractContentTypeAndReply(message, messageType) {
103
103
  async function isRecentMessage(chatId) {
104
104
  const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
105
105
 
106
- const recentMessage = await LegacyMessage.find({
106
+ const recentMessage = await Message.find({
107
107
  $or: [{ group_id: chatId }, { numero: chatId }],
108
108
  timestamp: { $gte: fiveMinutesAgo.toISOString() }
109
109
  }).sort({ timestamp: -1 }).limit(1);
@@ -112,7 +112,7 @@ async function isRecentMessage(chatId) {
112
112
  }
113
113
 
114
114
  async function getLastMessages(chatId, n) {
115
- const messages = await LegacyMessage.find({ group_id: chatId })
115
+ const messages = await Message.find({ group_id: chatId })
116
116
  .sort({ timestamp: -1 })
117
117
  .limit(n)
118
118
  .select('timestamp numero nombre_whatsapp body');
@@ -1,4 +1,4 @@
1
- const { LegacyMessage } = require('../models/messageModel');
1
+ const { Message } = require('../models/messageModel');
2
2
 
3
3
  const axios = require('axios');
4
4
  const { v4: uuidv4 } = require('uuid');
@@ -68,7 +68,7 @@ function extractTitle(message, mediaType) {
68
68
  async function isRecentMessage(chatId) {
69
69
  const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
70
70
 
71
- const recentMessage = await LegacyMessage.find({
71
+ const recentMessage = await Message.find({
72
72
  $or: [{ group_id: chatId }, { numero: chatId }],
73
73
  timestamp: { $gte: fiveMinutesAgo.toISOString() }
74
74
  }).sort({ timestamp: -1 }).limit(1);
@@ -78,7 +78,7 @@ async function isRecentMessage(chatId) {
78
78
 
79
79
 
80
80
  async function getLastMessages(chatId, n) {
81
- const messages = await LegacyMessage.find({ numero: chatId })
81
+ const messages = await Message.find({ numero: chatId })
82
82
  .sort({ timestamp: -1 })
83
83
  .limit(n)
84
84
  .select('timestamp numero nombre_whatsapp body');
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- declare module '@peopl/nexus' {
1
+ declare module '@peopl-health/nexus' {
2
2
  import { EventEmitter } from 'events';
3
3
  import mongoose from 'mongoose';
4
4
 
package/lib/index.js CHANGED
@@ -1,9 +1,19 @@
1
1
  const { NexusMessaging } = require('./core/NexusMessaging');
2
- const { TwilioProvider } = require('./adapters/TwilioProvider');
3
- const { BaileysProvider } = require('./adapters/BaileysProvider');
4
2
  const { MongoStorage } = require('./storage/MongoStorage');
5
3
  const { MessageParser } = require('./utils/messageParser');
6
4
  const { DefaultLLMProvider } = require('./utils/defaultLLMProvider');
5
+ const { loadNexusConfig } = require('./config/configLoader');
6
+ const templateController = require('./controllers/templateController');
7
+ const templateFlowController = require('./controllers/templateFlowController');
8
+ const interactive = require('./interactive');
9
+ const {
10
+ configureLLMProvider: configureAssistantsLLM,
11
+ registerAssistant,
12
+ overrideGetAssistantById,
13
+ configureAssistants: setAssistantsConfig
14
+ } = require('./services/assistantService');
15
+ const { TwilioProvider } = require('./adapters/TwilioProvider');
16
+ const { BaileysProvider } = require('./adapters/BaileysProvider');
7
17
 
8
18
  /**
9
19
  * Main Nexus class that orchestrates all components
@@ -40,17 +50,50 @@ class Nexus {
40
50
  parser = 'MessageParser',
41
51
  parserConfig = {},
42
52
  llm = 'openai',
43
- llmConfig = {}
53
+ llmConfig = {},
54
+ assistants: assistantsOpt = undefined,
55
+ assistant: assistantOpt = undefined
44
56
  } = options;
45
57
 
46
58
  // Initialize messaging provider
47
59
  await this.messaging.initializeProvider(provider, providerConfig);
48
60
 
61
+ // Auto-configure template controllers with active provider when Twilio is used
62
+ try {
63
+ const activeProvider = typeof this.messaging.getProvider === 'function'
64
+ ? this.messaging.getProvider()
65
+ : null;
66
+ if (activeProvider && activeProvider.constructor && activeProvider.constructor.name === 'TwilioProvider') {
67
+ if (typeof templateController.configureNexusProvider === 'function') {
68
+ templateController.configureNexusProvider(activeProvider);
69
+ }
70
+ if (typeof templateFlowController.configureNexusProvider === 'function') {
71
+ templateFlowController.configureNexusProvider(activeProvider);
72
+ }
73
+ }
74
+ } catch (e) {
75
+ console.warn('Warning: failed to auto-configure template providers:', e?.message || e);
76
+ }
77
+
49
78
  // Initialize storage if provided
50
- if (storage === 'mongo') {
51
- this.storage = new MongoStorage(storageConfig);
52
- await this.storage.connect();
53
- this.messaging.setMessageStorage(this.storage);
79
+ if (storage) {
80
+ try {
81
+ const { createStorage } = require('./storage/registry');
82
+ if (typeof storage === 'string') {
83
+ this.storage = createStorage(storage, storageConfig || {});
84
+ } else if (typeof storage === 'object' && (storage.saveMessage || storage.saveInteractive)) {
85
+ this.storage = storage; // external adapter instance
86
+ } else {
87
+ // default to mongo if truthy but not recognized
88
+ this.storage = createStorage('mongo', storageConfig || {});
89
+ }
90
+ if (this.storage && typeof this.storage.connect === 'function') {
91
+ await this.storage.connect();
92
+ }
93
+ this.messaging.setMessageStorage(this.storage);
94
+ } catch (e) {
95
+ console.warn('Warning: storage initialization failed:', e?.message || e);
96
+ }
54
97
  }
55
98
 
56
99
  // Initialize message parser if provided
@@ -63,6 +106,31 @@ class Nexus {
63
106
  this.llmProvider = new DefaultLLMProvider(llmConfig);
64
107
  }
65
108
 
109
+
110
+ // Configure Assistants (registry + overrides)
111
+ const assistantsConfig = assistantsOpt || assistantOpt;
112
+ try {
113
+ if (this.llmProvider && typeof configureAssistantsLLM === 'function') {
114
+ // Provide the raw OpenAI client to the assistant service
115
+ configureAssistantsLLM(this.llmProvider.getClient());
116
+ }
117
+ if (assistantsConfig) {
118
+ if (assistantsConfig.registry && typeof assistantsConfig.registry === 'object') {
119
+ for (const [id, AssistantClass] of Object.entries(assistantsConfig.registry)) {
120
+ registerAssistant(id, AssistantClass);
121
+ }
122
+ }
123
+ if (typeof assistantsConfig.getAssistantById === 'function') {
124
+ overrideGetAssistantById(assistantsConfig.getAssistantById);
125
+ }
126
+ if (typeof setAssistantsConfig === 'function') {
127
+ setAssistantsConfig(assistantsConfig);
128
+ }
129
+ }
130
+ } catch (e) {
131
+ console.warn('Warning: failed to configure assistants:', e?.message || e);
132
+ }
133
+
66
134
  this.isInitialized = true;
67
135
  }
68
136
 
@@ -193,7 +261,14 @@ module.exports = {
193
261
  MessageParser,
194
262
  DefaultLLMProvider,
195
263
  routes,
196
- // Direct access to route utilities for convenience
197
264
  setupDefaultRoutes: routes.setupDefaultRoutes,
198
- createRouter: routes.createRouter
265
+ createRouter: routes.createRouter,
266
+ loadNexusConfig,
267
+ interactive,
268
+ registerFlow: interactive.registerFlow,
269
+ getFlow: interactive.getFlow,
270
+ listFlows: interactive.listFlows,
271
+ sendInteractive: interactive.sendInteractive,
272
+ registerInteractiveHandler: interactive.registerInteractiveHandler,
273
+ attachInteractiveRouter: interactive.attachInteractiveRouter
199
274
  };
@@ -0,0 +1,86 @@
1
+
2
+ const { toTwilioContent } = require('./twilioMapper');
3
+ const { registerFlow, getFlow, listFlows, registerInteractiveHandler, listInteractiveHandlers } = require('./registry');
4
+
5
+ async function sendInteractive(nexusOrMessaging, params) {
6
+ const { to, spec, id, variables } = params || {};
7
+ if (!nexusOrMessaging) throw new Error('sendInteractive requires a Nexus or NexusMessaging instance');
8
+ const messaging = typeof nexusOrMessaging.getMessaging === 'function' ? nexusOrMessaging.getMessaging() : nexusOrMessaging;
9
+ const provider = typeof messaging.getProvider === 'function' ? messaging.getProvider() : null;
10
+ if (!provider) throw new Error('No active provider');
11
+
12
+ const useSpec = spec || (id ? getFlow(id) : null);
13
+ if (!useSpec) throw new Error('Interactive spec not found');
14
+
15
+ // If user supplied a contentSid directly in spec, just send it
16
+ if (useSpec.contentSid) {
17
+ return await provider.sendMessage({ to, contentSid: useSpec.contentSid, variables });
18
+ }
19
+
20
+ // Twilio mapping
21
+ if (provider.constructor && provider.constructor.name === 'TwilioProvider') {
22
+ const content = toTwilioContent(useSpec);
23
+ const created = await provider.createTemplate(content);
24
+ return await provider.sendMessage({ to, contentSid: created.sid, variables });
25
+ }
26
+
27
+ // Baileys or others: not supported yet
28
+ throw new Error('Interactive/flows not supported for this provider');
29
+ }
30
+
31
+ module.exports = {
32
+ registerFlow,
33
+ getFlow,
34
+ listFlows,
35
+ sendInteractive,
36
+ registerInteractiveHandler,
37
+ attachInteractiveRouter
38
+ };
39
+
40
+
41
+ function _matchInteractive(match, interactive) {
42
+ if (!match) return true;
43
+ if (!interactive) return false;
44
+ // Type match (button, list, flow)
45
+ if (match.type && String(match.type).toLowerCase() !== String(interactive.type || '').toLowerCase()) return false;
46
+ // ID match (e.g., ListId)
47
+ if (match.id) {
48
+ const targetId = interactive.id || interactive.payload || interactive.title || '';
49
+ if (match.id instanceof RegExp) {
50
+ if (!match.id.test(String(targetId))) return false;
51
+ } else if (String(match.id) != String(targetId)) {
52
+ return false;
53
+ }
54
+ }
55
+ // Payload includes
56
+ if (match.payloadIncludes) {
57
+ const blob = JSON.stringify(interactive);
58
+ if (!blob.includes(match.payloadIncludes)) return false;
59
+ }
60
+ // Custom predicate
61
+ if (typeof match.predicate === 'function' && !match.predicate(interactive)) return false;
62
+ return true;
63
+ }
64
+
65
+ function attachInteractiveRouter(nexusOrMessaging) {
66
+ const messaging = typeof nexusOrMessaging.getMessaging === 'function' ? nexusOrMessaging.getMessaging() : nexusOrMessaging;
67
+ const bus = typeof messaging.getEventBus === 'function' ? messaging.getEventBus() : null;
68
+ if (!bus) throw new Error('Interactive router requires event bus support');
69
+
70
+ const handler = async (messageData) => {
71
+ const interactive = messageData && messageData.interactive ? messageData.interactive : null;
72
+ const items = listInteractiveHandlers();
73
+ for (const { match, handler } of items) {
74
+ try {
75
+ if (_matchInteractive(match, interactive)) {
76
+ await handler(messageData, messaging);
77
+ }
78
+ } catch (e) {
79
+ console.warn('Interactive handler error:', e && e.message || e);
80
+ }
81
+ }
82
+ };
83
+
84
+ bus.on('interactive:received', handler);
85
+ return () => bus.off && bus.off('interactive:received', handler);
86
+ }
@@ -0,0 +1,31 @@
1
+
2
+ // Simple in-memory registry for interactive specs (flows, quick replies)
3
+ const _flows = new Map();
4
+ const _handlers = [];
5
+
6
+ function registerFlow(id, spec) {
7
+ if (!id || typeof id !== 'string') throw new Error('registerFlow requires id');
8
+ if (!spec || typeof spec !== 'object') throw new Error('registerFlow requires spec object');
9
+ _flows.set(id, spec);
10
+ }
11
+
12
+ function getFlow(id) {
13
+ return _flows.get(id) || null;
14
+ }
15
+
16
+ function listFlows() {
17
+ return Array.from(_flows.entries()).map(([id, spec]) => ({ id, spec }));
18
+ }
19
+
20
+ module.exports = { registerFlow, getFlow, listFlows, registerInteractiveHandler, listInteractiveHandlers };
21
+
22
+
23
+ function registerInteractiveHandler(match, handler) {
24
+ if (typeof handler !== 'function') throw new Error('Handler must be a function');
25
+ const m = match || {};
26
+ _handlers.push({ match: m, handler });
27
+ }
28
+
29
+ function listInteractiveHandlers() {
30
+ return _handlers.slice();
31
+ }
@@ -0,0 +1,60 @@
1
+
2
+ // Map a provider-agnostic interactive spec to Twilio Content API payload
3
+
4
+ function toTwilioContent(spec) {
5
+ if (!spec || typeof spec !== 'object') throw new Error('Interactive spec must be an object');
6
+ const { type = 'text', language = 'es', body = '', footer, variables, buttons, flow } = spec;
7
+
8
+ const content = {
9
+ friendly_name: spec.friendlyName || (spec.name || ('interactive_' + Date.now())),
10
+ language,
11
+ variables: {},
12
+ types: {}
13
+ };
14
+
15
+ // Build variables map from array/object
16
+ if (Array.isArray(variables)) {
17
+ variables.forEach((v, idx) => {
18
+ content.variables[String(idx + 1)] = typeof v === 'string' ? v : (v.example || '');
19
+ });
20
+ } else if (variables && typeof variables === 'object') {
21
+ Object.keys(variables).forEach(k => {
22
+ content.variables[k] = variables[k];
23
+ });
24
+ }
25
+
26
+ if (type === 'flow') {
27
+ content.types['twilio/flows'] = {
28
+ body: flow?.body || body || '',
29
+ button_text: flow?.buttonText || spec.buttonText || undefined,
30
+ subtitle: flow?.subtitle || spec.subtitle || undefined,
31
+ pages: flow?.pages || spec.pages || []
32
+ };
33
+ } else if (type === 'quick-reply') {
34
+ const actions = (buttons || []).slice(0, 3).map((b, i) => ({
35
+ title: b.text || b.title || ('Button ' + (i + 1)),
36
+ id: b.id || ('button_' + (i + 1))
37
+ }));
38
+ content.types['twilio/quick-reply'] = {
39
+ body: body || '',
40
+ actions
41
+ };
42
+ // Also include base text body type
43
+ content.types['twilio/text'] = { body: body || '' };
44
+ } else {
45
+ // Plain text
46
+ content.types['twilio/text'] = { body: body || '' };
47
+ }
48
+
49
+ if (footer) {
50
+ ['twilio/text', 'twilio/quick-reply'].forEach(t => {
51
+ if (content.types[t]) {
52
+ content.types[t].footer = footer;
53
+ }
54
+ });
55
+ }
56
+
57
+ return content;
58
+ }
59
+
60
+ module.exports = { toTwilioContent };