@conversionpros/aiva 1.0.0 → 2.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.
Files changed (150) hide show
  1. package/bin/aiva.js +26 -14
  2. package/lib/bluebubbles.js +145 -0
  3. package/lib/config-gen.js +253 -0
  4. package/lib/config.js +1 -1
  5. package/lib/constants.js +72 -0
  6. package/lib/launch-agent.js +112 -0
  7. package/lib/prerequisites.js +236 -0
  8. package/lib/process.js +59 -145
  9. package/lib/setup.js +224 -194
  10. package/lib/validate.js +194 -0
  11. package/package.json +7 -32
  12. package/auto-deploy.js +0 -190
  13. package/cli-sync.js +0 -126
  14. package/d2a-prompt-template.txt +0 -106
  15. package/diagnostics-api.js +0 -304
  16. package/docs/ara-dedup-fix-scope.md +0 -112
  17. package/docs/ara-fix-round2-scope.md +0 -61
  18. package/docs/ara-greeting-fix-scope.md +0 -70
  19. package/docs/calendar-date-fix-scope.md +0 -28
  20. package/docs/getting-started.md +0 -115
  21. package/docs/network-architecture-rollout-scope.md +0 -43
  22. package/docs/scope-google-oauth-integration.md +0 -351
  23. package/docs/settings-page-scope.md +0 -50
  24. package/docs/xai-imagine-scope.md +0 -116
  25. package/docs/xai-voice-integration-scope.md +0 -115
  26. package/docs/xai-voice-tools-scope.md +0 -165
  27. package/email-router.js +0 -512
  28. package/follow-up-handler.js +0 -606
  29. package/gateway-monitor.js +0 -158
  30. package/google-email.js +0 -379
  31. package/google-oauth.js +0 -310
  32. package/grok-imagine.js +0 -97
  33. package/health-reporter.js +0 -287
  34. package/invisible-prefix-base.txt +0 -206
  35. package/invisible-prefix-owner.txt +0 -26
  36. package/invisible-prefix-slim.txt +0 -10
  37. package/invisible-prefix.txt +0 -43
  38. package/knowledge-base.js +0 -472
  39. package/lib/cli.js +0 -19
  40. package/lib/server.js +0 -42
  41. package/meta-capi.js +0 -206
  42. package/meta-leads.js +0 -411
  43. package/notion-oauth.js +0 -323
  44. package/public/agent-config.html +0 -241
  45. package/public/aiva-avatar-anime.png +0 -0
  46. package/public/css/docs.css.bak +0 -688
  47. package/public/css/onboarding.css +0 -543
  48. package/public/diagrams/claude-subscription-pool.html +0 -329
  49. package/public/diagrams/claude-subscription-pool.png +0 -0
  50. package/public/docs-icon.png +0 -0
  51. package/public/escalation.html +0 -237
  52. package/public/group-config.html +0 -300
  53. package/public/icon-192.png +0 -0
  54. package/public/icon-512.png +0 -0
  55. package/public/icons/agents.svg +0 -1
  56. package/public/icons/attach.svg +0 -1
  57. package/public/icons/characters.svg +0 -1
  58. package/public/icons/chat.svg +0 -1
  59. package/public/icons/docs.svg +0 -1
  60. package/public/icons/heartbeat.svg +0 -1
  61. package/public/icons/messages.svg +0 -1
  62. package/public/icons/mic.svg +0 -1
  63. package/public/icons/notes.svg +0 -1
  64. package/public/icons/settings.svg +0 -1
  65. package/public/icons/tasks.svg +0 -1
  66. package/public/images/onboarding/p0-communication-layer.png +0 -0
  67. package/public/images/onboarding/p0-infinite-surface.png +0 -0
  68. package/public/images/onboarding/p0-learning-model.png +0 -0
  69. package/public/images/onboarding/p0-meet-aiva.png +0 -0
  70. package/public/images/onboarding/p4-contact-intelligence.png +0 -0
  71. package/public/images/onboarding/p4-context-compounds.png +0 -0
  72. package/public/images/onboarding/p4-message-router.png +0 -0
  73. package/public/images/onboarding/p4-per-contact-rules.png +0 -0
  74. package/public/images/onboarding/p4-send-messages.png +0 -0
  75. package/public/images/onboarding/p6-be-precise.png +0 -0
  76. package/public/images/onboarding/p6-review-escalations.png +0 -0
  77. package/public/images/onboarding/p6-voice-input.png +0 -0
  78. package/public/images/onboarding/p7-completion.png +0 -0
  79. package/public/index.html +0 -11594
  80. package/public/js/onboarding.js +0 -699
  81. package/public/manifest.json +0 -24
  82. package/public/messages-v2.html +0 -2824
  83. package/public/permission-approve.html.bak +0 -107
  84. package/public/permissions.html +0 -150
  85. package/public/styles/design-system.css +0 -68
  86. package/router-db.js +0 -604
  87. package/router-utils.js +0 -28
  88. package/router-v2/adapters/imessage.js +0 -191
  89. package/router-v2/adapters/quo.js +0 -82
  90. package/router-v2/adapters/whatsapp.js +0 -192
  91. package/router-v2/contact-manager.js +0 -234
  92. package/router-v2/conversation-engine.js +0 -498
  93. package/router-v2/data/knowledge-base.json +0 -176
  94. package/router-v2/data/router-v2.db +0 -0
  95. package/router-v2/data/router-v2.db-shm +0 -0
  96. package/router-v2/data/router-v2.db-wal +0 -0
  97. package/router-v2/data/router.db +0 -0
  98. package/router-v2/db.js +0 -457
  99. package/router-v2/escalation-bridge.js +0 -540
  100. package/router-v2/follow-up-engine.js +0 -347
  101. package/router-v2/index.js +0 -441
  102. package/router-v2/ingestion.js +0 -213
  103. package/router-v2/knowledge-base.js +0 -231
  104. package/router-v2/lead-qualifier.js +0 -152
  105. package/router-v2/learning-loop.js +0 -202
  106. package/router-v2/outbound-sender.js +0 -160
  107. package/router-v2/package.json +0 -13
  108. package/router-v2/permission-gate.js +0 -86
  109. package/router-v2/playbook.js +0 -177
  110. package/router-v2/prompts/base.js +0 -52
  111. package/router-v2/prompts/first-contact.js +0 -38
  112. package/router-v2/prompts/lead-qualification.js +0 -37
  113. package/router-v2/prompts/scheduling.js +0 -72
  114. package/router-v2/prompts/style-overrides.js +0 -22
  115. package/router-v2/scheduler.js +0 -301
  116. package/router-v2/scripts/migrate-v1-to-v2.js +0 -215
  117. package/router-v2/scripts/seed-faq.js +0 -67
  118. package/router-v2/seed-knowledge-base.js +0 -39
  119. package/router-v2/utils/ai.js +0 -129
  120. package/router-v2/utils/phone.js +0 -52
  121. package/router-v2/utils/response-validator.js +0 -98
  122. package/router-v2/utils/sanitize.js +0 -222
  123. package/router.js +0 -5005
  124. package/routes/google-calendar.js +0 -186
  125. package/scripts/deploy.sh +0 -62
  126. package/scripts/macos-calendar.sh +0 -232
  127. package/scripts/onboard-device.sh +0 -466
  128. package/server.js +0 -5131
  129. package/start.sh +0 -24
  130. package/templates/AGENTS.md +0 -548
  131. package/templates/IDENTITY.md +0 -15
  132. package/templates/docs-agents.html +0 -132
  133. package/templates/docs-app.html +0 -130
  134. package/templates/docs-home.html +0 -83
  135. package/templates/docs-imessage.html +0 -121
  136. package/templates/docs-tasks.html +0 -123
  137. package/templates/docs-tips.html +0 -175
  138. package/templates/getting-started.html +0 -809
  139. package/templates/invisible-prefix-base.txt +0 -171
  140. package/templates/invisible-prefix-owner.txt +0 -282
  141. package/templates/invisible-prefix.txt +0 -338
  142. package/templates/manifest.json +0 -61
  143. package/templates/memory-org/clients.md +0 -7
  144. package/templates/memory-org/credentials.md +0 -9
  145. package/templates/memory-org/devices.md +0 -7
  146. package/templates/updates.html +0 -464
  147. package/tts-proxy.js +0 -96
  148. package/voice-call-local.js +0 -731
  149. package/voice-call.js +0 -732
  150. package/wa-listener.js +0 -354
