@conversionpros/aiva 1.0.1 → 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 (149) 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/constants.js +72 -0
  5. package/lib/launch-agent.js +112 -0
  6. package/lib/prerequisites.js +236 -0
  7. package/lib/process.js +59 -145
  8. package/lib/setup.js +224 -194
  9. package/lib/validate.js +194 -0
  10. package/package.json +7 -32
  11. package/auto-deploy.js +0 -190
  12. package/cli-sync.js +0 -126
  13. package/d2a-prompt-template.txt +0 -106
  14. package/diagnostics-api.js +0 -304
  15. package/docs/ara-dedup-fix-scope.md +0 -112
  16. package/docs/ara-fix-round2-scope.md +0 -61
  17. package/docs/ara-greeting-fix-scope.md +0 -70
  18. package/docs/calendar-date-fix-scope.md +0 -28
  19. package/docs/getting-started.md +0 -115
  20. package/docs/network-architecture-rollout-scope.md +0 -43
  21. package/docs/scope-google-oauth-integration.md +0 -351
  22. package/docs/settings-page-scope.md +0 -50
  23. package/docs/xai-imagine-scope.md +0 -116
  24. package/docs/xai-voice-integration-scope.md +0 -115
  25. package/docs/xai-voice-tools-scope.md +0 -165
  26. package/email-router.js +0 -512
  27. package/follow-up-handler.js +0 -606
  28. package/gateway-monitor.js +0 -158
  29. package/google-email.js +0 -379
  30. package/google-oauth.js +0 -310
  31. package/grok-imagine.js +0 -97
  32. package/health-reporter.js +0 -287
  33. package/invisible-prefix-base.txt +0 -206
  34. package/invisible-prefix-owner.txt +0 -26
  35. package/invisible-prefix-slim.txt +0 -10
  36. package/invisible-prefix.txt +0 -43
  37. package/knowledge-base.js +0 -472
  38. package/lib/cli.js +0 -19
  39. package/lib/server.js +0 -42
  40. package/meta-capi.js +0 -206
  41. package/meta-leads.js +0 -411
  42. package/notion-oauth.js +0 -323
  43. package/public/agent-config.html +0 -241
  44. package/public/aiva-avatar-anime.png +0 -0
  45. package/public/css/docs.css.bak +0 -688
  46. package/public/css/onboarding.css +0 -543
  47. package/public/diagrams/claude-subscription-pool.html +0 -329
  48. package/public/diagrams/claude-subscription-pool.png +0 -0
  49. package/public/docs-icon.png +0 -0
  50. package/public/escalation.html +0 -237
  51. package/public/group-config.html +0 -300
  52. package/public/icon-192.png +0 -0
  53. package/public/icon-512.png +0 -0
  54. package/public/icons/agents.svg +0 -1
  55. package/public/icons/attach.svg +0 -1
  56. package/public/icons/characters.svg +0 -1
  57. package/public/icons/chat.svg +0 -1
  58. package/public/icons/docs.svg +0 -1
  59. package/public/icons/heartbeat.svg +0 -1
  60. package/public/icons/messages.svg +0 -1
  61. package/public/icons/mic.svg +0 -1
  62. package/public/icons/notes.svg +0 -1
  63. package/public/icons/settings.svg +0 -1
  64. package/public/icons/tasks.svg +0 -1
  65. package/public/images/onboarding/p0-communication-layer.png +0 -0
  66. package/public/images/onboarding/p0-infinite-surface.png +0 -0
  67. package/public/images/onboarding/p0-learning-model.png +0 -0
  68. package/public/images/onboarding/p0-meet-aiva.png +0 -0
  69. package/public/images/onboarding/p4-contact-intelligence.png +0 -0
  70. package/public/images/onboarding/p4-context-compounds.png +0 -0
  71. package/public/images/onboarding/p4-message-router.png +0 -0
  72. package/public/images/onboarding/p4-per-contact-rules.png +0 -0
  73. package/public/images/onboarding/p4-send-messages.png +0 -0
  74. package/public/images/onboarding/p6-be-precise.png +0 -0
  75. package/public/images/onboarding/p6-review-escalations.png +0 -0
  76. package/public/images/onboarding/p6-voice-input.png +0 -0
  77. package/public/images/onboarding/p7-completion.png +0 -0
  78. package/public/index.html +0 -11594
  79. package/public/js/onboarding.js +0 -699
  80. package/public/manifest.json +0 -24
  81. package/public/messages-v2.html +0 -2824
  82. package/public/permission-approve.html.bak +0 -107
  83. package/public/permissions.html +0 -150
  84. package/public/styles/design-system.css +0 -68
  85. package/router-db.js +0 -604
  86. package/router-utils.js +0 -28
  87. package/router-v2/adapters/imessage.js +0 -191
  88. package/router-v2/adapters/quo.js +0 -82
  89. package/router-v2/adapters/whatsapp.js +0 -192
  90. package/router-v2/contact-manager.js +0 -234
  91. package/router-v2/conversation-engine.js +0 -498
  92. package/router-v2/data/knowledge-base.json +0 -176
  93. package/router-v2/data/router-v2.db +0 -0
  94. package/router-v2/data/router-v2.db-shm +0 -0
  95. package/router-v2/data/router-v2.db-wal +0 -0
  96. package/router-v2/data/router.db +0 -0
  97. package/router-v2/db.js +0 -457
  98. package/router-v2/escalation-bridge.js +0 -540
  99. package/router-v2/follow-up-engine.js +0 -347
  100. package/router-v2/index.js +0 -441
  101. package/router-v2/ingestion.js +0 -213
  102. package/router-v2/knowledge-base.js +0 -231
  103. package/router-v2/lead-qualifier.js +0 -152
  104. package/router-v2/learning-loop.js +0 -202
  105. package/router-v2/outbound-sender.js +0 -160
  106. package/router-v2/package.json +0 -13
  107. package/router-v2/permission-gate.js +0 -86
  108. package/router-v2/playbook.js +0 -177
  109. package/router-v2/prompts/base.js +0 -52
  110. package/router-v2/prompts/first-contact.js +0 -38
  111. package/router-v2/prompts/lead-qualification.js +0 -37
  112. package/router-v2/prompts/scheduling.js +0 -72
  113. package/router-v2/prompts/style-overrides.js +0 -22
  114. package/router-v2/scheduler.js +0 -301
  115. package/router-v2/scripts/migrate-v1-to-v2.js +0 -215
  116. package/router-v2/scripts/seed-faq.js +0 -67
  117. package/router-v2/seed-knowledge-base.js +0 -39
  118. package/router-v2/utils/ai.js +0 -129
  119. package/router-v2/utils/phone.js +0 -52
  120. package/router-v2/utils/response-validator.js +0 -98
  121. package/router-v2/utils/sanitize.js +0 -222
  122. package/router.js +0 -5005
  123. package/routes/google-calendar.js +0 -186
  124. package/scripts/deploy.sh +0 -62
  125. package/scripts/macos-calendar.sh +0 -232
  126. package/scripts/onboard-device.sh +0 -466
  127. package/server.js +0 -5131
  128. package/start.sh +0 -24
  129. package/templates/AGENTS.md +0 -548
  130. package/templates/IDENTITY.md +0 -15
  131. package/templates/docs-agents.html +0 -132
  132. package/templates/docs-app.html +0 -130
  133. package/templates/docs-home.html +0 -83
  134. package/templates/docs-imessage.html +0 -121
  135. package/templates/docs-tasks.html +0 -123
  136. package/templates/docs-tips.html +0 -175
  137. package/templates/getting-started.html +0 -809
  138. package/templates/invisible-prefix-base.txt +0 -171
  139. package/templates/invisible-prefix-owner.txt +0 -282
  140. package/templates/invisible-prefix.txt +0 -338
  141. package/templates/manifest.json +0 -61
  142. package/templates/memory-org/clients.md +0 -7
  143. package/templates/memory-org/credentials.md +0 -9
  144. package/templates/memory-org/devices.md +0 -7
  145. package/templates/updates.html +0 -464
  146. package/tts-proxy.js +0 -96
  147. package/voice-call-local.js +0 -731
  148. package/voice-call.js +0 -732
  149. package/wa-listener.js +0 -354
