@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/router-db.js
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const DB_PATH = path.join(process.env.HOME, '.openclaw', 'workspace', 'message-router', 'router.db');
|
|
5
|
+
const db = new DatabaseSync(DB_PATH);
|
|
6
|
+
|
|
7
|
+
db.exec(`PRAGMA journal_mode = WAL`);
|
|
8
|
+
db.exec(`PRAGMA foreign_keys = ON`);
|
|
9
|
+
|
|
10
|
+
db.exec(`
|
|
11
|
+
CREATE TABLE IF NOT EXISTS contact_rules (
|
|
12
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13
|
+
phone TEXT UNIQUE NOT NULL,
|
|
14
|
+
name TEXT NOT NULL DEFAULT 'Unknown',
|
|
15
|
+
category TEXT NOT NULL DEFAULT 'unknown',
|
|
16
|
+
response_mode TEXT NOT NULL DEFAULT 'escalate',
|
|
17
|
+
style TEXT NOT NULL DEFAULT 'casual',
|
|
18
|
+
context TEXT DEFAULT '',
|
|
19
|
+
instructions TEXT DEFAULT '',
|
|
20
|
+
priority TEXT NOT NULL DEFAULT 'normal',
|
|
21
|
+
created_at DATETIME DEFAULT (datetime('now')),
|
|
22
|
+
updated_at DATETIME DEFAULT (datetime('now'))
|
|
23
|
+
)
|
|
24
|
+
`);
|
|
25
|
+
|
|
26
|
+
db.exec(`
|
|
27
|
+
CREATE TABLE IF NOT EXISTS message_log (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
phone TEXT NOT NULL,
|
|
30
|
+
direction TEXT NOT NULL DEFAULT 'inbound',
|
|
31
|
+
message_preview TEXT DEFAULT '',
|
|
32
|
+
rules_applied TEXT DEFAULT '{}',
|
|
33
|
+
forwarded_to TEXT DEFAULT '',
|
|
34
|
+
timestamp DATETIME DEFAULT (datetime('now'))
|
|
35
|
+
)
|
|
36
|
+
`);
|
|
37
|
+
|
|
38
|
+
db.exec(`
|
|
39
|
+
CREATE TABLE IF NOT EXISTS contact_context (
|
|
40
|
+
phone TEXT PRIMARY KEY,
|
|
41
|
+
name TEXT,
|
|
42
|
+
relationship TEXT DEFAULT '',
|
|
43
|
+
last_topic TEXT DEFAULT '',
|
|
44
|
+
pending_items TEXT DEFAULT '[]',
|
|
45
|
+
preferences_learned TEXT DEFAULT '{}',
|
|
46
|
+
conversation_summary TEXT DEFAULT '',
|
|
47
|
+
last_interaction TEXT,
|
|
48
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
49
|
+
)
|
|
50
|
+
`);
|
|
51
|
+
|
|
52
|
+
db.exec(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS pending_requests (
|
|
54
|
+
request_id TEXT PRIMARY KEY,
|
|
55
|
+
phone TEXT NOT NULL,
|
|
56
|
+
type TEXT NOT NULL,
|
|
57
|
+
conversation_context TEXT DEFAULT '',
|
|
58
|
+
status TEXT NOT NULL DEFAULT 'waiting',
|
|
59
|
+
request_data TEXT DEFAULT '{}',
|
|
60
|
+
response_data TEXT DEFAULT '{}',
|
|
61
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
62
|
+
resolved_at TEXT
|
|
63
|
+
)
|
|
64
|
+
`);
|
|
65
|
+
|
|
66
|
+
db.exec(`
|
|
67
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
68
|
+
key TEXT PRIMARY KEY,
|
|
69
|
+
value TEXT NOT NULL
|
|
70
|
+
)
|
|
71
|
+
`);
|
|
72
|
+
|
|
73
|
+
db.exec(`
|
|
74
|
+
CREATE TABLE IF NOT EXISTS escalation_log (
|
|
75
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
76
|
+
phone TEXT NOT NULL,
|
|
77
|
+
type TEXT NOT NULL,
|
|
78
|
+
details_hash TEXT NOT NULL,
|
|
79
|
+
details_text TEXT DEFAULT '',
|
|
80
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
81
|
+
)
|
|
82
|
+
`);
|
|
83
|
+
db.exec(`
|
|
84
|
+
CREATE TABLE IF NOT EXISTS monitoring_sessions (
|
|
85
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
86
|
+
active INTEGER NOT NULL DEFAULT 0,
|
|
87
|
+
reason TEXT DEFAULT '',
|
|
88
|
+
auto INTEGER NOT NULL DEFAULT 0,
|
|
89
|
+
started_at TEXT DEFAULT (datetime('now')),
|
|
90
|
+
ended_at TEXT
|
|
91
|
+
)
|
|
92
|
+
`);
|
|
93
|
+
try { db.exec(`ALTER TABLE monitoring_sessions ADD COLUMN auto INTEGER NOT NULL DEFAULT 0`); } catch(e) {}
|
|
94
|
+
|
|
95
|
+
db.exec(`
|
|
96
|
+
CREATE TABLE IF NOT EXISTS monitoring_introductions (
|
|
97
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
98
|
+
session_id INTEGER NOT NULL,
|
|
99
|
+
phone TEXT NOT NULL,
|
|
100
|
+
introduced_at TEXT DEFAULT (datetime('now'))
|
|
101
|
+
)
|
|
102
|
+
`);
|
|
103
|
+
try { db.exec(`CREATE INDEX idx_monitoring_intro_session_phone ON monitoring_introductions(session_id, phone)`); } catch(e) {}
|
|
104
|
+
|
|
105
|
+
db.exec(`
|
|
106
|
+
CREATE TABLE IF NOT EXISTS escalations (
|
|
107
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
108
|
+
code TEXT UNIQUE NOT NULL,
|
|
109
|
+
phone TEXT NOT NULL,
|
|
110
|
+
contact_name TEXT,
|
|
111
|
+
message_text TEXT,
|
|
112
|
+
created_at DATETIME DEFAULT (datetime('now')),
|
|
113
|
+
expires_at DATETIME,
|
|
114
|
+
status TEXT DEFAULT 'pending',
|
|
115
|
+
response_instructions TEXT,
|
|
116
|
+
ai_response TEXT,
|
|
117
|
+
responded_at DATETIME
|
|
118
|
+
)
|
|
119
|
+
`);
|
|
120
|
+
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_escalations_code ON escalations(code)`); } catch(e) {}
|
|
121
|
+
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_escalations_status ON escalations(status)`); } catch(e) {}
|
|
122
|
+
try { db.exec(`ALTER TABLE escalations ADD COLUMN token TEXT`); } catch(e) {}
|
|
123
|
+
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_escalations_token ON escalations(token)`); } catch(e) {}
|
|
124
|
+
|
|
125
|
+
try { db.exec(`CREATE INDEX idx_escalation_phone_type ON escalation_log(phone, type)`); } catch(e) {}
|
|
126
|
+
|
|
127
|
+
try { db.exec(`ALTER TABLE contact_rules ADD COLUMN aiva_introduced INTEGER DEFAULT 0`); } catch(e) {}
|
|
128
|
+
try { db.exec(`ALTER TABLE contact_rules ADD COLUMN source TEXT DEFAULT 'unknown'`); } catch(e) {}
|
|
129
|
+
try { db.exec(`ALTER TABLE message_log ADD COLUMN source TEXT DEFAULT ''`); } catch(e) {}
|
|
130
|
+
|
|
131
|
+
// One-time migration: tag existing contacts by source
|
|
132
|
+
try {
|
|
133
|
+
const unknownContacts = db.prepare("SELECT phone FROM contact_rules WHERE source IS NULL OR source = 'unknown' OR source = ''").all();
|
|
134
|
+
for (const c of unknownContacts) {
|
|
135
|
+
const waMsg = db.prepare("SELECT 1 FROM message_log WHERE phone = ? AND (rules_applied LIKE '%whatsapp%' OR source = 'whatsapp') LIMIT 1").get(c.phone);
|
|
136
|
+
if (waMsg) {
|
|
137
|
+
db.prepare("UPDATE contact_rules SET source = 'whatsapp' WHERE phone = ?").run(c.phone);
|
|
138
|
+
} else {
|
|
139
|
+
db.prepare("UPDATE contact_rules SET source = 'imessage' WHERE phone = ?").run(c.phone);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch(e) { console.log('Source migration error:', e.message); }
|
|
143
|
+
|
|
144
|
+
db.exec(`
|
|
145
|
+
CREATE TABLE IF NOT EXISTS scheduling_rules (
|
|
146
|
+
category TEXT PRIMARY KEY,
|
|
147
|
+
rule_preset TEXT DEFAULT 'flexible',
|
|
148
|
+
custom_instructions TEXT DEFAULT '',
|
|
149
|
+
structured_overrides TEXT DEFAULT '{}',
|
|
150
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
151
|
+
)
|
|
152
|
+
`);
|
|
153
|
+
// Migration: add structured_overrides column if missing
|
|
154
|
+
try { db.exec("ALTER TABLE scheduling_rules ADD COLUMN structured_overrides TEXT DEFAULT '{}'"); } catch(e) { /* already exists */ }
|
|
155
|
+
db.exec(`INSERT OR IGNORE INTO scheduling_rules (category, rule_preset) VALUES ('family', 'flexible')`);
|
|
156
|
+
db.exec(`INSERT OR IGNORE INTO scheduling_rules (category, rule_preset) VALUES ('team', 'work-hours')`);
|
|
157
|
+
db.exec(`INSERT OR IGNORE INTO scheduling_rules (category, rule_preset) VALUES ('client', 'professional')`);
|
|
158
|
+
db.exec(`INSERT OR IGNORE INTO scheduling_rules (category, rule_preset) VALUES ('unknown', 'gatekeeper')`);
|
|
159
|
+
db.exec(`INSERT OR IGNORE INTO scheduling_rules (category, rule_preset) VALUES ('owner', 'flexible')`);
|
|
160
|
+
|
|
161
|
+
db.exec(`
|
|
162
|
+
CREATE TABLE IF NOT EXISTS group_chats (
|
|
163
|
+
chat_guid TEXT PRIMARY KEY,
|
|
164
|
+
name TEXT DEFAULT '',
|
|
165
|
+
purpose TEXT DEFAULT '',
|
|
166
|
+
response_mode TEXT DEFAULT 'mentioned-only',
|
|
167
|
+
personality TEXT DEFAULT 'casual',
|
|
168
|
+
custom_instructions TEXT DEFAULT '',
|
|
169
|
+
enabled INTEGER DEFAULT 1,
|
|
170
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
171
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
172
|
+
)
|
|
173
|
+
`);
|
|
174
|
+
|
|
175
|
+
db.exec(`
|
|
176
|
+
CREATE TABLE IF NOT EXISTS group_chat_context (
|
|
177
|
+
chat_guid TEXT PRIMARY KEY,
|
|
178
|
+
last_topic TEXT DEFAULT '',
|
|
179
|
+
conversation_summary TEXT DEFAULT '',
|
|
180
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
181
|
+
)
|
|
182
|
+
`);
|
|
183
|
+
|
|
184
|
+
// Group participants tracking
|
|
185
|
+
db.exec(`
|
|
186
|
+
CREATE TABLE IF NOT EXISTS group_participants (
|
|
187
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
188
|
+
chat_guid TEXT NOT NULL,
|
|
189
|
+
phone TEXT NOT NULL,
|
|
190
|
+
display_name TEXT DEFAULT '',
|
|
191
|
+
first_seen TEXT DEFAULT (datetime('now')),
|
|
192
|
+
last_seen TEXT DEFAULT (datetime('now')),
|
|
193
|
+
UNIQUE(chat_guid, phone)
|
|
194
|
+
)
|
|
195
|
+
`);
|
|
196
|
+
try { db.exec(`CREATE INDEX idx_group_participants_chat ON group_participants(chat_guid)`); } catch(e) {}
|
|
197
|
+
try { db.exec(`CREATE INDEX idx_group_participants_phone ON group_participants(phone)`); } catch(e) {}
|
|
198
|
+
|
|
199
|
+
// Add columns to group_chats for escalation tracking
|
|
200
|
+
try { db.exec(`ALTER TABLE group_chats ADD COLUMN participants TEXT DEFAULT '[]'`); } catch(e) {}
|
|
201
|
+
try { db.exec(`ALTER TABLE group_chats ADD COLUMN detected_at TEXT DEFAULT (datetime('now'))`); } catch(e) {}
|
|
202
|
+
try { db.exec(`ALTER TABLE group_chats ADD COLUMN owner_notified INTEGER DEFAULT 0`); } catch(e) {}
|
|
203
|
+
try { db.exec(`ALTER TABLE group_chats ADD COLUMN escalation_code TEXT DEFAULT ''`); } catch(e) {}
|
|
204
|
+
try { db.exec(`ALTER TABLE group_chats ADD COLUMN nickname TEXT DEFAULT ''`); } catch(e) {}
|
|
205
|
+
try { db.exec(`ALTER TABLE group_chats ADD COLUMN context_notes TEXT DEFAULT ''`); } catch(e) {}
|
|
206
|
+
|
|
207
|
+
// ── Email Router Tables ──────────────────────────────────
|
|
208
|
+
db.exec(`
|
|
209
|
+
CREATE TABLE IF NOT EXISTS email_rules (
|
|
210
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
211
|
+
email TEXT,
|
|
212
|
+
domain TEXT,
|
|
213
|
+
sender_name TEXT,
|
|
214
|
+
action TEXT NOT NULL DEFAULT 'surface',
|
|
215
|
+
instructions TEXT DEFAULT '',
|
|
216
|
+
auto_respond_template TEXT DEFAULT '',
|
|
217
|
+
auto_respond_mode TEXT DEFAULT 'once',
|
|
218
|
+
created_at DATETIME DEFAULT (datetime('now')),
|
|
219
|
+
updated_at DATETIME DEFAULT (datetime('now'))
|
|
220
|
+
)
|
|
221
|
+
`);
|
|
222
|
+
|
|
223
|
+
db.exec(`
|
|
224
|
+
CREATE TABLE IF NOT EXISTS email_drafts (
|
|
225
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
226
|
+
gmail_id TEXT,
|
|
227
|
+
account TEXT,
|
|
228
|
+
from_email TEXT,
|
|
229
|
+
from_name TEXT,
|
|
230
|
+
to_email TEXT,
|
|
231
|
+
subject TEXT,
|
|
232
|
+
body_text TEXT,
|
|
233
|
+
body_html TEXT,
|
|
234
|
+
ai_draft TEXT,
|
|
235
|
+
status TEXT DEFAULT 'pending',
|
|
236
|
+
action_taken TEXT,
|
|
237
|
+
rule_id INTEGER,
|
|
238
|
+
created_at DATETIME DEFAULT (datetime('now')),
|
|
239
|
+
sent_at DATETIME,
|
|
240
|
+
FOREIGN KEY (rule_id) REFERENCES email_rules(id)
|
|
241
|
+
)
|
|
242
|
+
`);
|
|
243
|
+
|
|
244
|
+
db.exec(`
|
|
245
|
+
CREATE TABLE IF NOT EXISTS email_processed (
|
|
246
|
+
gmail_id TEXT PRIMARY KEY,
|
|
247
|
+
account TEXT,
|
|
248
|
+
action_taken TEXT,
|
|
249
|
+
processed_at DATETIME DEFAULT (datetime('now'))
|
|
250
|
+
)
|
|
251
|
+
`);
|
|
252
|
+
|
|
253
|
+
try { db.exec(`CREATE INDEX idx_email_rules_email ON email_rules(email)`); } catch(e) {}
|
|
254
|
+
try { db.exec(`CREATE INDEX idx_email_rules_domain ON email_rules(domain)`); } catch(e) {}
|
|
255
|
+
try { db.exec(`CREATE INDEX idx_email_drafts_status ON email_drafts(status)`); } catch(e) {}
|
|
256
|
+
try { db.exec(`CREATE INDEX idx_email_drafts_gmail_id ON email_drafts(gmail_id)`); } catch(e) {}
|
|
257
|
+
|
|
258
|
+
try { db.exec(`CREATE INDEX idx_contact_phone ON contact_rules(phone)`); } catch(e) {}
|
|
259
|
+
try { db.exec(`CREATE INDEX idx_log_phone ON message_log(phone)`); } catch(e) {}
|
|
260
|
+
try { db.exec(`CREATE INDEX idx_log_timestamp ON message_log(timestamp)`); } catch(e) {}
|
|
261
|
+
try { db.exec(`ALTER TABLE message_log ADD COLUMN attachment_url TEXT DEFAULT ''`); } catch(e) {}
|
|
262
|
+
try { db.exec(`ALTER TABLE message_log ADD COLUMN sent_by TEXT DEFAULT ''`); } catch(e) {}
|
|
263
|
+
try { db.exec(`CREATE INDEX idx_pending_phone ON pending_requests(phone)`); } catch(e) {}
|
|
264
|
+
try { db.exec(`CREATE INDEX idx_pending_status ON pending_requests(status)`); } catch(e) {}
|
|
265
|
+
|
|
266
|
+
// ── Contact Permissions Tables ──────────────────────────────────
|
|
267
|
+
db.exec(`
|
|
268
|
+
CREATE TABLE IF NOT EXISTS contact_permissions (
|
|
269
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
270
|
+
phone TEXT NOT NULL,
|
|
271
|
+
permission_type TEXT NOT NULL,
|
|
272
|
+
allowed INTEGER NOT NULL DEFAULT 0,
|
|
273
|
+
granted_by TEXT DEFAULT 'system',
|
|
274
|
+
granted_at DATETIME DEFAULT (datetime('now')),
|
|
275
|
+
booking_category TEXT DEFAULT '',
|
|
276
|
+
booking_description TEXT DEFAULT '',
|
|
277
|
+
UNIQUE(phone, permission_type)
|
|
278
|
+
)
|
|
279
|
+
`);
|
|
280
|
+
|
|
281
|
+
db.exec(`
|
|
282
|
+
CREATE TABLE IF NOT EXISTS permission_requests (
|
|
283
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
284
|
+
phone TEXT NOT NULL,
|
|
285
|
+
contact_name TEXT DEFAULT 'Unknown',
|
|
286
|
+
permission_type TEXT NOT NULL,
|
|
287
|
+
request_details TEXT DEFAULT '',
|
|
288
|
+
original_message TEXT DEFAULT '',
|
|
289
|
+
token TEXT UNIQUE NOT NULL,
|
|
290
|
+
status TEXT DEFAULT 'pending',
|
|
291
|
+
created_at DATETIME DEFAULT (datetime('now')),
|
|
292
|
+
resolved_at DATETIME
|
|
293
|
+
)
|
|
294
|
+
`);
|
|
295
|
+
|
|
296
|
+
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_permission_requests_token ON permission_requests(token)`); } catch(e) {}
|
|
297
|
+
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_permission_requests_status ON permission_requests(status)`); } catch(e) {}
|
|
298
|
+
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_contact_permissions_phone ON contact_permissions(phone)`); } catch(e) {}
|
|
299
|
+
|
|
300
|
+
// ── Contact Scopes Table (OAuth-style permission scopes) ──────────
|
|
301
|
+
db.exec(`
|
|
302
|
+
CREATE TABLE IF NOT EXISTS contact_scopes (
|
|
303
|
+
phone TEXT NOT NULL,
|
|
304
|
+
scope TEXT NOT NULL,
|
|
305
|
+
granted INTEGER DEFAULT 0,
|
|
306
|
+
granted_by TEXT DEFAULT 'manual',
|
|
307
|
+
granted_at TEXT,
|
|
308
|
+
PRIMARY KEY (phone, scope)
|
|
309
|
+
)
|
|
310
|
+
`);
|
|
311
|
+
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_contact_scopes_phone ON contact_scopes(phone)`); } catch(e) {}
|
|
312
|
+
|
|
313
|
+
// ── Migrate existing contact_permissions → contact_scopes (one-time) ──
|
|
314
|
+
const PERMISSION_TO_SCOPES_MAP = {
|
|
315
|
+
'scheduling': ['calendar.view', 'calendar.book'],
|
|
316
|
+
'reminders': ['reminders.create'],
|
|
317
|
+
'messaging': ['messages.send', 'messages.auto-reply'],
|
|
318
|
+
'task-creation': ['tasks.create'],
|
|
319
|
+
'info-lookup': ['info.business'],
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const migrated = db.prepare("SELECT value FROM settings WHERE key = 'scopes_migrated'").get();
|
|
324
|
+
if (!migrated) {
|
|
325
|
+
const existingPerms = db.prepare('SELECT * FROM contact_permissions WHERE allowed = 1').all();
|
|
326
|
+
const upsertScope = db.prepare(`INSERT INTO contact_scopes (phone, scope, granted, granted_by, granted_at) VALUES (?, ?, 1, 'migrated', datetime('now')) ON CONFLICT(phone, scope) DO NOTHING`);
|
|
327
|
+
for (const perm of existingPerms) {
|
|
328
|
+
const scopes = PERMISSION_TO_SCOPES_MAP[perm.permission_type] || [];
|
|
329
|
+
for (const scope of scopes) {
|
|
330
|
+
upsertScope.run(perm.phone, scope);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
db.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('scopes_migrated', '1')").run();
|
|
334
|
+
console.log(`[scopes-migration] Migrated ${existingPerms.length} permission records to scopes`);
|
|
335
|
+
}
|
|
336
|
+
} catch(e) { console.error('[scopes-migration] Error:', e.message); }
|
|
337
|
+
|
|
338
|
+
const stmts = {
|
|
339
|
+
getAllRules: db.prepare('SELECT * FROM contact_rules ORDER BY name'),
|
|
340
|
+
getRuleByPhone: db.prepare('SELECT * FROM contact_rules WHERE phone = ?'),
|
|
341
|
+
searchRules: db.prepare("SELECT * FROM contact_rules WHERE name LIKE ? OR phone LIKE ? ORDER BY name"),
|
|
342
|
+
filterByCategory: db.prepare('SELECT * FROM contact_rules WHERE category = ? ORDER BY name'),
|
|
343
|
+
upsertRule: db.prepare(`
|
|
344
|
+
INSERT INTO contact_rules (phone, name, category, response_mode, style, context, instructions, priority, updated_at)
|
|
345
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
346
|
+
ON CONFLICT(phone) DO UPDATE SET
|
|
347
|
+
name=excluded.name, category=excluded.category, response_mode=excluded.response_mode,
|
|
348
|
+
style=excluded.style, context=excluded.context, instructions=excluded.instructions,
|
|
349
|
+
priority=excluded.priority, updated_at=datetime('now')
|
|
350
|
+
`),
|
|
351
|
+
deleteRule: db.prepare('DELETE FROM contact_rules WHERE phone = ?'),
|
|
352
|
+
updateContactCategory: db.prepare('UPDATE contact_rules SET category = ? WHERE phone = ?'),
|
|
353
|
+
insertLog: db.prepare(`
|
|
354
|
+
INSERT INTO message_log (phone, direction, message_preview, rules_applied, forwarded_to, source, sent_by)
|
|
355
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
356
|
+
`),
|
|
357
|
+
insertLogWithAttachment: db.prepare(`
|
|
358
|
+
INSERT INTO message_log (phone, direction, message_preview, rules_applied, forwarded_to, attachment_url, source, sent_by)
|
|
359
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
360
|
+
`),
|
|
361
|
+
getRecentLogs: db.prepare(`SELECT m.*, COALESCE(NULLIF(cr.name, ''), NULLIF(cc.name, ''), m.phone) AS contact_name FROM message_log m LEFT JOIN contact_rules cr ON cr.phone = m.phone LEFT JOIN contact_context cc ON cc.phone = m.phone ORDER BY m.timestamp DESC LIMIT ?`),
|
|
362
|
+
getLogsByPhone: db.prepare(`SELECT m.*, COALESCE(NULLIF(cr.name, ''), NULLIF(cc.name, ''), m.phone) AS contact_name FROM message_log m LEFT JOIN contact_rules cr ON cr.phone = m.phone LEFT JOIN contact_context cc ON cc.phone = m.phone WHERE m.phone = ? ORDER BY m.timestamp DESC LIMIT ?`),
|
|
363
|
+
countLogsByPhone: db.prepare('SELECT COUNT(*) as count FROM message_log WHERE phone = ?'),
|
|
364
|
+
countRules: db.prepare('SELECT COUNT(*) as c FROM contact_rules'),
|
|
365
|
+
insertOrIgnoreRule: db.prepare(`
|
|
366
|
+
INSERT OR IGNORE INTO contact_rules (phone, name, category, response_mode, style, context, instructions, priority)
|
|
367
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
368
|
+
`),
|
|
369
|
+
|
|
370
|
+
getContext: db.prepare('SELECT * FROM contact_context WHERE phone = ?'),
|
|
371
|
+
getAllContexts: db.prepare('SELECT * FROM contact_context ORDER BY last_interaction DESC'),
|
|
372
|
+
upsertContext: db.prepare(`
|
|
373
|
+
INSERT INTO contact_context (phone, name, relationship, last_topic, pending_items, preferences_learned, conversation_summary, last_interaction, updated_at)
|
|
374
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
375
|
+
ON CONFLICT(phone) DO UPDATE SET
|
|
376
|
+
name=excluded.name, relationship=excluded.relationship, last_topic=excluded.last_topic,
|
|
377
|
+
pending_items=excluded.pending_items, preferences_learned=excluded.preferences_learned,
|
|
378
|
+
conversation_summary=excluded.conversation_summary, last_interaction=datetime('now'), updated_at=datetime('now')
|
|
379
|
+
`),
|
|
380
|
+
deleteContext: db.prepare('DELETE FROM contact_context WHERE phone = ?'),
|
|
381
|
+
|
|
382
|
+
insertPending: db.prepare(`
|
|
383
|
+
INSERT INTO pending_requests (request_id, phone, type, conversation_context, status, request_data)
|
|
384
|
+
VALUES (?, ?, ?, ?, 'waiting', ?)
|
|
385
|
+
`),
|
|
386
|
+
getPending: db.prepare('SELECT * FROM pending_requests WHERE request_id = ?'),
|
|
387
|
+
getPendingByPhone: db.prepare("SELECT * FROM pending_requests WHERE phone = ? AND status = 'waiting' ORDER BY created_at DESC"),
|
|
388
|
+
resolvePending: db.prepare(`
|
|
389
|
+
UPDATE pending_requests SET status = 'resolved', response_data = ?, resolved_at = datetime('now') WHERE request_id = ?
|
|
390
|
+
`),
|
|
391
|
+
getRecentPending: db.prepare('SELECT * FROM pending_requests ORDER BY created_at DESC LIMIT ?'),
|
|
392
|
+
|
|
393
|
+
checkEscalation: db.prepare('SELECT id FROM escalation_log WHERE phone = ? AND type = ? AND details_hash = ?'),
|
|
394
|
+
insertEscalation: db.prepare(`INSERT INTO escalation_log (phone, type, details_hash, details_text) VALUES (?, ?, ?, ?)`),
|
|
395
|
+
getEscalationsByPhone: db.prepare('SELECT * FROM escalation_log WHERE phone = ? ORDER BY created_at DESC LIMIT ?'),
|
|
396
|
+
clearEscalations: db.prepare('DELETE FROM escalation_log WHERE phone = ?'),
|
|
397
|
+
|
|
398
|
+
getActiveMonitoring: db.prepare('SELECT * FROM monitoring_sessions WHERE active = 1 ORDER BY id DESC LIMIT 1'),
|
|
399
|
+
insertMonitoringSession: db.prepare('INSERT INTO monitoring_sessions (active, reason, auto) VALUES (1, ?, 0)'),
|
|
400
|
+
insertAutoMonitoringSession: db.prepare('INSERT INTO monitoring_sessions (active, reason, auto) VALUES (1, ?, 1)'),
|
|
401
|
+
endMonitoringSession: db.prepare("UPDATE monitoring_sessions SET active = 0, ended_at = datetime('now') WHERE active = 1"),
|
|
402
|
+
endAutoMonitoringSession: db.prepare("UPDATE monitoring_sessions SET active = 0, ended_at = datetime('now') WHERE active = 1 AND auto = 1"),
|
|
403
|
+
getIntroduction: db.prepare('SELECT id FROM monitoring_introductions WHERE session_id = ? AND phone = ?'),
|
|
404
|
+
insertIntroduction: db.prepare('INSERT INTO monitoring_introductions (session_id, phone) VALUES (?, ?)'),
|
|
405
|
+
|
|
406
|
+
getLastOutboundByPhone: db.prepare(`SELECT timestamp FROM message_log WHERE phone = ? AND direction = 'outbound' ORDER BY timestamp DESC LIMIT 1`),
|
|
407
|
+
getLastSentBy: db.prepare(`SELECT sent_by FROM message_log WHERE phone = ? AND direction = 'outbound' AND sent_by != '' ORDER BY timestamp DESC LIMIT 1`),
|
|
408
|
+
markIntroduced: db.prepare('UPDATE contact_rules SET aiva_introduced = 1 WHERE phone = ?'),
|
|
409
|
+
resetIntroduced: db.prepare('UPDATE contact_rules SET aiva_introduced = 0 WHERE phone = ?'),
|
|
410
|
+
|
|
411
|
+
getSchedulingRule: db.prepare('SELECT * FROM scheduling_rules WHERE category = ?'),
|
|
412
|
+
upsertSchedulingRule: db.prepare("INSERT OR REPLACE INTO scheduling_rules (category, rule_preset, custom_instructions, structured_overrides, updated_at) VALUES (?, ?, ?, ?, datetime('now'))"),
|
|
413
|
+
getAllSchedulingRules: db.prepare('SELECT * FROM scheduling_rules'),
|
|
414
|
+
|
|
415
|
+
getGroupChat: db.prepare('SELECT * FROM group_chats WHERE chat_guid = ?'),
|
|
416
|
+
upsertGroupChat: db.prepare("INSERT OR REPLACE INTO group_chats (chat_guid, name, purpose, response_mode, personality, custom_instructions, enabled, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))"),
|
|
417
|
+
getAllGroupChats: db.prepare('SELECT * FROM group_chats ORDER BY updated_at DESC'),
|
|
418
|
+
getGroupContext: db.prepare('SELECT * FROM group_chat_context WHERE chat_guid = ?'),
|
|
419
|
+
upsertGroupContext: db.prepare("INSERT OR REPLACE INTO group_chat_context (chat_guid, last_topic, conversation_summary, updated_at) VALUES (?, ?, ?, datetime('now'))"),
|
|
420
|
+
|
|
421
|
+
// Group participants
|
|
422
|
+
upsertGroupParticipant: db.prepare("INSERT INTO group_participants (chat_guid, phone, display_name) VALUES (?, ?, ?) ON CONFLICT(chat_guid, phone) DO UPDATE SET display_name = CASE WHEN excluded.display_name != '' THEN excluded.display_name ELSE display_name END, last_seen = datetime('now')"),
|
|
423
|
+
getGroupParticipants: db.prepare('SELECT * FROM group_participants WHERE chat_guid = ? ORDER BY last_seen DESC'),
|
|
424
|
+
getGroupByEscalationCode: db.prepare('SELECT * FROM group_chats WHERE escalation_code = ?'),
|
|
425
|
+
|
|
426
|
+
insertEscalationV2: db.prepare(`INSERT INTO escalations (code, phone, contact_name, message_text, expires_at, token) VALUES (?, ?, ?, ?, datetime('now', '+24 hours'), ?)`),
|
|
427
|
+
getEscalationByCode: db.prepare(`SELECT * FROM escalations WHERE code = ?`),
|
|
428
|
+
getEscalationByToken: db.prepare(`SELECT * FROM escalations WHERE token = ? AND status = 'pending' AND expires_at > datetime('now')`),
|
|
429
|
+
resolveEscalation: db.prepare(`UPDATE escalations SET status = 'responded', response_instructions = ?, ai_response = ?, responded_at = datetime('now') WHERE code = ?`),
|
|
430
|
+
getActiveEscalations: db.prepare(`SELECT * FROM escalations WHERE status = 'pending' AND expires_at > datetime('now')`),
|
|
431
|
+
expireOldEscalations: db.prepare(`UPDATE escalations SET status = 'expired' WHERE status = 'pending' AND expires_at < datetime('now')`),
|
|
432
|
+
|
|
433
|
+
// Email router statements
|
|
434
|
+
getAllEmailRules: db.prepare('SELECT * FROM email_rules ORDER BY id DESC'),
|
|
435
|
+
getEmailRuleByEmail: db.prepare('SELECT * FROM email_rules WHERE email = ?'),
|
|
436
|
+
getEmailRuleByDomain: db.prepare('SELECT * FROM email_rules WHERE domain = ?'),
|
|
437
|
+
insertEmailRule: db.prepare(`INSERT INTO email_rules (email, domain, sender_name, action, instructions, auto_respond_template, auto_respond_mode) VALUES (?, ?, ?, ?, ?, ?, ?)`),
|
|
438
|
+
updateEmailRule: db.prepare(`UPDATE email_rules SET email=?, domain=?, sender_name=?, action=?, instructions=?, auto_respond_template=?, auto_respond_mode=?, updated_at=datetime('now') WHERE id=?`),
|
|
439
|
+
deleteEmailRule: db.prepare('DELETE FROM email_rules WHERE id = ?'),
|
|
440
|
+
getEmailRuleById: db.prepare('SELECT * FROM email_rules WHERE id = ?'),
|
|
441
|
+
|
|
442
|
+
getAllEmailDrafts: db.prepare('SELECT * FROM email_drafts ORDER BY created_at DESC'),
|
|
443
|
+
getEmailDraftsByStatus: db.prepare('SELECT * FROM email_drafts WHERE status = ? ORDER BY created_at DESC LIMIT ?'),
|
|
444
|
+
getEmailDraftById: db.prepare('SELECT * FROM email_drafts WHERE id = ?'),
|
|
445
|
+
insertEmailDraft: db.prepare(`INSERT INTO email_drafts (gmail_id, account, from_email, from_name, to_email, subject, body_text, body_html, ai_draft, status, action_taken, rule_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
446
|
+
updateEmailDraftStatus: db.prepare(`UPDATE email_drafts SET status=?, action_taken=?, sent_at=CASE WHEN ?='sent' THEN datetime('now') ELSE sent_at END WHERE id=?`),
|
|
447
|
+
updateEmailDraftAiDraft: db.prepare(`UPDATE email_drafts SET ai_draft=? WHERE id=?`),
|
|
448
|
+
|
|
449
|
+
getEmailProcessed: db.prepare('SELECT * FROM email_processed WHERE gmail_id = ?'),
|
|
450
|
+
insertEmailProcessed: db.prepare(`INSERT OR IGNORE INTO email_processed (gmail_id, account, action_taken) VALUES (?, ?, ?)`),
|
|
451
|
+
|
|
452
|
+
// Permission statements
|
|
453
|
+
getPermission: db.prepare('SELECT * FROM contact_permissions WHERE phone = ? AND permission_type = ?'),
|
|
454
|
+
getAllPermissions: db.prepare('SELECT * FROM contact_permissions WHERE phone = ?'),
|
|
455
|
+
getAllPermissionsAll: db.prepare('SELECT * FROM contact_permissions ORDER BY phone, permission_type'),
|
|
456
|
+
upsertPermission: db.prepare(`INSERT INTO contact_permissions (phone, permission_type, allowed, granted_by, granted_at) VALUES (?, ?, ?, ?, datetime('now')) ON CONFLICT(phone, permission_type) DO UPDATE SET allowed=excluded.allowed, granted_by=excluded.granted_by, granted_at=datetime('now')`),
|
|
457
|
+
deletePermission: db.prepare('DELETE FROM contact_permissions WHERE phone = ? AND permission_type = ?'),
|
|
458
|
+
|
|
459
|
+
// Scope statements
|
|
460
|
+
getScope: db.prepare('SELECT * FROM contact_scopes WHERE phone = ? AND scope = ?'),
|
|
461
|
+
getAllScopes: db.prepare('SELECT * FROM contact_scopes WHERE phone = ?'),
|
|
462
|
+
getAllScopesAll: db.prepare('SELECT * FROM contact_scopes ORDER BY phone, scope'),
|
|
463
|
+
upsertScope: db.prepare(`INSERT INTO contact_scopes (phone, scope, granted, granted_by, granted_at) VALUES (?, ?, ?, ?, datetime('now')) ON CONFLICT(phone, scope) DO UPDATE SET granted=excluded.granted, granted_by=excluded.granted_by, granted_at=datetime('now')`),
|
|
464
|
+
deleteScope: db.prepare('DELETE FROM contact_scopes WHERE phone = ? AND scope = ?'),
|
|
465
|
+
deleteScopesByPhone: db.prepare('DELETE FROM contact_scopes WHERE phone = ?'),
|
|
466
|
+
insertPermissionRequest: db.prepare(`INSERT INTO permission_requests (phone, contact_name, permission_type, request_details, original_message, token) VALUES (?, ?, ?, ?, ?, ?)`),
|
|
467
|
+
getPermissionRequest: db.prepare('SELECT * FROM permission_requests WHERE token = ?'),
|
|
468
|
+
resolvePermissionRequest: db.prepare(`UPDATE permission_requests SET status = ?, resolved_at = datetime('now') WHERE token = ?`),
|
|
469
|
+
getPendingPermissionRequests: db.prepare("SELECT * FROM permission_requests WHERE status = 'pending' ORDER BY created_at DESC"),
|
|
470
|
+
|
|
471
|
+
updateContactSource: db.prepare("UPDATE contact_rules SET source = ?, updated_at = datetime('now') WHERE phone = ?"),
|
|
472
|
+
|
|
473
|
+
// Rate-limiting: count recent outbound messages to a phone
|
|
474
|
+
countRecentOutbound: db.prepare("SELECT COUNT(*) as count FROM message_log WHERE phone = ? AND direction = 'outbound' AND sent_by = 'aiva' AND timestamp >= datetime('now', '-' || ? || ' hours')"),
|
|
475
|
+
// Rate-limiting: get the last inbound message timestamp for a phone
|
|
476
|
+
getLastInboundByPhone: db.prepare("SELECT timestamp FROM message_log WHERE phone = ? AND direction = 'inbound' ORDER BY timestamp DESC LIMIT 1"),
|
|
477
|
+
getLastInboundWithPreview: db.prepare("SELECT timestamp, message_preview FROM message_log WHERE phone = ? AND direction = 'inbound' ORDER BY timestamp DESC LIMIT 1"),
|
|
478
|
+
// Rate-limiting: count follow-ups sent today for a phone
|
|
479
|
+
countRecentFollowUps: db.prepare("SELECT COUNT(*) as count FROM message_log WHERE phone = ? AND direction = 'outbound' AND rules_applied LIKE '%followUp%' AND timestamp >= datetime('now', '-24 hours')"),
|
|
480
|
+
|
|
481
|
+
getSetting: db.prepare('SELECT value FROM settings WHERE key = ?'),
|
|
482
|
+
setSetting: db.prepare(`
|
|
483
|
+
INSERT INTO settings (key, value) VALUES (?, ?)
|
|
484
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
485
|
+
`),
|
|
486
|
+
getAllSettings: db.prepare('SELECT * FROM settings'),
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// ── Character Profiles Tables ──────────────────────────────────
|
|
490
|
+
db.exec(`
|
|
491
|
+
CREATE TABLE IF NOT EXISTS characters (
|
|
492
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
493
|
+
name TEXT NOT NULL,
|
|
494
|
+
description TEXT DEFAULT '',
|
|
495
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
496
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
497
|
+
)
|
|
498
|
+
`);
|
|
499
|
+
|
|
500
|
+
db.exec(`
|
|
501
|
+
CREATE TABLE IF NOT EXISTS character_images (
|
|
502
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
503
|
+
character_id INTEGER NOT NULL,
|
|
504
|
+
filename TEXT NOT NULL,
|
|
505
|
+
original_name TEXT,
|
|
506
|
+
file_path TEXT NOT NULL,
|
|
507
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
508
|
+
FOREIGN KEY (character_id) REFERENCES characters(id) ON DELETE CASCADE
|
|
509
|
+
)
|
|
510
|
+
`);
|
|
511
|
+
|
|
512
|
+
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_character_images_char ON character_images(character_id)`); } catch(e) {}
|
|
513
|
+
|
|
514
|
+
// ── Follow-Up Tracker Table ──────────────────────────────
|
|
515
|
+
db.exec(`
|
|
516
|
+
CREATE TABLE IF NOT EXISTS follow_up_tracker (
|
|
517
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
518
|
+
phone TEXT NOT NULL,
|
|
519
|
+
channel TEXT DEFAULT 'imessage',
|
|
520
|
+
contact_name TEXT DEFAULT 'Unknown',
|
|
521
|
+
last_our_message TEXT DEFAULT '',
|
|
522
|
+
last_our_message_at DATETIME,
|
|
523
|
+
context_summary TEXT DEFAULT '',
|
|
524
|
+
follow_up_count INTEGER DEFAULT 0,
|
|
525
|
+
max_follow_ups INTEGER DEFAULT 3,
|
|
526
|
+
next_follow_up_at DATETIME,
|
|
527
|
+
status TEXT DEFAULT 'active',
|
|
528
|
+
last_follow_up_at DATETIME,
|
|
529
|
+
opted_out INTEGER DEFAULT 0,
|
|
530
|
+
created_at DATETIME DEFAULT (datetime('now')),
|
|
531
|
+
updated_at DATETIME DEFAULT (datetime('now')),
|
|
532
|
+
UNIQUE(phone)
|
|
533
|
+
)
|
|
534
|
+
`);
|
|
535
|
+
|
|
536
|
+
try { db.exec(`ALTER TABLE contact_context ADD COLUMN pending_action TEXT DEFAULT NULL`); } catch(e) {}
|
|
537
|
+
try { db.exec(`ALTER TABLE contact_context ADD COLUMN pending_action_set_at TEXT DEFAULT NULL`); } catch(e) {}
|
|
538
|
+
|
|
539
|
+
try { db.exec(`ALTER TABLE follow_up_tracker ADD COLUMN last_follow_up_topic TEXT DEFAULT ''`); } catch(e) {}
|
|
540
|
+
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_follow_up_status ON follow_up_tracker(status)`); } catch(e) {}
|
|
541
|
+
try { db.exec(`CREATE INDEX IF NOT EXISTS idx_follow_up_next ON follow_up_tracker(next_follow_up_at)`); } catch(e) {}
|
|
542
|
+
|
|
543
|
+
// Follow-up prepared statements
|
|
544
|
+
stmts.getActiveFollowUps = db.prepare("SELECT * FROM follow_up_tracker WHERE status = 'active' AND next_follow_up_at <= datetime('now') AND opted_out = 0");
|
|
545
|
+
stmts.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");
|
|
546
|
+
stmts.getFollowUpByPhone = db.prepare("SELECT * FROM follow_up_tracker WHERE phone = ?");
|
|
547
|
+
stmts.upsertFollowUp = db.prepare(`
|
|
548
|
+
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)
|
|
549
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, datetime(?, '+2 hours'), datetime('now'))
|
|
550
|
+
ON CONFLICT(phone) DO UPDATE SET
|
|
551
|
+
channel=excluded.channel, contact_name=excluded.contact_name,
|
|
552
|
+
last_our_message=excluded.last_our_message, last_our_message_at=excluded.last_our_message_at,
|
|
553
|
+
status=excluded.status, follow_up_count=0, next_follow_up_at=datetime(excluded.last_our_message_at, '+2 hours'),
|
|
554
|
+
updated_at=datetime('now')
|
|
555
|
+
`);
|
|
556
|
+
stmts.updateFollowUpStatus = db.prepare("UPDATE follow_up_tracker SET status = ?, updated_at = datetime('now') WHERE phone = ?");
|
|
557
|
+
stmts.incrementFollowUp = db.prepare(`
|
|
558
|
+
UPDATE follow_up_tracker SET
|
|
559
|
+
follow_up_count = follow_up_count + 1,
|
|
560
|
+
last_follow_up_at = datetime('now'),
|
|
561
|
+
next_follow_up_at = ?,
|
|
562
|
+
updated_at = datetime('now')
|
|
563
|
+
WHERE phone = ?
|
|
564
|
+
`);
|
|
565
|
+
stmts.deleteFollowUp = db.prepare("DELETE FROM follow_up_tracker WHERE phone = ?");
|
|
566
|
+
stmts.markFollowUpCold = db.prepare("UPDATE follow_up_tracker SET status = 'cold', updated_at = datetime('now') WHERE phone = ?");
|
|
567
|
+
stmts.updateFollowUpOptOut = db.prepare("UPDATE follow_up_tracker SET opted_out = ?, updated_at = datetime('now') WHERE phone = ?");
|
|
568
|
+
|
|
569
|
+
// Pending action support (for scheduling flow)
|
|
570
|
+
stmts.setPendingAction = db.prepare("UPDATE contact_context SET pending_action = ?, pending_action_set_at = datetime('now') WHERE phone = ?");
|
|
571
|
+
stmts.clearPendingAction = db.prepare("UPDATE contact_context SET pending_action = NULL, pending_action_set_at = NULL WHERE phone = ?");
|
|
572
|
+
stmts.getPendingAction = db.prepare("SELECT pending_action, pending_action_set_at FROM contact_context WHERE phone = ?");
|
|
573
|
+
|
|
574
|
+
// Follow-up settings defaults
|
|
575
|
+
db.exec(`INSERT OR IGNORE INTO settings (key, value) VALUES ('followUpEnabled', 'false')`);
|
|
576
|
+
db.exec(`INSERT OR IGNORE INTO settings (key, value) VALUES ('followUpStartHour', '8')`);
|
|
577
|
+
db.exec(`INSERT OR IGNORE INTO settings (key, value) VALUES ('followUpEndHour', '18')`);
|
|
578
|
+
db.exec(`INSERT OR IGNORE INTO settings (key, value) VALUES ('followUpMaxDefault', '3')`);
|
|
579
|
+
db.exec(`INSERT OR IGNORE INTO settings (key, value) VALUES ('followUpCheckIntervalMin', '30')`);
|
|
580
|
+
db.exec(`INSERT OR IGNORE INTO settings (key, value) VALUES ('maxDailyAutoResponses', '3')`);
|
|
581
|
+
db.exec(`INSERT OR IGNORE INTO settings (key, value) VALUES ('maxDailyFollowUps', '1')`);
|
|
582
|
+
|
|
583
|
+
// ── Agent Sessions (Direct-to-Agent) ──────────────────────
|
|
584
|
+
db.exec(`
|
|
585
|
+
CREATE TABLE IF NOT EXISTS agent_sessions (
|
|
586
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
587
|
+
phone TEXT NOT NULL UNIQUE,
|
|
588
|
+
session_key TEXT NOT NULL,
|
|
589
|
+
skills TEXT DEFAULT '[]',
|
|
590
|
+
tool_policies TEXT DEFAULT '{}',
|
|
591
|
+
instructions TEXT DEFAULT '',
|
|
592
|
+
status TEXT DEFAULT 'active',
|
|
593
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
594
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
595
|
+
)
|
|
596
|
+
`);
|
|
597
|
+
|
|
598
|
+
stmts.getAgentSession = db.prepare("SELECT * FROM agent_sessions WHERE phone = ? AND status = 'active'");
|
|
599
|
+
stmts.insertAgentSession = db.prepare(`INSERT INTO agent_sessions (phone, session_key, skills, tool_policies, instructions, status) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
600
|
+
stmts.updateAgentSession = db.prepare(`UPDATE agent_sessions SET skills = ?, tool_policies = ?, instructions = ?, status = ?, updated_at = CURRENT_TIMESTAMP WHERE phone = ?`);
|
|
601
|
+
stmts.deleteAgentSession = db.prepare("UPDATE agent_sessions SET status = 'disabled', updated_at = CURRENT_TIMESTAMP WHERE phone = ?");
|
|
602
|
+
stmts.listAgentSessions = db.prepare("SELECT * FROM agent_sessions ORDER BY created_at DESC");
|
|
603
|
+
|
|
604
|
+
module.exports = { db, stmts };
|
package/router-utils.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// ── Shared Router Utilities ──────────────────────────────
|
|
2
|
+
// Shared between router.js and follow-up-handler.js
|
|
3
|
+
|
|
4
|
+
const { stmts } = require('./router-db');
|
|
5
|
+
|
|
6
|
+
const REINTRO_PHRASES = [
|
|
7
|
+
"Hey, Aiva here — ",
|
|
8
|
+
"Hey, it's Aiva — ",
|
|
9
|
+
"Hey, Aiva here. ",
|
|
10
|
+
"It's Aiva — ",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function needsReintro(phone) {
|
|
14
|
+
try {
|
|
15
|
+
const row = stmts.getLastSentBy.get(phone);
|
|
16
|
+
return row && row.sent_by === 'owner';
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function addReintroIfNeeded(phone, message) {
|
|
23
|
+
if (!needsReintro(phone)) return message;
|
|
24
|
+
const phrase = REINTRO_PHRASES[Math.floor(Math.random() * REINTRO_PHRASES.length)];
|
|
25
|
+
return phrase + message;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { REINTRO_PHRASES, needsReintro, addReintroIfNeeded };
|