@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.
Files changed (152) hide show
  1. package/README.md +148 -0
  2. package/auto-deploy.js +190 -0
  3. package/bin/aiva.js +81 -0
  4. package/cli-sync.js +126 -0
  5. package/d2a-prompt-template.txt +106 -0
  6. package/diagnostics-api.js +304 -0
  7. package/docs/ara-dedup-fix-scope.md +112 -0
  8. package/docs/ara-fix-round2-scope.md +61 -0
  9. package/docs/ara-greeting-fix-scope.md +70 -0
  10. package/docs/calendar-date-fix-scope.md +28 -0
  11. package/docs/getting-started.md +115 -0
  12. package/docs/network-architecture-rollout-scope.md +43 -0
  13. package/docs/scope-google-oauth-integration.md +351 -0
  14. package/docs/settings-page-scope.md +50 -0
  15. package/docs/xai-imagine-scope.md +116 -0
  16. package/docs/xai-voice-integration-scope.md +115 -0
  17. package/docs/xai-voice-tools-scope.md +165 -0
  18. package/email-router.js +512 -0
  19. package/follow-up-handler.js +606 -0
  20. package/gateway-monitor.js +158 -0
  21. package/google-email.js +379 -0
  22. package/google-oauth.js +310 -0
  23. package/grok-imagine.js +97 -0
  24. package/health-reporter.js +287 -0
  25. package/invisible-prefix-base.txt +206 -0
  26. package/invisible-prefix-owner.txt +26 -0
  27. package/invisible-prefix-slim.txt +10 -0
  28. package/invisible-prefix.txt +43 -0
  29. package/knowledge-base.js +472 -0
  30. package/lib/cli.js +19 -0
  31. package/lib/config.js +124 -0
  32. package/lib/health.js +57 -0
  33. package/lib/process.js +207 -0
  34. package/lib/server.js +42 -0
  35. package/lib/setup.js +472 -0
  36. package/meta-capi.js +206 -0
  37. package/meta-leads.js +411 -0
  38. package/notion-oauth.js +323 -0
  39. package/package.json +61 -0
  40. package/public/agent-config.html +241 -0
  41. package/public/aiva-avatar-anime.png +0 -0
  42. package/public/css/docs.css.bak +688 -0
  43. package/public/css/onboarding.css +543 -0
  44. package/public/diagrams/claude-subscription-pool.html +329 -0
  45. package/public/diagrams/claude-subscription-pool.png +0 -0
  46. package/public/docs-icon.png +0 -0
  47. package/public/escalation.html +237 -0
  48. package/public/group-config.html +300 -0
  49. package/public/icon-192.png +0 -0
  50. package/public/icon-512.png +0 -0
  51. package/public/icons/agents.svg +1 -0
  52. package/public/icons/attach.svg +1 -0
  53. package/public/icons/characters.svg +1 -0
  54. package/public/icons/chat.svg +1 -0
  55. package/public/icons/docs.svg +1 -0
  56. package/public/icons/heartbeat.svg +1 -0
  57. package/public/icons/messages.svg +1 -0
  58. package/public/icons/mic.svg +1 -0
  59. package/public/icons/notes.svg +1 -0
  60. package/public/icons/settings.svg +1 -0
  61. package/public/icons/tasks.svg +1 -0
  62. package/public/images/onboarding/p0-communication-layer.png +0 -0
  63. package/public/images/onboarding/p0-infinite-surface.png +0 -0
  64. package/public/images/onboarding/p0-learning-model.png +0 -0
  65. package/public/images/onboarding/p0-meet-aiva.png +0 -0
  66. package/public/images/onboarding/p4-contact-intelligence.png +0 -0
  67. package/public/images/onboarding/p4-context-compounds.png +0 -0
  68. package/public/images/onboarding/p4-message-router.png +0 -0
  69. package/public/images/onboarding/p4-per-contact-rules.png +0 -0
  70. package/public/images/onboarding/p4-send-messages.png +0 -0
  71. package/public/images/onboarding/p6-be-precise.png +0 -0
  72. package/public/images/onboarding/p6-review-escalations.png +0 -0
  73. package/public/images/onboarding/p6-voice-input.png +0 -0
  74. package/public/images/onboarding/p7-completion.png +0 -0
  75. package/public/index.html +11594 -0
  76. package/public/js/onboarding.js +699 -0
  77. package/public/manifest.json +24 -0
  78. package/public/messages-v2.html +2824 -0
  79. package/public/permission-approve.html.bak +107 -0
  80. package/public/permissions.html +150 -0
  81. package/public/styles/design-system.css +68 -0
  82. package/router-db.js +604 -0
  83. package/router-utils.js +28 -0
  84. package/router-v2/adapters/imessage.js +191 -0
  85. package/router-v2/adapters/quo.js +82 -0
  86. package/router-v2/adapters/whatsapp.js +192 -0
  87. package/router-v2/contact-manager.js +234 -0
  88. package/router-v2/conversation-engine.js +498 -0
  89. package/router-v2/data/knowledge-base.json +176 -0
  90. package/router-v2/data/router-v2.db +0 -0
  91. package/router-v2/data/router-v2.db-shm +0 -0
  92. package/router-v2/data/router-v2.db-wal +0 -0
  93. package/router-v2/data/router.db +0 -0
  94. package/router-v2/db.js +457 -0
  95. package/router-v2/escalation-bridge.js +540 -0
  96. package/router-v2/follow-up-engine.js +347 -0
  97. package/router-v2/index.js +441 -0
  98. package/router-v2/ingestion.js +213 -0
  99. package/router-v2/knowledge-base.js +231 -0
  100. package/router-v2/lead-qualifier.js +152 -0
  101. package/router-v2/learning-loop.js +202 -0
  102. package/router-v2/outbound-sender.js +160 -0
  103. package/router-v2/package.json +13 -0
  104. package/router-v2/permission-gate.js +86 -0
  105. package/router-v2/playbook.js +177 -0
  106. package/router-v2/prompts/base.js +52 -0
  107. package/router-v2/prompts/first-contact.js +38 -0
  108. package/router-v2/prompts/lead-qualification.js +37 -0
  109. package/router-v2/prompts/scheduling.js +72 -0
  110. package/router-v2/prompts/style-overrides.js +22 -0
  111. package/router-v2/scheduler.js +301 -0
  112. package/router-v2/scripts/migrate-v1-to-v2.js +215 -0
  113. package/router-v2/scripts/seed-faq.js +67 -0
  114. package/router-v2/seed-knowledge-base.js +39 -0
  115. package/router-v2/utils/ai.js +129 -0
  116. package/router-v2/utils/phone.js +52 -0
  117. package/router-v2/utils/response-validator.js +98 -0
  118. package/router-v2/utils/sanitize.js +222 -0
  119. package/router.js +5005 -0
  120. package/routes/google-calendar.js +186 -0
  121. package/scripts/deploy.sh +62 -0
  122. package/scripts/macos-calendar.sh +232 -0
  123. package/scripts/onboard-device.sh +466 -0
  124. package/server.js +5131 -0
  125. package/start.sh +24 -0
  126. package/templates/AGENTS.md +548 -0
  127. package/templates/IDENTITY.md +15 -0
  128. package/templates/docs-agents.html +132 -0
  129. package/templates/docs-app.html +130 -0
  130. package/templates/docs-home.html +83 -0
  131. package/templates/docs-imessage.html +121 -0
  132. package/templates/docs-tasks.html +123 -0
  133. package/templates/docs-tips.html +175 -0
  134. package/templates/getting-started.html +809 -0
  135. package/templates/invisible-prefix-base.txt +171 -0
  136. package/templates/invisible-prefix-owner.txt +282 -0
  137. package/templates/invisible-prefix.txt +338 -0
  138. package/templates/manifest.json +61 -0
  139. package/templates/memory-org/clients.md +7 -0
  140. package/templates/memory-org/credentials.md +9 -0
  141. package/templates/memory-org/devices.md +7 -0
  142. package/templates/updates.html +464 -0
  143. package/templates/workspace/AGENTS.md.tmpl +161 -0
  144. package/templates/workspace/HEARTBEAT.md.tmpl +17 -0
  145. package/templates/workspace/IDENTITY.md.tmpl +15 -0
  146. package/templates/workspace/MEMORY.md.tmpl +16 -0
  147. package/templates/workspace/SOUL.md.tmpl +51 -0
  148. package/templates/workspace/USER.md.tmpl +25 -0
  149. package/tts-proxy.js +96 -0
  150. package/voice-call-local.js +731 -0
  151. package/voice-call.js +732 -0
  152. package/wa-listener.js +354 -0