@@ -1,441 +0,0 @@
1
- // ── Router v2 Entry Point - Express Routes + Initialization ──
2
- 'use strict';
3
-
4
- const { initDatabase, getSetting, setSetting, getStmts } = require('./db');
5
- const { ingestMessage, startBBPolling, stopBBPolling } = require('./ingestion');
6
- const { processMessage } = require('./conversation-engine');
7
- const { handleReply, processTimeouts } = require('./escalation-bridge');
8
- const { processFollowUps } = require('./follow-up-engine');
9
- const { listContacts, getContact, updateContact, deleteContact, setScopes, getOrCreateContact } = require('./contact-manager');
10
- const { listFaq, createFaq, updateFaq, deleteFaq, search: searchKB } = require('./knowledge-base');
11
- const { listSections, createSection, updateSection, deleteSection } = require('./playbook');
12
- const { listPreferences, deletePreference, getMetrics, decayConfidence } = require('./learning-loop');
13
- const { normalizePhone } = require('./utils/phone');
14
-
15
- let initialized = false;
16
- let timeoutTimer = null;
17
- let followUpTimer = null;
18
- let decayTimer = null;
19
-
20
- function log(msg, data) {
21
- const ts = new Date().toISOString();
22
- if (data) console.log(`[${ts}] [ROUTER-V2] ${msg}`, JSON.stringify(data));
23
- else console.log(`[${ts}] [ROUTER-V2] ${msg}`);
24
- }
25
-
26
- /**
27
- * Initialize the router v2. Call once at startup.
28
- * @returns {Object} { db, stmts }
29
- */
30
- function initialize() {
31
- if (initialized) return;
32
-
33
- const result = initDatabase();
34
- log('Database initialized');
35
-
36
- // Start BB polling for Brandon's manual messages
37
- startBBPolling(30000);
38
-
39
- // Start escalation timeout checker (every 60s)
40
- timeoutTimer = setInterval(() => {
41
- processTimeouts().catch(err => log('Timeout check error', { error: err.message }));
42
- }, 60000);
43
-
44
- // Start follow-up engine (every 30 min)
45
- followUpTimer = setInterval(() => {
46
- processFollowUps().catch(err => log('Follow-up error', { error: err.message }));
47
- }, 30 * 60000);
48
-
49
- // Confidence decay (daily)
50
- decayTimer = setInterval(() => {
51
- decayConfidence();
52
- }, 24 * 3600000);
53
-
54
- initialized = true;
55
- log('Router v2 initialized');
56
- return result;
57
- }
58
-
59
- /**
60
- * Shutdown cleanup.
61
- */
62
- function shutdown() {
63
- stopBBPolling();
64
- if (timeoutTimer) clearInterval(timeoutTimer);
65
- if (followUpTimer) clearInterval(followUpTimer);
66
- if (decayTimer) clearInterval(decayTimer);
67
- log('Router v2 shut down');
68
- }
69
-
70
- /**
71
- * Register Express routes on an existing app.
72
- * @param {import('express').Application} app - Express app
73
- */
74
- function registerRoutes(app) {
75
- // ── Webhook endpoints (inbound messages) ──
76
-
77
- // iMessage / BlueBubbles webhook
78
- app.post('/webhook/imessage', (req, res) => {
79
- try {
80
- ingestMessage(req.body, 'imessage', (message) => {
81
- processMessage(message).catch(err => log('Process error', { phone: message.phone, error: err.message }));
82
- });
83
- res.json({ ok: true });
84
- } catch (err) {
85
- log('iMessage webhook error', { error: err.message });
86
- res.status(500).json({ error: err.message });
87
- }
88
- });
89
-
90
- // WhatsApp webhook
91
- app.post('/webhook/whatsapp', (req, res) => {
92
- try {
93
- ingestMessage(req.body, 'whatsapp', (message) => {
94
- processMessage(message).catch(err => log('Process error', { phone: message.phone, error: err.message }));
95
- });
96
- res.json({ ok: true });
97
- } catch (err) {
98
- log('WhatsApp webhook error', { error: err.message });
99
- res.status(500).json({ error: err.message });
100
- }
101
- });
102
-
103
- // Quo / OpenPhone webhook
104
- app.post('/webhook/quo', (req, res) => {
105
- try {
106
- ingestMessage(req.body, 'quo', (message) => {
107
- processMessage(message).catch(err => log('Process error', { phone: message.phone, error: err.message }));
108
- });
109
- res.json({ ok: true });
110
- } catch (err) {
111
- log('Quo webhook error', { error: err.message });
112
- res.status(500).json({ error: err.message });
113
- }
114
- });
115
-
116
- // ── Escalation reply endpoint ──
117
- app.post('/api/router/escalation-reply', async (req, res) => {
118
- try {
119
- const result = await handleReply(req.body);
120
- res.json(result);
121
- } catch (err) {
122
- log('Escalation reply error', { error: err.message });
123
- res.status(500).json({ error: err.message });
124
- }
125
- });
126
-
127
- // ── Escalation action handler (from interactive UI) ──
128
- app.post('/api/router/escalation-action', async (req, res) => {
129
- try {
130
- const { escalationId, actionIndex, actionType, scopes, phone } = req.body;
131
- log('Escalation action received', { escalationId, actionType, scopes, phone });
132
-
133
- // Grant scopes if action requires it
134
- if (actionType === 'approve_and_grant' && scopes && scopes.length > 0 && phone) {
135
- const scopeMap = {};
136
- scopes.forEach(s => { scopeMap[s] = true; });
137
- setScopes(normalizePhone(phone), scopeMap);
138
- log('Scopes granted via escalation action', { phone, scopes });
139
- }
140
-
141
- // Handle the reply based on action type
142
- if (actionType === 'approve_and_grant' || actionType === 'approve_one_time') {
143
- // Reply affirmatively to the contact
144
- const result = await handleReply({ escalationId, response: 'Yes, approve and confirm their request' });
145
- res.json({ success: true, action: actionType, scopesGranted: scopes || [], reply: result });
146
- } else if (actionType === 'decline') {
147
- const result = await handleReply({ escalationId, response: 'Politely decline their request' });
148
- res.json({ success: true, action: actionType, reply: result });
149
- } else if (actionType === 'ignore' || actionType === 'handle_personally') {
150
- // No reply to contact
151
- res.json({ success: true, action: actionType, noReply: true });
152
- } else {
153
- res.json({ success: true, action: 'unknown', note: 'No automated action taken' });
154
- }
155
- } catch (err) {
156
- log('Escalation action error', { error: err.message });
157
- res.status(500).json({ error: err.message });
158
- }
159
- });
160
-
161
- // ── Contact management API ──
162
-
163
- app.get('/api/v2/contacts', (req, res) => {
164
- try {
165
- const filter = {};
166
- if (req.query.category) filter.category = req.query.category;
167
- if (req.query.search) filter.search = req.query.search;
168
- res.json(listContacts(filter));
169
- } catch (err) {
170
- res.status(500).json({ error: err.message });
171
- }
172
- });
173
-
174
- app.get('/api/v2/contacts/:phone', (req, res) => {
175
- try {
176
- const contact = getContact(normalizePhone(req.params.phone));
177
- if (!contact) return res.status(404).json({ error: 'Contact not found' });
178
- res.json(contact);
179
- } catch (err) {
180
- res.status(500).json({ error: err.message });
181
- }
182
- });
183
-
184
- app.put('/api/v2/contacts/:phone', (req, res) => {
185
- try {
186
- const phone = normalizePhone(req.params.phone);
187
- // Ensure contact exists
188
- getOrCreateContact(phone, req.body);
189
- const updated = updateContact(phone, req.body);
190
- if (!updated) return res.status(404).json({ error: 'Contact not found' });
191
- res.json(updated);
192
- } catch (err) {
193
- res.status(500).json({ error: err.message });
194
- }
195
- });
196
-
197
- app.delete('/api/v2/contacts/:phone', (req, res) => {
198
- try {
199
- deleteContact(normalizePhone(req.params.phone));
200
- res.json({ ok: true });
201
- } catch (err) {
202
- res.status(500).json({ error: err.message });
203
- }
204
- });
205
-
206
- // Scopes management
207
- app.put('/api/v2/contacts/:phone/scopes', (req, res) => {
208
- try {
209
- setScopes(normalizePhone(req.params.phone), req.body.scopes || {});
210
- const contact = getContact(normalizePhone(req.params.phone));
211
- res.json({ scopes: contact?.scopes || [] });
212
- } catch (err) {
213
- res.status(500).json({ error: err.message });
214
- }
215
- });
216
-
217
- // ── FAQ management API ──
218
-
219
- app.get('/api/v2/faq', (req, res) => {
220
- try {
221
- res.json(listFaq(req.query.deviceId || null));
222
- } catch (err) {
223
- res.status(500).json({ error: err.message });
224
- }
225
- });
226
-
227
- app.post('/api/v2/faq', (req, res) => {
228
- try {
229
- const entry = createFaq(req.body);
230
- res.json(entry);
231
- } catch (err) {
232
- res.status(500).json({ error: err.message });
233
- }
234
- });
235
-
236
- app.put('/api/v2/faq/:id', (req, res) => {
237
- try {
238
- const entry = updateFaq(parseInt(req.params.id), req.body);
239
- if (!entry) return res.status(404).json({ error: 'FAQ entry not found' });
240
- res.json(entry);
241
- } catch (err) {
242
- res.status(500).json({ error: err.message });
243
- }
244
- });
245
-
246
- app.delete('/api/v2/faq/:id', (req, res) => {
247
- try {
248
- deleteFaq(parseInt(req.params.id));
249
- res.json({ ok: true });
250
- } catch (err) {
251
- res.status(500).json({ error: err.message });
252
- }
253
- });
254
-
255
- app.get('/api/v2/faq/search', (req, res) => {
256
- try {
257
- const results = searchKB(req.query.q || '', req.query.deviceId || null);
258
- res.json(results);
259
- } catch (err) {
260
- res.status(500).json({ error: err.message });
261
- }
262
- });
263
-
264
- // ── Playbook management API ──
265
-
266
- app.get('/api/v2/playbook', (req, res) => {
267
- try {
268
- res.json(listSections(req.query.deviceId || null));
269
- } catch (err) {
270
- res.status(500).json({ error: err.message });
271
- }
272
- });
273
-
274
- app.post('/api/v2/playbook', (req, res) => {
275
- try {
276
- const section = createSection(req.body);
277
- res.json(section);
278
- } catch (err) {
279
- res.status(500).json({ error: err.message });
280
- }
281
- });
282
-
283
- app.put('/api/v2/playbook/:id', (req, res) => {
284
- try {
285
- const section = updateSection(parseInt(req.params.id), req.body);
286
- if (!section) return res.status(404).json({ error: 'Section not found' });
287
- res.json(section);
288
- } catch (err) {
289
- res.status(500).json({ error: err.message });
290
- }
291
- });
292
-
293
- app.delete('/api/v2/playbook/:id', (req, res) => {
294
- try {
295
- deleteSection(parseInt(req.params.id));
296
- res.json({ ok: true });
297
- } catch (err) {
298
- res.status(500).json({ error: err.message });
299
- }
300
- });
301
-
302
- // ── Preferences / Learning API ──
303
-
304
- app.get('/api/v2/preferences', (req, res) => {
305
- try {
306
- res.json(listPreferences());
307
- } catch (err) {
308
- res.status(500).json({ error: err.message });
309
- }
310
- });
311
-
312
- app.delete('/api/v2/preferences/:id', (req, res) => {
313
- try {
314
- deletePreference(parseInt(req.params.id));
315
- res.json({ ok: true });
316
- } catch (err) {
317
- res.status(500).json({ error: err.message });
318
- }
319
- });
320
-
321
- app.get('/api/v2/learning/metrics', (req, res) => {
322
- try {
323
- res.json(getMetrics());
324
- } catch (err) {
325
- res.status(500).json({ error: err.message });
326
- }
327
- });
328
-
329
- // ── Settings API ──
330
-
331
- app.get('/api/v2/settings', (req, res) => {
332
- try {
333
- const stmts = getStmts();
334
- const all = stmts.getAllSettings.all();
335
- const settings = Object.fromEntries(all.map(s => [s.key, s.value]));
336
- res.json(settings);
337
- } catch (err) {
338
- res.status(500).json({ error: err.message });
339
- }
340
- });
341
-
342
- app.put('/api/v2/settings', (req, res) => {
343
- try {
344
- for (const [key, value] of Object.entries(req.body)) {
345
- setSetting(key, value);
346
- }
347
- res.json({ ok: true });
348
- } catch (err) {
349
- res.status(500).json({ error: err.message });
350
- }
351
- });
352
-
353
- // ── Follow-up management ──
354
-
355
- app.get('/api/v2/follow-ups', (req, res) => {
356
- try {
357
- res.json(getStmts().getAllFollowUps.all());
358
- } catch (err) {
359
- res.status(500).json({ error: err.message });
360
- }
361
- });
362
-
363
- app.post('/api/v2/follow-ups/:phone/send', async (req, res) => {
364
- try {
365
- const { sendFollowUpNow } = require('./follow-up-engine');
366
- const result = await sendFollowUpNow(normalizePhone(req.params.phone), req.body.message);
367
- res.json(result);
368
- } catch (err) {
369
- res.status(500).json({ error: err.message });
370
- }
371
- });
372
-
373
- app.delete('/api/v2/follow-ups/:phone', (req, res) => {
374
- try {
375
- getStmts().deleteFollowUp.run(normalizePhone(req.params.phone));
376
- res.json({ ok: true });
377
- } catch (err) {
378
- res.status(500).json({ error: err.message });
379
- }
380
- });
381
-
382
- // ── Message log ──
383
-
384
- app.get('/api/v2/messages', (req, res) => {
385
- try {
386
- const phone = req.query.phone ? normalizePhone(req.query.phone) : null;
387
- const limit = parseInt(req.query.limit) || 50;
388
- const stmts = getStmts();
389
- const messages = phone
390
- ? stmts.getRecentMessages.all(phone, limit)
391
- : stmts.getRecentMessagesAll.all(limit);
392
- res.json(messages);
393
- } catch (err) {
394
- res.status(500).json({ error: err.message });
395
- }
396
- });
397
-
398
- // ── Send message (manual / API) ──
399
-
400
- app.post('/api/v2/send', async (req, res) => {
401
- try {
402
- const { sendMessage } = require('./outbound-sender');
403
- const result = await sendMessage({
404
- phone: normalizePhone(req.body.phone),
405
- text: req.body.message || req.body.text,
406
- channel: req.body.channel || 'imessage',
407
- sentBy: req.body.sentBy || 'aiva',
408
- stateAtTime: req.body.stateAtTime || 'manual',
409
- });
410
- res.json(result);
411
- } catch (err) {
412
- res.status(500).json({ error: err.message });
413
- }
414
- });
415
-
416
- // ── Escalation management ──
417
-
418
- app.get('/api/v2/escalations', (req, res) => {
419
- try {
420
- const db = require('./db').getDb();
421
- const escalations = db.prepare("SELECT * FROM escalations ORDER BY created_at DESC LIMIT ?").all(parseInt(req.query.limit) || 50);
422
- res.json(escalations);
423
- } catch (err) {
424
- res.status(500).json({ error: err.message });
425
- }
426
- });
427
-
428
- // ── Health check ──
429
-
430
- app.get('/api/v2/health', (req, res) => {
431
- res.json({
432
- version: 'v2',
433
- initialized,
434
- timestamp: new Date().toISOString(),
435
- });
436
- });
437
-
438
- log('Routes registered');
439
- }
440
-
441
- module.exports = { initialize, shutdown, registerRoutes };
@@ -1,213 +0,0 @@
1
- // ── Message Ingestion - Normalize + Debounce + BB Polling ──
2
- 'use strict';
3
-
4
- const { getStmts, getSetting } = require('./db');
5
- const { normalizePhone } = require('./utils/phone');
6
- const imessageAdapter = require('./adapters/imessage');
7
-
8
- // Debounce state: phone -> { timer, messages: [] }
9
- const debounceMap = new Map();
10
- // Per-phone processing lock to prevent concurrent AI calls
11
- const processingLocks = new Set();
12
- // Processed message IDs for dedup
13
- const processedIds = new Set();
14
- const MAX_PROCESSED = 5000;
15
- // Last BB poll timestamp (epoch ms)
16
- let lastBBPollMs = Date.now() - 60000;
17
- let bbPollTimer = null;
18
-
19
- function log(msg, data) {
20
- const ts = new Date().toISOString();
21
- if (data) console.log(`[${ts}] [INGESTION] ${msg}`, JSON.stringify(data));
22
- else console.log(`[${ts}] [INGESTION] ${msg}`);
23
- }
24
-
25
- /**
26
- * Normalize a raw webhook payload from any channel into the unified InboundMessage format.
27
- * @param {Object} rawPayload - Raw webhook data
28
- * @param {string} channel - 'imessage' | 'whatsapp' | 'quo'
29
- * @returns {Object|null} Normalized message or null if irrelevant
30
- */
31
- function normalizeMessage(rawPayload, channel) {
32
- let adapter;
33
- switch (channel) {
34
- case 'imessage':
35
- adapter = require('./adapters/imessage');
36
- break;
37
- case 'whatsapp':
38
- adapter = require('./adapters/whatsapp');
39
- break;
40
- case 'quo':
41
- adapter = require('./adapters/quo');
42
- break;
43
- default:
44
- log('Unknown channel', { channel });
45
- return null;
46
- }
47
-
48
- const parsed = adapter.parseWebhook(rawPayload);
49
- if (!parsed) return null;
50
-
51
- // Normalize phone
52
- parsed.phone = normalizePhone(parsed.phone);
53
- if (!parsed.phone) return null;
54
-
55
- return parsed;
56
- }
57
-
58
- /**
59
- * Check if a message is a duplicate (already processed).
60
- * @param {string} messageId
61
- * @returns {boolean}
62
- */
63
- function isDuplicate(messageId) {
64
- if (!messageId) return false;
65
- if (processedIds.has(messageId)) return true;
66
-
67
- processedIds.add(messageId);
68
- // Prune old IDs
69
- if (processedIds.size > MAX_PROCESSED) {
70
- const idsArray = Array.from(processedIds);
71
- for (let i = 0; i < idsArray.length - MAX_PROCESSED / 2; i++) {
72
- processedIds.delete(idsArray[i]);
73
- }
74
- }
75
-
76
- return false;
77
- }
78
-
79
- /**
80
- * Debounce rapid-fire messages from the same contact.
81
- * Calls the callback with all accumulated messages after the debounce window.
82
- * @param {Object} message - Normalized inbound message
83
- * @param {Function} onReady - Callback(messages[]) when debounce window closes
84
- */
85
- function debounceMessage(message, onReady) {
86
- const debounceMs = parseInt(getSetting('debounceMs')) || 3000;
87
- const phone = message.phone;
88
-
89
- if (debounceMap.has(phone)) {
90
- const entry = debounceMap.get(phone);
91
- clearTimeout(entry.timer);
92
- entry.messages.push(message);
93
- entry.timer = setTimeout(() => {
94
- debounceMap.delete(phone);
95
- onReady(entry.messages);
96
- }, debounceMs);
97
- } else {
98
- const entry = {
99
- messages: [message],
100
- timer: setTimeout(() => {
101
- debounceMap.delete(phone);
102
- onReady(entry.messages);
103
- }, debounceMs),
104
- };
105
- debounceMap.set(phone, entry);
106
- }
107
- }
108
-
109
- /**
110
- * Process a raw inbound webhook: normalize, dedup, debounce, then invoke callback.
111
- * @param {Object} rawPayload - Raw webhook data
112
- * @param {string} channel - Channel identifier
113
- * @param {Function} onMessage - Callback(normalizedMessage) for processing
114
- */
115
- function ingestMessage(rawPayload, channel, onMessage) {
116
- const message = normalizeMessage(rawPayload, channel);
117
- if (!message) return;
118
-
119
- // TEMPORARILY DISABLED for testing (2026-02-27)
120
- // Skip master phone messages (Brandon's own messages come through polling)
121
- // const masterPhone = getSetting('masterPhone');
122
- // if (message.phone === masterPhone) return;
123
-
124
- // Dedup
125
- if (isDuplicate(message.id)) {
126
- log('Duplicate skipped', { id: message.id, phone: message.phone });
127
- return;
128
- }
129
-
130
- // Log inbound message
131
- const stmts = getStmts();
132
- stmts.insertMessage.run(
133
- message.phone, message.channel, 'inbound', message.text,
134
- JSON.stringify(message.attachments || []), 'contact', '',
135
- );
136
-
137
- // Debounce then process (with per-phone lock to prevent concurrent AI calls)
138
- debounceMessage(message, async (messages) => {
139
- const phone = messages[0].phone;
140
- const msg = messages.length === 1
141
- ? messages[0]
142
- : { ...messages[messages.length - 1], text: messages.map(m => m.text).filter(Boolean).join('\n') };
143
-
144
- // Wait for any in-flight processing for this phone to finish
145
- while (processingLocks.has(phone)) {
146
- await new Promise(r => setTimeout(r, 200));
147
- }
148
-
149
- processingLocks.add(phone);
150
- try {
151
- await onMessage(msg);
152
- } catch (err) {
153
- log('Processing error', { phone, error: err.message });
154
- } finally {
155
- processingLocks.delete(phone);
156
- }
157
- });
158
- }
159
-
160
- /**
161
- * Start polling BlueBubbles for Brandon's manual messages.
162
- * These are messages Brandon sends directly via iMessage (not through AIVA).
163
- * @param {number} [intervalMs=30000] - Poll interval
164
- */
165
- function startBBPolling(intervalMs = 30000) {
166
- if (bbPollTimer) return;
167
-
168
- async function poll() {
169
- try {
170
- const messages = await imessageAdapter.pollBrandonMessages(lastBBPollMs);
171
- if (messages.length > 0) {
172
- const stmts = getStmts();
173
- for (const msg of messages) {
174
- if (!msg.phone || isDuplicate(msg.guid)) continue;
175
-
176
- const phone = normalizePhone(msg.phone);
177
- // Log as outbound with sent_by: 'brandon'
178
- stmts.insertMessage.run(
179
- phone, 'imessage', 'outbound', msg.text,
180
- '[]', 'brandon', '',
181
- );
182
- log('Tracked Brandon message', { phone, len: msg.text.length });
183
- }
184
- lastBBPollMs = Math.max(...messages.map(m => m.timestamp), lastBBPollMs);
185
- }
186
- } catch (err) {
187
- log('BB poll error', { error: err.message });
188
- }
189
- }
190
-
191
- bbPollTimer = setInterval(poll, intervalMs);
192
- poll(); // Initial poll
193
- log('Started BB polling', { intervalMs });
194
- }
195
-
196
- /**
197
- * Stop BB polling.
198
- */
199
- function stopBBPolling() {
200
- if (bbPollTimer) {
201
- clearInterval(bbPollTimer);
202
- bbPollTimer = null;
203
- }
204
- }
205
-
206
- module.exports = {
207
- normalizeMessage,
208
- isDuplicate,
209
- debounceMessage,
210
- ingestMessage,
211
- startBBPolling,
212
- stopBBPolling,
213
- };