@conversionpros/aiva 1.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.
- package/README.md +148 -0
- package/auto-deploy.js +190 -0
- package/bin/aiva.js +81 -0
- package/cli-sync.js +126 -0
- package/d2a-prompt-template.txt +106 -0
- package/diagnostics-api.js +304 -0
- package/docs/ara-dedup-fix-scope.md +112 -0
- package/docs/ara-fix-round2-scope.md +61 -0
- package/docs/ara-greeting-fix-scope.md +70 -0
- package/docs/calendar-date-fix-scope.md +28 -0
- package/docs/getting-started.md +115 -0
- package/docs/network-architecture-rollout-scope.md +43 -0
- package/docs/scope-google-oauth-integration.md +351 -0
- package/docs/settings-page-scope.md +50 -0
- package/docs/xai-imagine-scope.md +116 -0
- package/docs/xai-voice-integration-scope.md +115 -0
- package/docs/xai-voice-tools-scope.md +165 -0
- package/email-router.js +512 -0
- package/follow-up-handler.js +606 -0
- package/gateway-monitor.js +158 -0
- package/google-email.js +379 -0
- package/google-oauth.js +310 -0
- package/grok-imagine.js +97 -0
- package/health-reporter.js +287 -0
- package/invisible-prefix-base.txt +206 -0
- package/invisible-prefix-owner.txt +26 -0
- package/invisible-prefix-slim.txt +10 -0
- package/invisible-prefix.txt +43 -0
- package/knowledge-base.js +472 -0
- package/lib/cli.js +19 -0
- package/lib/config.js +124 -0
- package/lib/health.js +57 -0
- package/lib/process.js +207 -0
- package/lib/server.js +42 -0
- package/lib/setup.js +472 -0
- package/meta-capi.js +206 -0
- package/meta-leads.js +411 -0
- package/notion-oauth.js +323 -0
- package/package.json +61 -0
- package/public/agent-config.html +241 -0
- package/public/aiva-avatar-anime.png +0 -0
- package/public/css/docs.css.bak +688 -0
- package/public/css/onboarding.css +543 -0
- package/public/diagrams/claude-subscription-pool.html +329 -0
- package/public/diagrams/claude-subscription-pool.png +0 -0
- package/public/docs-icon.png +0 -0
- package/public/escalation.html +237 -0
- package/public/group-config.html +300 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icons/agents.svg +1 -0
- package/public/icons/attach.svg +1 -0
- package/public/icons/characters.svg +1 -0
- package/public/icons/chat.svg +1 -0
- package/public/icons/docs.svg +1 -0
- package/public/icons/heartbeat.svg +1 -0
- package/public/icons/messages.svg +1 -0
- package/public/icons/mic.svg +1 -0
- package/public/icons/notes.svg +1 -0
- package/public/icons/settings.svg +1 -0
- package/public/icons/tasks.svg +1 -0
- package/public/images/onboarding/p0-communication-layer.png +0 -0
- package/public/images/onboarding/p0-infinite-surface.png +0 -0
- package/public/images/onboarding/p0-learning-model.png +0 -0
- package/public/images/onboarding/p0-meet-aiva.png +0 -0
- package/public/images/onboarding/p4-contact-intelligence.png +0 -0
- package/public/images/onboarding/p4-context-compounds.png +0 -0
- package/public/images/onboarding/p4-message-router.png +0 -0
- package/public/images/onboarding/p4-per-contact-rules.png +0 -0
- package/public/images/onboarding/p4-send-messages.png +0 -0
- package/public/images/onboarding/p6-be-precise.png +0 -0
- package/public/images/onboarding/p6-review-escalations.png +0 -0
- package/public/images/onboarding/p6-voice-input.png +0 -0
- package/public/images/onboarding/p7-completion.png +0 -0
- package/public/index.html +11594 -0
- package/public/js/onboarding.js +699 -0
- package/public/manifest.json +24 -0
- package/public/messages-v2.html +2824 -0
- package/public/permission-approve.html.bak +107 -0
- package/public/permissions.html +150 -0
- package/public/styles/design-system.css +68 -0
- package/router-db.js +604 -0
- package/router-utils.js +28 -0
- package/router-v2/adapters/imessage.js +191 -0
- package/router-v2/adapters/quo.js +82 -0
- package/router-v2/adapters/whatsapp.js +192 -0
- package/router-v2/contact-manager.js +234 -0
- package/router-v2/conversation-engine.js +498 -0
- package/router-v2/data/knowledge-base.json +176 -0
- package/router-v2/data/router-v2.db +0 -0
- package/router-v2/data/router-v2.db-shm +0 -0
- package/router-v2/data/router-v2.db-wal +0 -0
- package/router-v2/data/router.db +0 -0
- package/router-v2/db.js +457 -0
- package/router-v2/escalation-bridge.js +540 -0
- package/router-v2/follow-up-engine.js +347 -0
- package/router-v2/index.js +441 -0
- package/router-v2/ingestion.js +213 -0
- package/router-v2/knowledge-base.js +231 -0
- package/router-v2/lead-qualifier.js +152 -0
- package/router-v2/learning-loop.js +202 -0
- package/router-v2/outbound-sender.js +160 -0
- package/router-v2/package.json +13 -0
- package/router-v2/permission-gate.js +86 -0
- package/router-v2/playbook.js +177 -0
- package/router-v2/prompts/base.js +52 -0
- package/router-v2/prompts/first-contact.js +38 -0
- package/router-v2/prompts/lead-qualification.js +37 -0
- package/router-v2/prompts/scheduling.js +72 -0
- package/router-v2/prompts/style-overrides.js +22 -0
- package/router-v2/scheduler.js +301 -0
- package/router-v2/scripts/migrate-v1-to-v2.js +215 -0
- package/router-v2/scripts/seed-faq.js +67 -0
- package/router-v2/seed-knowledge-base.js +39 -0
- package/router-v2/utils/ai.js +129 -0
- package/router-v2/utils/phone.js +52 -0
- package/router-v2/utils/response-validator.js +98 -0
- package/router-v2/utils/sanitize.js +222 -0
- package/router.js +5005 -0
- package/routes/google-calendar.js +186 -0
- package/scripts/deploy.sh +62 -0
- package/scripts/macos-calendar.sh +232 -0
- package/scripts/onboard-device.sh +466 -0
- package/server.js +5131 -0
- package/start.sh +24 -0
- package/templates/AGENTS.md +548 -0
- package/templates/IDENTITY.md +15 -0
- package/templates/docs-agents.html +132 -0
- package/templates/docs-app.html +130 -0
- package/templates/docs-home.html +83 -0
- package/templates/docs-imessage.html +121 -0
- package/templates/docs-tasks.html +123 -0
- package/templates/docs-tips.html +175 -0
- package/templates/getting-started.html +809 -0
- package/templates/invisible-prefix-base.txt +171 -0
- package/templates/invisible-prefix-owner.txt +282 -0
- package/templates/invisible-prefix.txt +338 -0
- package/templates/manifest.json +61 -0
- package/templates/memory-org/clients.md +7 -0
- package/templates/memory-org/credentials.md +9 -0
- package/templates/memory-org/devices.md +7 -0
- package/templates/updates.html +464 -0
- package/templates/workspace/AGENTS.md.tmpl +161 -0
- package/templates/workspace/HEARTBEAT.md.tmpl +17 -0
- package/templates/workspace/IDENTITY.md.tmpl +15 -0
- package/templates/workspace/MEMORY.md.tmpl +16 -0
- package/templates/workspace/SOUL.md.tmpl +51 -0
- package/templates/workspace/USER.md.tmpl +25 -0
- package/tts-proxy.js +96 -0
- package/voice-call-local.js +731 -0
- package/voice-call.js +732 -0
- package/wa-listener.js +354 -0
package/wa-listener.js
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// ── WhatsApp Message Listener ────────────────────────────
|
|
2
|
+
// Polls wacli for new incoming WhatsApp messages and forwards them
|
|
3
|
+
// to the AIVA router webhook for processing.
|
|
4
|
+
//
|
|
5
|
+
// Architecture: wacli sync runs as a background process maintaining
|
|
6
|
+
// the WhatsApp connection. This module polls wacli's local DB for
|
|
7
|
+
// new messages and POSTs them to /webhook/whatsapp.
|
|
8
|
+
// Sending: sync must be stopped briefly to release the store lock,
|
|
9
|
+
// then send, then restart sync.
|
|
10
|
+
|
|
11
|
+
const { execSync, spawn } = require('child_process');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const WEBHOOK_URL = 'http://127.0.0.1:3847/webhook/whatsapp';
|
|
16
|
+
const POLL_INTERVAL_MS = 5000; // 5 seconds
|
|
17
|
+
const WACLI_BIN = '/opt/homebrew/bin/wacli';
|
|
18
|
+
|
|
19
|
+
// Check if wacli binary exists
|
|
20
|
+
let WACLI_AVAILABLE = false;
|
|
21
|
+
try {
|
|
22
|
+
fs.accessSync(WACLI_BIN, fs.constants.X_OK);
|
|
23
|
+
WACLI_AVAILABLE = true;
|
|
24
|
+
} catch {
|
|
25
|
+
// Also check PATH
|
|
26
|
+
try {
|
|
27
|
+
execSync('which wacli', { timeout: 3000, encoding: 'utf-8' });
|
|
28
|
+
WACLI_AVAILABLE = true;
|
|
29
|
+
} catch {
|
|
30
|
+
WACLI_AVAILABLE = false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Track processed messages to avoid duplicates
|
|
35
|
+
const processedMessageIds = new Set();
|
|
36
|
+
const MAX_PROCESSED_IDS = 5000;
|
|
37
|
+
let lastPollTimestamp = null; // ISO timestamp of last poll
|
|
38
|
+
let pollTimer = null;
|
|
39
|
+
let syncProcess = null;
|
|
40
|
+
let syncPid = null;
|
|
41
|
+
|
|
42
|
+
// Send queue -- serialized to avoid lock contention
|
|
43
|
+
let sendQueue = [];
|
|
44
|
+
let sendProcessing = false;
|
|
45
|
+
|
|
46
|
+
function log(msg, data) {
|
|
47
|
+
const ts = new Date().toISOString();
|
|
48
|
+
if (data) console.log(`[${ts}] [WA-LISTENER] ${msg}`, JSON.stringify(data));
|
|
49
|
+
else console.log(`[${ts}] [WA-LISTENER] ${msg}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isAuthenticated() {
|
|
53
|
+
try {
|
|
54
|
+
const output = execSync(`${WACLI_BIN} doctor`, { timeout: 10000, encoding: 'utf-8' });
|
|
55
|
+
return output.includes('AUTHENTICATED true');
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isLocked() {
|
|
62
|
+
try {
|
|
63
|
+
const output = execSync(`${WACLI_BIN} doctor`, { timeout: 10000, encoding: 'utf-8' });
|
|
64
|
+
return output.includes('LOCKED true');
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function killAllWacliSync() {
|
|
71
|
+
try {
|
|
72
|
+
execSync('pkill -f "wacli (sync|auth)"', { timeout: 5000 });
|
|
73
|
+
log('Killed existing wacli sync processes');
|
|
74
|
+
} catch {
|
|
75
|
+
// No processes to kill
|
|
76
|
+
}
|
|
77
|
+
syncProcess = null;
|
|
78
|
+
syncPid = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function startSync() {
|
|
82
|
+
// Check if already running
|
|
83
|
+
try {
|
|
84
|
+
const ps = execSync('pgrep -f "wacli (sync|auth)"', { timeout: 5000, encoding: 'utf-8' }).trim();
|
|
85
|
+
if (ps) {
|
|
86
|
+
syncPid = parseInt(ps.split('\n')[0]);
|
|
87
|
+
log('Sync already running', { pid: syncPid });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// No process found
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (isLocked()) {
|
|
95
|
+
log('Store locked by another process, waiting...');
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
syncProcess = spawn(WACLI_BIN, ['sync', '--follow', '--refresh-contacts'], {
|
|
101
|
+
detached: true,
|
|
102
|
+
stdio: 'ignore',
|
|
103
|
+
});
|
|
104
|
+
syncProcess.unref();
|
|
105
|
+
syncPid = syncProcess.pid;
|
|
106
|
+
log('Started wacli sync', { pid: syncPid });
|
|
107
|
+
return true;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
log('Failed to start wacli sync', { error: err.message });
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function stopSync() {
|
|
115
|
+
if (syncPid) {
|
|
116
|
+
try {
|
|
117
|
+
process.kill(syncPid, 'SIGTERM');
|
|
118
|
+
log('Stopped sync process', { pid: syncPid });
|
|
119
|
+
} catch {
|
|
120
|
+
// Process already dead
|
|
121
|
+
}
|
|
122
|
+
syncProcess = null;
|
|
123
|
+
syncPid = null;
|
|
124
|
+
}
|
|
125
|
+
// Also kill any stray wacli sync processes
|
|
126
|
+
killAllWacliSync();
|
|
127
|
+
// Wait for processes to die and lock to release
|
|
128
|
+
try { execSync('sleep 0.5'); } catch {}
|
|
129
|
+
// Force kill if anything survived
|
|
130
|
+
try { execSync('pkill -9 -f "wacli sync"', { timeout: 3000 }); } catch {}
|
|
131
|
+
try { execSync('sleep 0.3'); } catch {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Send a WhatsApp message. Handles store lock by stopping sync,
|
|
136
|
+
* sending, then restarting sync.
|
|
137
|
+
* @param {string} phone - Phone number with + prefix
|
|
138
|
+
* @param {string} text - Message text
|
|
139
|
+
* @returns {Promise<{success: boolean, error?: string}>}
|
|
140
|
+
*/
|
|
141
|
+
function sendMessage(phone, text) {
|
|
142
|
+
if (!WACLI_AVAILABLE) {
|
|
143
|
+
log('sendMessage called but wacli not installed -- skipping');
|
|
144
|
+
return Promise.resolve({ success: false, error: 'wacli not installed' });
|
|
145
|
+
}
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
sendQueue.push({ phone, text, resolve });
|
|
148
|
+
processSendQueue();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function processSendQueue() {
|
|
153
|
+
if (sendProcessing || sendQueue.length === 0) return;
|
|
154
|
+
sendProcessing = true;
|
|
155
|
+
|
|
156
|
+
// Stop sync to release the lock
|
|
157
|
+
stopSync();
|
|
158
|
+
|
|
159
|
+
// Wait for lock to fully release and WA session to free up
|
|
160
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
161
|
+
|
|
162
|
+
while (sendQueue.length > 0) {
|
|
163
|
+
const { phone, text, resolve } = sendQueue.shift();
|
|
164
|
+
log('Sending WhatsApp message', { phone, textLen: text.length });
|
|
165
|
+
try {
|
|
166
|
+
const result = await new Promise((res, rej) => {
|
|
167
|
+
const child = require('child_process').exec(
|
|
168
|
+
`${WACLI_BIN} send text --to "${phone.replace(/^\+/, '')}" --message ${JSON.stringify(text)} --timeout 60s`,
|
|
169
|
+
{ timeout: 90000, encoding: 'utf-8' },
|
|
170
|
+
(err, stdout, stderr) => {
|
|
171
|
+
if (err) rej(err);
|
|
172
|
+
else res(stdout);
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
log('WhatsApp send success', { phone, result: (result || '').trim().substring(0, 100) });
|
|
177
|
+
resolve({ success: true });
|
|
178
|
+
} catch (err) {
|
|
179
|
+
log('WhatsApp send FAILED', { phone, error: err.message });
|
|
180
|
+
resolve({ success: false, error: err.message });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Restart sync
|
|
185
|
+
sendProcessing = false;
|
|
186
|
+
await new Promise(r => setTimeout(r, 300));
|
|
187
|
+
startSync();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function fetchNewMessages() {
|
|
191
|
+
try {
|
|
192
|
+
let cmd = `${WACLI_BIN} messages list --json --limit 20`;
|
|
193
|
+
if (lastPollTimestamp) {
|
|
194
|
+
cmd += ` --after "${lastPollTimestamp}"`;
|
|
195
|
+
} else {
|
|
196
|
+
const since = new Date(Date.now() - 60000).toISOString();
|
|
197
|
+
cmd += ` --after "${since}"`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const output = execSync(cmd, { timeout: 15000, encoding: 'utf-8' });
|
|
201
|
+
const result = JSON.parse(output);
|
|
202
|
+
|
|
203
|
+
if (!result.success || !result.data) {
|
|
204
|
+
log('fetchNewMessages: no success/data', { success: result.success, hasData: !!result.data });
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
const msgs = result.data.messages;
|
|
208
|
+
if (!Array.isArray(msgs)) return [];
|
|
209
|
+
return msgs;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
if (err.message && (err.message.includes('unknown flag') || err.message.includes('not found'))) {
|
|
212
|
+
return fetchNewMessagesFallback();
|
|
213
|
+
}
|
|
214
|
+
// Don't log lock errors during send operations
|
|
215
|
+
if (!sendProcessing) {
|
|
216
|
+
log('Failed to fetch messages', { error: err.message.substring(0, 150) });
|
|
217
|
+
}
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function fetchNewMessagesFallback() {
|
|
223
|
+
try {
|
|
224
|
+
const cmd = `${WACLI_BIN} messages list --json --limit 20`;
|
|
225
|
+
const output = execSync(cmd, { timeout: 15000, encoding: 'utf-8' });
|
|
226
|
+
const result = JSON.parse(output);
|
|
227
|
+
|
|
228
|
+
if (!result.success || !result.data) return [];
|
|
229
|
+
|
|
230
|
+
const messages = result.data.messages;
|
|
231
|
+
if (!Array.isArray(messages)) return [];
|
|
232
|
+
|
|
233
|
+
const cutoff = lastPollTimestamp ? new Date(lastPollTimestamp).getTime() : (Date.now() - 60000);
|
|
234
|
+
return messages.filter(m => {
|
|
235
|
+
const msgTime = new Date(m.Timestamp || m.timestamp || 0).getTime();
|
|
236
|
+
return !m.FromMe && !m.fromMe && msgTime > cutoff;
|
|
237
|
+
});
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (!sendProcessing) {
|
|
240
|
+
log('Fallback fetch failed', { error: err.message.substring(0, 150) });
|
|
241
|
+
}
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function forwardToRouter(phone, text, messageId) {
|
|
247
|
+
const dedupKey = messageId || `${phone}:${text.substring(0, 100)}`;
|
|
248
|
+
if (processedMessageIds.has(dedupKey)) return;
|
|
249
|
+
|
|
250
|
+
processedMessageIds.add(dedupKey);
|
|
251
|
+
|
|
252
|
+
// Prune old entries
|
|
253
|
+
if (processedMessageIds.size > MAX_PROCESSED_IDS) {
|
|
254
|
+
const arr = [...processedMessageIds];
|
|
255
|
+
arr.splice(0, arr.length - MAX_PROCESSED_IDS / 2);
|
|
256
|
+
processedMessageIds.clear();
|
|
257
|
+
arr.forEach(id => processedMessageIds.add(id));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
log('Forwarding WA message to router', { phone, textLen: text.length, messageId });
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const resp = await fetch(WEBHOOK_URL, {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: { 'Content-Type': 'application/json' },
|
|
266
|
+
body: JSON.stringify({
|
|
267
|
+
from: phone,
|
|
268
|
+
text: text,
|
|
269
|
+
fromMe: false,
|
|
270
|
+
messageId: messageId,
|
|
271
|
+
_source: 'whatsapp'
|
|
272
|
+
}),
|
|
273
|
+
signal: AbortSignal.timeout(30000),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const result = await resp.json();
|
|
277
|
+
log('Router response', { phone, status: result.status, action: result.action });
|
|
278
|
+
} catch (err) {
|
|
279
|
+
log('Failed to forward to router', { phone, error: err.message });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function normalizePhone(jid) {
|
|
284
|
+
if (!jid) return null;
|
|
285
|
+
let phone = jid.split('@')[0];
|
|
286
|
+
phone = phone.split(':')[0];
|
|
287
|
+
if (!phone || phone.length < 7) return null;
|
|
288
|
+
return phone.startsWith('+') ? phone : '+' + phone;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function pollMessages() {
|
|
292
|
+
// Don't poll while sending (sync is stopped for send)
|
|
293
|
+
if (sendProcessing) return;
|
|
294
|
+
|
|
295
|
+
// Ensure sync is running (receives messages from WA servers)
|
|
296
|
+
startSync();
|
|
297
|
+
|
|
298
|
+
const messages = fetchNewMessages();
|
|
299
|
+
if (!messages || !Array.isArray(messages)) return;
|
|
300
|
+
|
|
301
|
+
for (const msg of messages) {
|
|
302
|
+
if (msg.FromMe || msg.fromMe) continue;
|
|
303
|
+
|
|
304
|
+
const chatJid = msg.ChatJID || msg.chatJID || msg.chat || '';
|
|
305
|
+
if (chatJid.includes('@g.us')) continue;
|
|
306
|
+
|
|
307
|
+
const phone = normalizePhone(msg.SenderJID || msg.senderJID || msg.sender || chatJid);
|
|
308
|
+
const text = msg.Text || msg.DisplayText || msg.Snippet || msg.text || msg.Body || msg.body || '';
|
|
309
|
+
const messageId = msg.MsgID || msg.ID || msg.id || msg.MessageID || msg.messageId || '';
|
|
310
|
+
|
|
311
|
+
if (!phone || !text) continue;
|
|
312
|
+
|
|
313
|
+
await forwardToRouter(phone, text, messageId);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
lastPollTimestamp = new Date().toISOString();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function start() {
|
|
320
|
+
if (!WACLI_AVAILABLE) {
|
|
321
|
+
log('wacli not installed -- WhatsApp listener disabled');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
log('Starting WhatsApp listener (wacli polling)');
|
|
326
|
+
|
|
327
|
+
if (!isAuthenticated()) {
|
|
328
|
+
log('wacli not authenticated -- WhatsApp listener inactive. Run `wacli auth` to connect.');
|
|
329
|
+
} else {
|
|
330
|
+
log('wacli authenticated, ensuring sync is running');
|
|
331
|
+
startSync();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Look back 10 minutes on startup to catch messages that arrived during downtime
|
|
335
|
+
lastPollTimestamp = new Date(Date.now() - 600000).toISOString();
|
|
336
|
+
|
|
337
|
+
pollTimer = setInterval(async () => {
|
|
338
|
+
try {
|
|
339
|
+
await pollMessages();
|
|
340
|
+
} catch (err) {
|
|
341
|
+
log('Poll error', { error: err.message });
|
|
342
|
+
}
|
|
343
|
+
}, POLL_INTERVAL_MS);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function stop() {
|
|
347
|
+
if (pollTimer) {
|
|
348
|
+
clearInterval(pollTimer);
|
|
349
|
+
pollTimer = null;
|
|
350
|
+
}
|
|
351
|
+
log('WhatsApp listener stopped');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
module.exports = { start, stop, sendMessage };
|