Binary file
File without changes
@@ -0,0 +1,457 @@
1
+ // ── Database Setup, Migrations, Prepared Statements ──────
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ const DB_DIR = path.join(__dirname, 'data');
8
+ const DB_PATH = path.join(DB_DIR, 'router-v2.db');
9
+
10
+ let db = null;
11
+ let stmts = {};
12
+
13
+ /**
14
+ * Initialize the database. Creates tables if they don't exist.
15
+ * Must be called once at startup before any other db operations.
16
+ * @returns {{ db: import('better-sqlite3').Database, stmts: Object }}
17
+ */
18
+ function initDatabase() {
19
+ const Database = require('better-sqlite3');
20
+ if (!fs.existsSync(DB_DIR)) fs.mkdirSync(DB_DIR, { recursive: true });
21
+
22
+ db = new Database(DB_PATH);
23
+ db.pragma('journal_mode = WAL');
24
+ db.pragma('foreign_keys = ON');
25
+
26
+ createTables();
27
+ prepareStatements();
28
+
29
+ console.log(`[db] Initialized at ${DB_PATH}`);
30
+ return { db, stmts };
31
+ }
32
+
33
+ function createTables() {
34
+ // ── Contacts ──
35
+ db.exec(`
36
+ CREATE TABLE IF NOT EXISTS contacts (
37
+ phone TEXT PRIMARY KEY,
38
+ name TEXT NOT NULL DEFAULT 'Unknown',
39
+ category TEXT NOT NULL DEFAULT 'unknown',
40
+ response_mode TEXT NOT NULL DEFAULT 'auto',
41
+ style TEXT NOT NULL DEFAULT 'casual',
42
+ instructions TEXT DEFAULT '',
43
+ source TEXT DEFAULT 'unknown',
44
+ introduced INTEGER DEFAULT 0,
45
+ qualification_score INTEGER DEFAULT 0,
46
+ pipeline_stage TEXT DEFAULT 'none',
47
+ created_at DATETIME DEFAULT (datetime('now')),
48
+ updated_at DATETIME DEFAULT (datetime('now'))
49
+ )
50
+ `);
51
+
52
+ // ── Contact Context ──
53
+ db.exec(`
54
+ CREATE TABLE IF NOT EXISTS contact_context (
55
+ phone TEXT PRIMARY KEY,
56
+ relationship TEXT DEFAULT '',
57
+ last_topic TEXT DEFAULT '',
58
+ pending_items TEXT DEFAULT '[]',
59
+ conversation_summary TEXT DEFAULT '',
60
+ preferences TEXT DEFAULT '{}',
61
+ last_interaction DATETIME,
62
+ FOREIGN KEY (phone) REFERENCES contacts(phone) ON DELETE CASCADE
63
+ )
64
+ `);
65
+
66
+ // ── Contact Scopes ──
67
+ db.exec(`
68
+ CREATE TABLE IF NOT EXISTS contact_scopes (
69
+ phone TEXT NOT NULL,
70
+ scope TEXT NOT NULL,
71
+ granted INTEGER DEFAULT 0,
72
+ granted_by TEXT DEFAULT 'manual',
73
+ granted_at DATETIME,
74
+ PRIMARY KEY (phone, scope)
75
+ )
76
+ `);
77
+
78
+ // ── Conversation State ──
79
+ db.exec(`
80
+ CREATE TABLE IF NOT EXISTS conversation_state (
81
+ phone TEXT PRIMARY KEY,
82
+ state TEXT NOT NULL DEFAULT 'idle',
83
+ state_data TEXT DEFAULT '{}',
84
+ entered_at DATETIME DEFAULT (datetime('now')),
85
+ expires_at DATETIME
86
+ )
87
+ `);
88
+
89
+ // ── Message Log ──
90
+ db.exec(`
91
+ CREATE TABLE IF NOT EXISTS message_log (
92
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
93
+ phone TEXT NOT NULL,
94
+ channel TEXT DEFAULT 'imessage',
95
+ direction TEXT NOT NULL DEFAULT 'inbound',
96
+ text TEXT DEFAULT '',
97
+ attachments TEXT DEFAULT '[]',
98
+ sent_by TEXT DEFAULT 'contact',
99
+ state_at_time TEXT DEFAULT '',
100
+ created_at DATETIME DEFAULT (datetime('now'))
101
+ )
102
+ `);
103
+
104
+ // ── FAQ Entries ──
105
+ db.exec(`
106
+ CREATE TABLE IF NOT EXISTS faq_entries (
107
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
108
+ device_id TEXT,
109
+ question TEXT NOT NULL,
110
+ answer TEXT NOT NULL,
111
+ category TEXT DEFAULT 'general',
112
+ keywords TEXT DEFAULT '',
113
+ enabled INTEGER DEFAULT 1,
114
+ created_at DATETIME DEFAULT (datetime('now'))
115
+ )
116
+ `);
117
+
118
+ // ── Playbook Sections ──
119
+ db.exec(`
120
+ CREATE TABLE IF NOT EXISTS playbook_sections (
121
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
122
+ device_id TEXT,
123
+ section_type TEXT NOT NULL,
124
+ title TEXT DEFAULT '',
125
+ content TEXT NOT NULL,
126
+ priority INTEGER DEFAULT 10,
127
+ enabled INTEGER DEFAULT 1,
128
+ version INTEGER DEFAULT 1,
129
+ created_at DATETIME DEFAULT (datetime('now')),
130
+ updated_at DATETIME DEFAULT (datetime('now'))
131
+ )
132
+ `);
133
+
134
+ // ── Preferences (Learning Loop) ──
135
+ db.exec(`
136
+ CREATE TABLE IF NOT EXISTS preferences (
137
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
138
+ scope TEXT DEFAULT 'global',
139
+ question_pattern TEXT NOT NULL,
140
+ answer TEXT NOT NULL,
141
+ source TEXT DEFAULT 'router',
142
+ confidence REAL DEFAULT 0.7,
143
+ hit_count INTEGER DEFAULT 0,
144
+ last_used_at DATETIME,
145
+ created_at DATETIME DEFAULT (datetime('now')),
146
+ updated_at DATETIME DEFAULT (datetime('now'))
147
+ )
148
+ `);
149
+
150
+ // ── Preference Log (audit trail) ──
151
+ db.exec(`
152
+ CREATE TABLE IF NOT EXISTS preference_log (
153
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
154
+ preference_id INTEGER,
155
+ escalation_id TEXT,
156
+ phone TEXT,
157
+ original_question TEXT,
158
+ tier_resolved TEXT,
159
+ created_at DATETIME DEFAULT (datetime('now')),
160
+ FOREIGN KEY (preference_id) REFERENCES preferences(id)
161
+ )
162
+ `);
163
+
164
+ // ── Escalations ──
165
+ db.exec(`
166
+ CREATE TABLE IF NOT EXISTS escalations (
167
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
168
+ escalation_id TEXT UNIQUE NOT NULL,
169
+ phone TEXT NOT NULL,
170
+ trigger_message TEXT DEFAULT '',
171
+ context_sent TEXT DEFAULT '{}',
172
+ response_received TEXT DEFAULT '',
173
+ status TEXT DEFAULT 'pending',
174
+ strike_count INTEGER DEFAULT 0,
175
+ is_client_support INTEGER DEFAULT 0,
176
+ created_at DATETIME DEFAULT (datetime('now')),
177
+ responded_at DATETIME,
178
+ timeout_at DATETIME
179
+ )
180
+ `);
181
+
182
+ // ── Follow-Up Tracker ──
183
+ db.exec(`
184
+ CREATE TABLE IF NOT EXISTS follow_up_tracker (
185
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
186
+ phone TEXT NOT NULL UNIQUE,
187
+ channel TEXT DEFAULT 'imessage',
188
+ contact_name TEXT DEFAULT 'Unknown',
189
+ last_our_message TEXT DEFAULT '',
190
+ last_our_message_at DATETIME,
191
+ context_summary TEXT DEFAULT '',
192
+ follow_up_count INTEGER DEFAULT 0,
193
+ max_follow_ups INTEGER DEFAULT 3,
194
+ next_follow_up_at DATETIME,
195
+ status TEXT DEFAULT 'active',
196
+ last_follow_up_at DATETIME,
197
+ last_follow_up_topic TEXT DEFAULT '',
198
+ opted_out INTEGER DEFAULT 0,
199
+ created_at DATETIME DEFAULT (datetime('now')),
200
+ updated_at DATETIME DEFAULT (datetime('now'))
201
+ )
202
+ `);
203
+
204
+ // ── Scheduling Rules ──
205
+ db.exec(`
206
+ CREATE TABLE IF NOT EXISTS scheduling_rules (
207
+ category TEXT PRIMARY KEY,
208
+ rule_preset TEXT DEFAULT 'flexible',
209
+ custom_instructions TEXT DEFAULT '',
210
+ structured_overrides TEXT DEFAULT '{}',
211
+ updated_at DATETIME DEFAULT (datetime('now'))
212
+ )
213
+ `);
214
+
215
+ // ── Settings ──
216
+ db.exec(`
217
+ CREATE TABLE IF NOT EXISTS settings (
218
+ key TEXT PRIMARY KEY,
219
+ value TEXT NOT NULL
220
+ )
221
+ `);
222
+
223
+ // ── Indexes ──
224
+ const indexes = [
225
+ 'CREATE INDEX IF NOT EXISTS idx_msg_phone ON message_log(phone)',
226
+ 'CREATE INDEX IF NOT EXISTS idx_msg_created ON message_log(created_at)',
227
+ 'CREATE INDEX IF NOT EXISTS idx_msg_direction ON message_log(phone, direction)',
228
+ 'CREATE INDEX IF NOT EXISTS idx_scopes_phone ON contact_scopes(phone)',
229
+ 'CREATE INDEX IF NOT EXISTS idx_escalation_id ON escalations(escalation_id)',
230
+ 'CREATE INDEX IF NOT EXISTS idx_escalation_status ON escalations(status)',
231
+ 'CREATE INDEX IF NOT EXISTS idx_escalation_phone ON escalations(phone)',
232
+ 'CREATE INDEX IF NOT EXISTS idx_followup_status ON follow_up_tracker(status)',
233
+ 'CREATE INDEX IF NOT EXISTS idx_followup_next ON follow_up_tracker(next_follow_up_at)',
234
+ 'CREATE INDEX IF NOT EXISTS idx_faq_device ON faq_entries(device_id)',
235
+ 'CREATE INDEX IF NOT EXISTS idx_playbook_device ON playbook_sections(device_id)',
236
+ 'CREATE INDEX IF NOT EXISTS idx_playbook_type ON playbook_sections(section_type)',
237
+ 'CREATE INDEX IF NOT EXISTS idx_prefs_scope ON preferences(scope)',
238
+ 'CREATE INDEX IF NOT EXISTS idx_prefs_pattern ON preferences(question_pattern)',
239
+ 'CREATE INDEX IF NOT EXISTS idx_conv_state ON conversation_state(state)',
240
+ ];
241
+ for (const idx of indexes) {
242
+ try { db.exec(idx); } catch (e) { /* index may already exist */ }
243
+ }
244
+
245
+ // ── Default Settings ──
246
+ const defaults = {
247
+ followUpEnabled: 'true',
248
+ followUpStartHour: '8',
249
+ followUpEndHour: '18',
250
+ maxDailyAutoResponses: '50',
251
+ maxDailyFollowUps: '10',
252
+ debounceMs: '3000',
253
+ escalationNotifyPhone: '+15099794110',
254
+ sonnetModel: 'claude-sonnet-4-20250514',
255
+ awayMessage: '',
256
+ masterPhone: '+15099794110',
257
+ defaultResponseMode: 'block',
258
+ imessageDefaultMode: '',
259
+ whatsappDefaultMode: '',
260
+ v2DryRun: 'false',
261
+ calendarAccountId: '',
262
+ };
263
+ const setSetting = db.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)');
264
+ for (const [key, value] of Object.entries(defaults)) {
265
+ setSetting.run(key, value);
266
+ }
267
+
268
+ // ── Default Scheduling Rules ──
269
+ const setRule = db.prepare('INSERT OR IGNORE INTO scheduling_rules (category, rule_preset) VALUES (?, ?)');
270
+ setRule.run('family', 'flexible');
271
+ setRule.run('friend', 'flexible');
272
+ setRule.run('team', 'work-hours');
273
+ setRule.run('client', 'professional');
274
+ setRule.run('lead', 'professional');
275
+ setRule.run('unknown', 'gatekeeper');
276
+ }
277
+
278
+ function prepareStatements() {
279
+ stmts = {
280
+ // ── Contacts ──
281
+ getContact: db.prepare('SELECT * FROM contacts WHERE phone = ?'),
282
+ getAllContacts: db.prepare('SELECT * FROM contacts ORDER BY name'),
283
+ searchContacts: db.prepare("SELECT * FROM contacts WHERE name LIKE ? OR phone LIKE ? ORDER BY name"),
284
+ filterByCategory: db.prepare('SELECT * FROM contacts WHERE category = ? ORDER BY name'),
285
+ upsertContact: db.prepare(`
286
+ INSERT INTO contacts (phone, name, category, response_mode, style, instructions, source, introduced, qualification_score, pipeline_stage, updated_at)
287
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
288
+ ON CONFLICT(phone) DO UPDATE SET
289
+ name=excluded.name, category=excluded.category, response_mode=excluded.response_mode,
290
+ style=excluded.style, instructions=excluded.instructions, source=excluded.source,
291
+ introduced=excluded.introduced, qualification_score=excluded.qualification_score,
292
+ pipeline_stage=excluded.pipeline_stage, updated_at=datetime('now')
293
+ `),
294
+ updateContactField: db.prepare("UPDATE contacts SET updated_at = datetime('now') WHERE phone = ?"),
295
+ deleteContact: db.prepare('DELETE FROM contacts WHERE phone = ?'),
296
+ markIntroduced: db.prepare("UPDATE contacts SET introduced = 1, updated_at = datetime('now') WHERE phone = ?"),
297
+ updateQualification: db.prepare("UPDATE contacts SET qualification_score = ?, pipeline_stage = ?, updated_at = datetime('now') WHERE phone = ?"),
298
+ updateCategory: db.prepare("UPDATE contacts SET category = ?, updated_at = datetime('now') WHERE phone = ?"),
299
+
300
+ // ── Contact Context ──
301
+ getContext: db.prepare('SELECT * FROM contact_context WHERE phone = ?'),
302
+ upsertContext: db.prepare(`
303
+ INSERT INTO contact_context (phone, relationship, last_topic, pending_items, conversation_summary, preferences, last_interaction)
304
+ VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
305
+ ON CONFLICT(phone) DO UPDATE SET
306
+ relationship=excluded.relationship, last_topic=excluded.last_topic,
307
+ pending_items=excluded.pending_items, conversation_summary=excluded.conversation_summary,
308
+ preferences=excluded.preferences, last_interaction=datetime('now')
309
+ `),
310
+ deleteContext: db.prepare('DELETE FROM contact_context WHERE phone = ?'),
311
+
312
+ // ── Contact Scopes ──
313
+ getScope: db.prepare('SELECT * FROM contact_scopes WHERE phone = ? AND scope = ?'),
314
+ getAllScopes: db.prepare('SELECT * FROM contact_scopes WHERE phone = ?'),
315
+ upsertScope: db.prepare(`
316
+ INSERT INTO contact_scopes (phone, scope, granted, granted_by, granted_at)
317
+ VALUES (?, ?, ?, ?, datetime('now'))
318
+ ON CONFLICT(phone, scope) DO UPDATE SET granted=excluded.granted, granted_by=excluded.granted_by, granted_at=datetime('now')
319
+ `),
320
+ deleteScope: db.prepare('DELETE FROM contact_scopes WHERE phone = ? AND scope = ?'),
321
+ deleteScopesByPhone: db.prepare('DELETE FROM contact_scopes WHERE phone = ?'),
322
+
323
+ // ── Conversation State ──
324
+ getState: db.prepare('SELECT * FROM conversation_state WHERE phone = ?'),
325
+ upsertState: db.prepare(`
326
+ INSERT INTO conversation_state (phone, state, state_data, entered_at, expires_at)
327
+ VALUES (?, ?, ?, datetime('now'), ?)
328
+ ON CONFLICT(phone) DO UPDATE SET state=excluded.state, state_data=excluded.state_data,
329
+ entered_at=datetime('now'), expires_at=excluded.expires_at
330
+ `),
331
+ clearExpiredStates: db.prepare("UPDATE conversation_state SET state = 'idle', state_data = '{}' WHERE expires_at IS NOT NULL AND expires_at < datetime('now')"),
332
+
333
+ // ── Message Log ──
334
+ insertMessage: db.prepare(`
335
+ INSERT INTO message_log (phone, channel, direction, text, attachments, sent_by, state_at_time)
336
+ VALUES (?, ?, ?, ?, ?, ?, ?)
337
+ `),
338
+ getRecentMessages: db.prepare('SELECT * FROM message_log WHERE phone = ? ORDER BY created_at DESC LIMIT ?'),
339
+ getRecentMessagesAll: db.prepare(`
340
+ SELECT m.*, COALESCE(c.name, m.phone) as contact_name
341
+ FROM message_log m LEFT JOIN contacts c ON c.phone = m.phone
342
+ ORDER BY m.created_at DESC LIMIT ?
343
+ `),
344
+ getLastOutbound: db.prepare("SELECT * FROM message_log WHERE phone = ? AND direction = 'outbound' ORDER BY created_at DESC LIMIT 1"),
345
+ getLastInbound: db.prepare("SELECT * FROM message_log WHERE phone = ? AND direction = 'inbound' ORDER BY created_at DESC LIMIT 1"),
346
+ getLastSentBy: db.prepare("SELECT sent_by FROM message_log WHERE phone = ? AND direction = 'outbound' AND sent_by != '' ORDER BY created_at DESC LIMIT 1"),
347
+ countRecentOutbound: db.prepare("SELECT COUNT(*) as count FROM message_log WHERE phone = ? AND direction = 'outbound' AND sent_by = 'aiva' AND created_at >= datetime('now', '-' || ? || ' hours')"),
348
+ countRecentFollowUps: db.prepare("SELECT COUNT(*) as count FROM message_log WHERE phone = ? AND direction = 'outbound' AND state_at_time = 'follow-up' AND created_at >= datetime('now', '-24 hours')"),
349
+
350
+ // ── FAQ ──
351
+ getAllFaq: db.prepare('SELECT * FROM faq_entries WHERE enabled = 1 ORDER BY category, id'),
352
+ getFaqByDevice: db.prepare('SELECT * FROM faq_entries WHERE (device_id = ? OR device_id IS NULL) AND enabled = 1 ORDER BY category, id'),
353
+ insertFaq: db.prepare('INSERT INTO faq_entries (device_id, question, answer, category, keywords) VALUES (?, ?, ?, ?, ?)'),
354
+ updateFaq: db.prepare('UPDATE faq_entries SET question=?, answer=?, category=?, keywords=?, enabled=? WHERE id=?'),
355
+ deleteFaq: db.prepare('DELETE FROM faq_entries WHERE id = ?'),
356
+ getFaqById: db.prepare('SELECT * FROM faq_entries WHERE id = ?'),
357
+
358
+ // ── Playbook ──
359
+ getPlaybookSections: db.prepare('SELECT * FROM playbook_sections WHERE (device_id = ? OR device_id IS NULL) AND enabled = 1 ORDER BY priority, id'),
360
+ getPlaybookByType: db.prepare('SELECT * FROM playbook_sections WHERE section_type = ? AND (device_id = ? OR device_id IS NULL) AND enabled = 1 ORDER BY priority'),
361
+ insertPlaybook: db.prepare("INSERT INTO playbook_sections (device_id, section_type, title, content, priority) VALUES (?, ?, ?, ?, ?)"),
362
+ updatePlaybook: db.prepare("UPDATE playbook_sections SET section_type=?, title=?, content=?, priority=?, enabled=?, version=version+1, updated_at=datetime('now') WHERE id=?"),
363
+ deletePlaybook: db.prepare('DELETE FROM playbook_sections WHERE id = ?'),
364
+ getPlaybookById: db.prepare('SELECT * FROM playbook_sections WHERE id = ?'),
365
+
366
+ // ── Preferences ──
367
+ getAllPreferences: db.prepare('SELECT * FROM preferences ORDER BY hit_count DESC, confidence DESC'),
368
+ getPreferencesByScope: db.prepare('SELECT * FROM preferences WHERE (scope = ? OR scope = ?) AND confidence >= 0.5 ORDER BY confidence DESC, hit_count DESC LIMIT ?'),
369
+ insertPreference: db.prepare(`
370
+ INSERT INTO preferences (scope, question_pattern, answer, source, confidence)
371
+ VALUES (?, ?, ?, ?, ?)
372
+ `),
373
+ updatePreference: db.prepare("UPDATE preferences SET answer=?, source=?, confidence=?, hit_count=hit_count+1, last_used_at=datetime('now'), updated_at=datetime('now') WHERE id=?"),
374
+ bumpPreferenceHit: db.prepare("UPDATE preferences SET hit_count=hit_count+1, last_used_at=datetime('now') WHERE id=?"),
375
+ deletePreference: db.prepare('DELETE FROM preferences WHERE id = ?'),
376
+ decayPreferences: db.prepare("UPDATE preferences SET confidence = MAX(0.0, confidence - ?) WHERE source = ? AND updated_at < datetime('now', '-30 days')"),
377
+
378
+ // ── Preference Log ──
379
+ insertPreferenceLog: db.prepare('INSERT INTO preference_log (preference_id, escalation_id, phone, original_question, tier_resolved) VALUES (?, ?, ?, ?, ?)'),
380
+
381
+ // ── Escalations ──
382
+ insertEscalation: db.prepare(`
383
+ INSERT INTO escalations (escalation_id, phone, trigger_message, context_sent, status, is_client_support, timeout_at)
384
+ VALUES (?, ?, ?, ?, 'pending', ?, ?)
385
+ `),
386
+ getEscalation: db.prepare('SELECT * FROM escalations WHERE escalation_id = ?'),
387
+ getActiveEscalation: db.prepare("SELECT * FROM escalations WHERE phone = ? AND status = 'pending' ORDER BY created_at DESC LIMIT 1"),
388
+ resolveEscalation: db.prepare("UPDATE escalations SET status = 'responded', response_received = ?, responded_at = datetime('now') WHERE escalation_id = ?"),
389
+ timeoutEscalation: db.prepare("UPDATE escalations SET status = 'timeout' WHERE escalation_id = ?"),
390
+ failEscalation: db.prepare("UPDATE escalations SET status = 'failed' WHERE escalation_id = ?"),
391
+ incrementStrike: db.prepare('UPDATE escalations SET strike_count = strike_count + 1 WHERE escalation_id = ?'),
392
+ getTimedOutEscalations: db.prepare("SELECT * FROM escalations WHERE status = 'pending' AND timeout_at < datetime('now')"),
393
+
394
+ // ── Follow-Up Tracker ──
395
+ getActiveFollowUps: db.prepare("SELECT * FROM follow_up_tracker WHERE status = 'active' AND next_follow_up_at <= datetime('now') AND opted_out = 0"),
396
+ getAllFollowUps: db.prepare("SELECT * FROM follow_up_tracker ORDER BY CASE status WHEN 'active' THEN 0 WHEN 'cold' THEN 1 WHEN 'paused' THEN 2 ELSE 3 END, next_follow_up_at ASC"),
397
+ getFollowUpByPhone: db.prepare('SELECT * FROM follow_up_tracker WHERE phone = ?'),
398
+ upsertFollowUp: db.prepare(`
399
+ INSERT INTO follow_up_tracker (phone, channel, contact_name, last_our_message, last_our_message_at, status, follow_up_count, next_follow_up_at, updated_at)
400
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, datetime('now'))
401
+ ON CONFLICT(phone) DO UPDATE SET
402
+ channel=excluded.channel, contact_name=excluded.contact_name,
403
+ last_our_message=excluded.last_our_message, last_our_message_at=excluded.last_our_message_at,
404
+ status=excluded.status, follow_up_count=0, next_follow_up_at=excluded.next_follow_up_at,
405
+ updated_at=datetime('now')
406
+ `),
407
+ updateFollowUpStatus: db.prepare("UPDATE follow_up_tracker SET status = ?, updated_at = datetime('now') WHERE phone = ?"),
408
+ incrementFollowUp: db.prepare("UPDATE follow_up_tracker SET follow_up_count = follow_up_count + 1, last_follow_up_at = datetime('now'), next_follow_up_at = ?, updated_at = datetime('now') WHERE phone = ?"),
409
+ markFollowUpCold: db.prepare("UPDATE follow_up_tracker SET status = 'cold', updated_at = datetime('now') WHERE phone = ?"),
410
+ updateFollowUpOptOut: db.prepare("UPDATE follow_up_tracker SET opted_out = ?, updated_at = datetime('now') WHERE phone = ?"),
411
+ updateFollowUpTopic: db.prepare("UPDATE follow_up_tracker SET last_follow_up_topic = ?, updated_at = datetime('now') WHERE phone = ?"),
412
+ deleteFollowUp: db.prepare('DELETE FROM follow_up_tracker WHERE phone = ?'),
413
+
414
+ // ── Scheduling Rules ──
415
+ getSchedulingRule: db.prepare('SELECT * FROM scheduling_rules WHERE category = ?'),
416
+ upsertSchedulingRule: db.prepare("INSERT OR REPLACE INTO scheduling_rules (category, rule_preset, custom_instructions, structured_overrides, updated_at) VALUES (?, ?, ?, ?, datetime('now'))"),
417
+ getAllSchedulingRules: db.prepare('SELECT * FROM scheduling_rules'),
418
+
419
+ // ── Settings ──
420
+ getSetting: db.prepare('SELECT value FROM settings WHERE key = ?'),
421
+ setSetting: db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)'),
422
+ getAllSettings: db.prepare('SELECT * FROM settings'),
423
+ };
424
+ }
425
+
426
+ /**
427
+ * Get a setting value by key.
428
+ * @param {string} key
429
+ * @returns {string}
430
+ */
431
+ function getSetting(key) {
432
+ const row = stmts.getSetting.get(key);
433
+ return row ? row.value : '';
434
+ }
435
+
436
+ /**
437
+ * Set a setting value.
438
+ * @param {string} key
439
+ * @param {string} value
440
+ */
441
+ function setSetting(key, value) {
442
+ stmts.setSetting.run(key, String(value));
443
+ }
444
+
445
+ /**
446
+ * Get the raw database instance (for advanced queries).
447
+ * @returns {import('better-sqlite3').Database}
448
+ */
449
+ function getDb() { return db; }
450
+
451
+ /**
452
+ * Get all prepared statements.
453
+ * @returns {Object}
454
+ */
455
+ function getStmts() { return stmts; }
456
+
457
+ module.exports = { initDatabase, getSetting, setSetting, getDb, getStmts };