@@ -1,191 +0,0 @@
1
- // ── iMessage Adapter (BlueBubbles REST API) ──────────────
2
- // No CLI dependency - uses BlueBubbles REST API directly.
3
- 'use strict';
4
-
5
- const { getSetting } = require('../db');
6
-
7
- // BlueBubbles config - read from settings or env
8
- function getBBConfig() {
9
- return {
10
- url: process.env.BLUEBUBBLES_URL || getSetting('blueBubblesUrl') || 'http://localhost:1234',
11
- password: process.env.BLUEBUBBLES_PASSWORD || getSetting('blueBubblesPassword') || '',
12
- };
13
- }
14
-
15
- function log(msg, data) {
16
- const ts = new Date().toISOString();
17
- if (data) console.log(`[${ts}] [IMESSAGE] ${msg}`, JSON.stringify(data));
18
- else console.log(`[${ts}] [IMESSAGE] ${msg}`);
19
- }
20
-
21
- /**
22
- * Send an iMessage via BlueBubbles REST API.
23
- * @param {string} phone - E.164 phone number
24
- * @param {string} text - Message text
25
- * @returns {Promise<{success: boolean, error?: string}>}
26
- */
27
- async function send(phone, text) {
28
- const config = getBBConfig();
29
- if (!config.password) {
30
- log('No BlueBubbles password configured');
31
- return { success: false, error: 'no_bb_password' };
32
- }
33
-
34
- try {
35
- const resp = await fetch(`${config.url}/api/v1/message/text?password=${encodeURIComponent(config.password)}`, {
36
- method: 'POST',
37
- headers: { 'Content-Type': 'application/json' },
38
- body: JSON.stringify({
39
- chatGuid: `iMessage;-;${phone}`,
40
- tempGuid: `temp-${Date.now()}`,
41
- message: text,
42
- method: 'private-api',
43
- }),
44
- signal: AbortSignal.timeout(30000),
45
- });
46
-
47
- if (!resp.ok) {
48
- const errText = await resp.text().catch(() => 'unknown');
49
- log('BB API error', { status: resp.status, body: errText.substring(0, 200) });
50
- return { success: false, error: `bb_http_${resp.status}` };
51
- }
52
-
53
- const data = await resp.json();
54
- if (data.status === 200 || data.status === 201) {
55
- return { success: true };
56
- }
57
-
58
- log('BB API unexpected status', { data });
59
- return { success: false, error: `bb_status_${data.status}` };
60
- } catch (err) {
61
- log('BB API request failed', { error: err.message });
62
- return { success: false, error: err.message };
63
- }
64
- }
65
-
66
- /**
67
- * Fetch recent messages from BlueBubbles for a specific chat.
68
- * Used for manual message polling (tracking Brandon's direct texts).
69
- * @param {string} phone - E.164 phone number
70
- * @param {number} [limit=20]
71
- * @param {number} [afterMs] - Only messages after this timestamp (epoch ms)
72
- * @returns {Promise<Array>}
73
- */
74
- async function fetchRecentMessages(phone, limit = 20, afterMs = null) {
75
- const config = getBBConfig();
76
- if (!config.password) return [];
77
-
78
- try {
79
- const chatGuid = `iMessage;-;${phone}`;
80
- let url = `${config.url}/api/v1/chat/${encodeURIComponent(chatGuid)}/message?password=${encodeURIComponent(config.password)}&limit=${limit}&sort=desc`;
81
- if (afterMs) url += `&after=${afterMs}`;
82
-
83
- const resp = await fetch(url, {
84
- signal: AbortSignal.timeout(15000),
85
- });
86
- if (!resp.ok) return [];
87
-
88
- const data = await resp.json();
89
- return (data.data || []).map(msg => ({
90
- id: msg.guid,
91
- text: msg.text || '',
92
- isFromMe: msg.isFromMe,
93
- dateCreated: msg.dateCreated,
94
- attachments: (msg.attachments || []).map(a => ({
95
- type: a.mimeType,
96
- filename: a.transferName,
97
- url: `${config.url}/api/v1/attachment/${a.guid}/download`,
98
- password: config.password,
99
- })),
100
- }));
101
- } catch (err) {
102
- log('Fetch messages failed', { phone, error: err.message });
103
- return [];
104
- }
105
- }
106
-
107
- /**
108
- * Poll BlueBubbles for messages Brandon sent directly (manual messages).
109
- * Returns messages sent by Brandon (isFromMe=true) since the given timestamp.
110
- * @param {number} sinceMs - Epoch milliseconds
111
- * @returns {Promise<Array<{phone: string, text: string, timestamp: number}>>}
112
- */
113
- async function pollBrandonMessages(sinceMs) {
114
- const config = getBBConfig();
115
- if (!config.password) return [];
116
-
117
- try {
118
- const url = `${config.url}/api/v1/message?password=${encodeURIComponent(config.password)}&after=${sinceMs}&sort=desc&limit=50`;
119
- const resp = await fetch(url, {
120
- signal: AbortSignal.timeout(15000),
121
- });
122
- if (!resp.ok) return [];
123
-
124
- const data = await resp.json();
125
- const messages = (data.data || [])
126
- .filter(msg => msg.isFromMe && !msg.isArchived && msg.text)
127
- .map(msg => {
128
- // Extract phone from chat participants
129
- const chat = msg.chats?.[0];
130
- const chatGuid = chat?.guid || '';
131
- const phoneMatch = chatGuid.match(/;([+-]?\d+)$/);
132
- const phone = phoneMatch ? (phoneMatch[1].startsWith('+') ? phoneMatch[1] : `+${phoneMatch[1]}`) : '';
133
-
134
- return {
135
- phone,
136
- text: msg.text,
137
- timestamp: msg.dateCreated,
138
- guid: msg.guid,
139
- };
140
- })
141
- .filter(msg => msg.phone);
142
-
143
- return messages;
144
- } catch (err) {
145
- log('Poll Brandon messages failed', { error: err.message });
146
- return [];
147
- }
148
- }
149
-
150
- /**
151
- * Parse an incoming BlueBubbles webhook payload into normalized format.
152
- * @param {Object} payload - Raw webhook payload
153
- * @returns {Object|null} Normalized message or null if not a relevant message
154
- */
155
- function parseWebhook(payload) {
156
- const msg = payload?.data || payload;
157
- if (!msg) return null;
158
-
159
- // Skip non-text messages, reactions, and group chats
160
- if (msg.associatedMessageType || msg.isArchived) return null;
161
- if (msg.isFromMe) return null; // We track Brandon's messages via polling
162
-
163
- const chatGuid = msg.chats?.[0]?.guid || '';
164
- // Skip group chats (v2.1)
165
- if (chatGuid.includes(';+;')) return null;
166
-
167
- const phoneMatch = chatGuid.match(/;([+-]?\d+)$/);
168
- if (!phoneMatch) return null;
169
-
170
- let phone = phoneMatch[1];
171
- if (!phone.startsWith('+')) phone = `+${phone}`;
172
-
173
- // Check for tapback/reaction
174
- const tapbackPattern = /^(Loved|Liked|Disliked|Laughed at|Emphasized|Questioned) ".+"/;
175
- if (tapbackPattern.test(msg.text?.trim())) return null;
176
-
177
- return {
178
- id: msg.guid,
179
- phone,
180
- channel: 'imessage',
181
- text: msg.text || '',
182
- attachments: (msg.attachments || []).map(a => ({
183
- type: a.mimeType,
184
- filename: a.transferName,
185
- })),
186
- timestamp: new Date(msg.dateCreated),
187
- raw: payload,
188
- };
189
- }
190
-
191
- module.exports = { send, fetchRecentMessages, pollBrandonMessages, parseWebhook };
@@ -1,82 +0,0 @@
1
- // ── Quo / OpenPhone Adapter ──────────────────────────────
2
- // Handles OpenPhone webhook ingestion and outbound via API.
3
- 'use strict';
4
-
5
- const { getSetting } = require('../db');
6
-
7
- function log(msg, data) {
8
- const ts = new Date().toISOString();
9
- if (data) console.log(`[${ts}] [QUO] ${msg}`, JSON.stringify(data));
10
- else console.log(`[${ts}] [QUO] ${msg}`);
11
- }
12
-
13
- /**
14
- * Send a message via OpenPhone/Quo API.
15
- * @param {string} phone - E.164 phone number
16
- * @param {string} text - Message text
17
- * @returns {Promise<{success: boolean, error?: string}>}
18
- */
19
- async function send(phone, text) {
20
- const apiKey = process.env.OPENPHONE_API_KEY || getSetting('openphoneApiKey');
21
- const fromNumber = process.env.OPENPHONE_NUMBER || getSetting('openphoneNumber');
22
-
23
- if (!apiKey) {
24
- log('No OpenPhone API key configured');
25
- return { success: false, error: 'no_api_key' };
26
- }
27
-
28
- try {
29
- const resp = await fetch('https://api.openphone.com/v1/messages', {
30
- method: 'POST',
31
- headers: {
32
- 'Content-Type': 'application/json',
33
- 'Authorization': apiKey,
34
- },
35
- body: JSON.stringify({
36
- from: fromNumber,
37
- to: [phone],
38
- content: text,
39
- }),
40
- signal: AbortSignal.timeout(30000),
41
- });
42
-
43
- if (!resp.ok) {
44
- const errText = await resp.text().catch(() => 'unknown');
45
- log('API error', { status: resp.status, body: errText.substring(0, 200) });
46
- return { success: false, error: `openphone_http_${resp.status}` };
47
- }
48
-
49
- return { success: true };
50
- } catch (err) {
51
- log('API request failed', { error: err.message });
52
- return { success: false, error: err.message };
53
- }
54
- }
55
-
56
- /**
57
- * Parse an OpenPhone/Quo webhook payload into normalized format.
58
- * @param {Object} payload - Raw webhook payload
59
- * @returns {Object|null}
60
- */
61
- function parseWebhook(payload) {
62
- if (!payload) return null;
63
-
64
- const data = payload.data || payload;
65
- const phone = data.from || '';
66
- if (!phone) return null;
67
-
68
- return {
69
- id: data.id || `quo-${Date.now()}`,
70
- phone: phone.startsWith('+') ? phone : `+${phone}`,
71
- channel: 'quo',
72
- text: data.body || data.content || data.text || '',
73
- attachments: (data.media || []).map(m => ({
74
- type: m.type || 'unknown',
75
- url: m.url,
76
- })),
77
- timestamp: data.createdAt ? new Date(data.createdAt) : new Date(),
78
- raw: payload,
79
- };
80
- }
81
-
82
- module.exports = { send, parseWebhook };
@@ -1,192 +0,0 @@
1
- // ── WhatsApp Adapter (wacli CLI) ─────────────────────────
2
- // Improved send queue - no longer stops the world.
3
- // Uses a serialized queue that briefly pauses sync for sends.
4
- 'use strict';
5
-
6
- const { execSync, spawn, exec } = require('child_process');
7
- const fs = require('fs');
8
-
9
- const WACLI_BIN = '/opt/homebrew/bin/wacli';
10
- let WACLI_AVAILABLE = false;
11
-
12
- try {
13
- fs.accessSync(WACLI_BIN, fs.constants.X_OK);
14
- WACLI_AVAILABLE = true;
15
- } catch {
16
- try {
17
- execSync('which wacli', { timeout: 3000, encoding: 'utf-8' });
18
- WACLI_AVAILABLE = true;
19
- } catch {
20
- WACLI_AVAILABLE = false;
21
- }
22
- }
23
-
24
- let syncProcess = null;
25
- let syncPid = null;
26
- let sendQueue = [];
27
- let sendProcessing = false;
28
-
29
- function log(msg, data) {
30
- const ts = new Date().toISOString();
31
- if (data) console.log(`[${ts}] [WHATSAPP] ${msg}`, JSON.stringify(data));
32
- else console.log(`[${ts}] [WHATSAPP] ${msg}`);
33
- }
34
-
35
- function killAllSync() {
36
- try { execSync('pkill -f "wacli (sync|auth)"', { timeout: 5000 }); } catch { /* none running */ }
37
- syncProcess = null;
38
- syncPid = null;
39
- }
40
-
41
- /**
42
- * Start wacli sync process if not already running.
43
- * @returns {boolean}
44
- */
45
- function startSync() {
46
- if (!WACLI_AVAILABLE) return false;
47
-
48
- try {
49
- const ps = execSync('pgrep -f "wacli (sync|auth)"', { timeout: 5000, encoding: 'utf-8' }).trim();
50
- if (ps) {
51
- syncPid = parseInt(ps.split('\n')[0]);
52
- return true;
53
- }
54
- } catch { /* no process */ }
55
-
56
- try {
57
- syncProcess = spawn(WACLI_BIN, ['sync', '--follow', '--refresh-contacts'], {
58
- detached: true,
59
- stdio: 'ignore',
60
- });
61
- syncProcess.unref();
62
- syncPid = syncProcess.pid;
63
- log('Started wacli sync', { pid: syncPid });
64
- return true;
65
- } catch (err) {
66
- log('Failed to start sync', { error: err.message });
67
- return false;
68
- }
69
- }
70
-
71
- function stopSync() {
72
- if (syncPid) {
73
- try { process.kill(syncPid, 'SIGTERM'); } catch { /* dead */ }
74
- syncProcess = null;
75
- syncPid = null;
76
- }
77
- killAllSync();
78
- }
79
-
80
- /**
81
- * Send a WhatsApp message. Queued to avoid lock contention.
82
- * @param {string} phone - E.164 phone number
83
- * @param {string} text - Message text
84
- * @returns {Promise<{success: boolean, error?: string}>}
85
- */
86
- async function send(phone, text) {
87
- if (!WACLI_AVAILABLE) {
88
- return { success: false, error: 'wacli not installed' };
89
- }
90
-
91
- return new Promise((resolve) => {
92
- sendQueue.push({ phone, text, resolve });
93
- processSendQueue();
94
- });
95
- }
96
-
97
- async function processSendQueue() {
98
- if (sendProcessing || sendQueue.length === 0) return;
99
- sendProcessing = true;
100
-
101
- // Briefly stop sync to release lock
102
- stopSync();
103
- await new Promise(r => setTimeout(r, 2000));
104
-
105
- while (sendQueue.length > 0) {
106
- const { phone, text, resolve } = sendQueue.shift();
107
- log('Sending', { phone, textLen: text.length });
108
-
109
- try {
110
- const result = await new Promise((res, rej) => {
111
- exec(
112
- `${WACLI_BIN} send text --to "${phone.replace(/^\+/, '')}" --message ${JSON.stringify(text)} --timeout 60s`,
113
- { timeout: 90000, encoding: 'utf-8' },
114
- (err, stdout) => {
115
- if (err) rej(err);
116
- else res(stdout);
117
- }
118
- );
119
- });
120
- log('Send success', { phone });
121
- resolve({ success: true });
122
- } catch (err) {
123
- log('Send FAILED', { phone, error: err.message });
124
- resolve({ success: false, error: err.message });
125
- }
126
-
127
- // Small pause between sends
128
- if (sendQueue.length > 0) await new Promise(r => setTimeout(r, 500));
129
- }
130
-
131
- sendProcessing = false;
132
- // Restart sync
133
- await new Promise(r => setTimeout(r, 300));
134
- startSync();
135
- }
136
-
137
- /**
138
- * Fetch recent messages from wacli.
139
- * @param {string} [afterTimestamp] - ISO timestamp
140
- * @param {number} [limit=20]
141
- * @returns {Array}
142
- */
143
- function fetchMessages(afterTimestamp = null, limit = 20) {
144
- if (!WACLI_AVAILABLE) return [];
145
-
146
- try {
147
- let cmd = `${WACLI_BIN} messages list --json --limit ${limit}`;
148
- if (afterTimestamp) cmd += ` --after "${afterTimestamp}"`;
149
-
150
- const output = execSync(cmd, { timeout: 15000, encoding: 'utf-8' });
151
- const messages = JSON.parse(output);
152
- return Array.isArray(messages) ? messages : [];
153
- } catch {
154
- return [];
155
- }
156
- }
157
-
158
- /**
159
- * Parse a WhatsApp webhook payload into normalized format.
160
- * @param {Object} payload - Raw webhook payload
161
- * @returns {Object|null}
162
- */
163
- function parseWebhook(payload) {
164
- if (!payload) return null;
165
-
166
- // Handle wacli webhook format
167
- const msg = payload.message || payload;
168
- const phone = msg.from || msg.phone || '';
169
- if (!phone) return null;
170
-
171
- const normalized = phone.startsWith('+') ? phone : `+${phone.replace(/^\\+/, '')}`;
172
-
173
- return {
174
- id: msg.id || `wa-${Date.now()}`,
175
- phone: normalized,
176
- channel: 'whatsapp',
177
- text: msg.text || msg.body || '',
178
- attachments: [],
179
- timestamp: msg.timestamp ? new Date(msg.timestamp) : new Date(),
180
- raw: payload,
181
- };
182
- }
183
-
184
- /**
185
- * Check if wacli is available and authenticated.
186
- * @returns {boolean}
187
- */
188
- function isAvailable() {
189
- return WACLI_AVAILABLE;
190
- }
191
-
192
- module.exports = { send, fetchMessages, parseWebhook, startSync, stopSync, isAvailable };
@@ -1,234 +0,0 @@
1
- // ── Contact Manager - CRUD + Context + Qualification ─────
2
- 'use strict';
3
-
4
- const { getStmts, getSetting } = require('./db');
5
- const { normalizePhone } = require('./utils/phone');
6
-
7
- // Default scopes per category (from design doc Section 7.3)
8
- const DEFAULT_SCOPES = {
9
- family: ['calendar.view', 'calendar.book', 'calendar.modify', 'messages.send', 'messages.auto-reply', 'info.business', 'info.personal', 'reminders.create'],
10
- friend: ['calendar.view', 'calendar.book', 'messages.auto-reply', 'info.business'],
11
- client: ['calendar.view', 'calendar.book', 'messages.auto-reply', 'info.business', 'support.technical'],
12
- 'qualified-lead': ['calendar.view', 'calendar.book', 'messages.auto-reply', 'info.business'],
13
- lead: ['messages.auto-reply', 'info.business'],
14
- team: ['calendar.view', 'calendar.book', 'calendar.modify', 'messages.send', 'messages.auto-reply', 'tasks.create', 'info.business', 'reminders.create'],
15
- unknown: ['messages.auto-reply'],
16
- blocked: [],
17
- };
18
-
19
- function log(msg, data) {
20
- const ts = new Date().toISOString();
21
- if (data) console.log(`[${ts}] [CONTACT-MGR] ${msg}`, JSON.stringify(data));
22
- else console.log(`[${ts}] [CONTACT-MGR] ${msg}`);
23
- }
24
-
25
- /**
26
- * Get a contact by phone number. Returns null if not found.
27
- * @param {string} phone - E.164 phone
28
- * @returns {Object|null}
29
- */
30
- function getContact(phone) {
31
- const stmts = getStmts();
32
- const contact = stmts.getContact.get(normalizePhone(phone));
33
- if (!contact) return null;
34
- const context = stmts.getContext.get(contact.phone) || {};
35
- const scopes = stmts.getAllScopes.all(contact.phone).filter(s => s.granted).map(s => s.scope);
36
- return { ...contact, context, scopes };
37
- }
38
-
39
- /**
40
- * Get or create a contact. If the contact doesn't exist, creates with defaults.
41
- * @param {string} phone - E.164 phone
42
- * @param {Object} [defaults] - Default values for new contacts
43
- * @returns {Object}
44
- */
45
- function getOrCreateContact(phone, defaults = {}) {
46
- const normalized = normalizePhone(phone);
47
- let contact = getContact(normalized);
48
- if (contact) return contact;
49
-
50
- const stmts = getStmts();
51
- const name = defaults.name || 'Unknown';
52
- const category = defaults.category || 'unknown';
53
- const source = defaults.source || 'unknown';
54
-
55
- stmts.upsertContact.run(normalized, name, category, 'auto', 'casual', '', source, 0, 0, 'none');
56
- stmts.upsertContext.run(normalized, '', '', '[]', '', '{}');
57
-
58
- // Apply default scopes for category
59
- const categoryScopes = DEFAULT_SCOPES[category] || DEFAULT_SCOPES.unknown;
60
- for (const scope of categoryScopes) {
61
- stmts.upsertScope.run(normalized, scope, 1, 'auto');
62
- }
63
-
64
- log('Created new contact', { phone: normalized, name, category, source });
65
- return getContact(normalized);
66
- }
67
-
68
- /**
69
- * Update a contact's fields.
70
- * @param {string} phone - E.164 phone
71
- * @param {Object} updates - Fields to update
72
- * @returns {Object|null} Updated contact
73
- */
74
- function updateContact(phone, updates) {
75
- const normalized = normalizePhone(phone);
76
- const existing = getContact(normalized);
77
- if (!existing) return null;
78
-
79
- const stmts = getStmts();
80
- stmts.upsertContact.run(
81
- normalized,
82
- updates.name ?? existing.name,
83
- updates.category ?? existing.category,
84
- updates.response_mode ?? existing.response_mode,
85
- updates.style ?? existing.style,
86
- updates.instructions ?? existing.instructions,
87
- updates.source ?? existing.source,
88
- updates.introduced ?? existing.introduced,
89
- updates.qualification_score ?? existing.qualification_score,
90
- updates.pipeline_stage ?? existing.pipeline_stage,
91
- );
92
-
93
- // If category changed, apply new default scopes
94
- if (updates.category && updates.category !== existing.category) {
95
- applyDefaultScopes(normalized, updates.category);
96
- }
97
-
98
- log('Updated contact', { phone: normalized, updates: Object.keys(updates) });
99
- return getContact(normalized);
100
- }
101
-
102
- /**
103
- * Update contact context.
104
- * @param {string} phone - E.164 phone
105
- * @param {Object} contextUpdates
106
- */
107
- function updateContext(phone, contextUpdates) {
108
- const normalized = normalizePhone(phone);
109
- const stmts = getStmts();
110
- const existing = stmts.getContext.get(normalized) || {};
111
-
112
- stmts.upsertContext.run(
113
- normalized,
114
- contextUpdates.relationship ?? existing.relationship ?? '',
115
- contextUpdates.last_topic ?? existing.last_topic ?? '',
116
- contextUpdates.pending_items ?? existing.pending_items ?? '[]',
117
- contextUpdates.conversation_summary ?? existing.conversation_summary ?? '',
118
- contextUpdates.preferences ?? existing.preferences ?? '{}',
119
- );
120
- }
121
-
122
- /**
123
- * Delete a contact and all associated data.
124
- * @param {string} phone - E.164 phone
125
- * @returns {boolean}
126
- */
127
- function deleteContact(phone) {
128
- const normalized = normalizePhone(phone);
129
- const stmts = getStmts();
130
- stmts.deleteContext.run(normalized);
131
- stmts.deleteScopesByPhone.run(normalized);
132
- stmts.deleteContact.run(normalized);
133
- log('Deleted contact', { phone: normalized });
134
- return true;
135
- }
136
-
137
- /**
138
- * Get all contacts, optionally filtered.
139
- * @param {Object} [filter] - { category, search }
140
- * @returns {Array}
141
- */
142
- function listContacts(filter = {}) {
143
- const stmts = getStmts();
144
- if (filter.category) {
145
- return stmts.filterByCategory.all(filter.category);
146
- }
147
- if (filter.search) {
148
- const q = `%${filter.search}%`;
149
- return stmts.searchContacts.all(q, q);
150
- }
151
- return stmts.getAllContacts.all();
152
- }
153
-
154
- /**
155
- * Apply default scopes for a category. Does NOT revoke existing scopes.
156
- * @param {string} phone
157
- * @param {string} category
158
- */
159
- function applyDefaultScopes(phone, category) {
160
- const stmts = getStmts();
161
- const scopes = DEFAULT_SCOPES[category] || DEFAULT_SCOPES.unknown;
162
- for (const scope of scopes) {
163
- stmts.upsertScope.run(phone, scope, 1, 'auto');
164
- }
165
- }
166
-
167
- /**
168
- * Set specific scopes for a contact.
169
- * @param {string} phone
170
- * @param {Object} scopeMap - { 'calendar.book': true, 'info.personal': false }
171
- */
172
- function setScopes(phone, scopeMap) {
173
- const normalized = normalizePhone(phone);
174
- const stmts = getStmts();
175
- for (const [scope, granted] of Object.entries(scopeMap)) {
176
- stmts.upsertScope.run(normalized, scope, granted ? 1 : 0, 'manual');
177
- }
178
- }
179
-
180
- /**
181
- * Check if a contact has a specific scope.
182
- * @param {string} phone
183
- * @param {string} scope
184
- * @returns {boolean}
185
- */
186
- function hasScope(phone, scope) {
187
- const stmts = getStmts();
188
- const row = stmts.getScope.get(normalizePhone(phone), scope);
189
- return row?.granted === 1;
190
- }
191
-
192
- /**
193
- * Mark a contact as introduced by AIVA.
194
- * @param {string} phone
195
- */
196
- function markIntroduced(phone) {
197
- getStmts().markIntroduced.run(normalizePhone(phone));
198
- }
199
-
200
- /**
201
- * Update qualification score and pipeline stage.
202
- * @param {string} phone
203
- * @param {number} score - 0-100
204
- * @param {string} stage - none, cold, warm, hot, qualified, client
205
- */
206
- function updateQualification(phone, score, stage) {
207
- getStmts().updateQualification.run(score, stage, normalizePhone(phone));
208
- }
209
-
210
- /**
211
- * Check if the last outbound message was sent by Brandon (for reintro logic).
212
- * @param {string} phone
213
- * @returns {boolean}
214
- */
215
- function needsReintro(phone) {
216
- const row = getStmts().getLastSentBy.get(normalizePhone(phone));
217
- return row && row.sent_by === 'brandon';
218
- }
219
-
220
- module.exports = {
221
- getContact,
222
- getOrCreateContact,
223
- updateContact,
224
- updateContext,
225
- deleteContact,
226
- listContacts,
227
- applyDefaultScopes,
228
- setScopes,
229
- hasScope,
230
- markIntroduced,
231
- updateQualification,
232
- needsReintro,
233
- DEFAULT_SCOPES,
234
- };