@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
@@ -0,0 +1,301 @@
1
+ // ── Scheduler - Calendar Booking / Reschedule / Cancel ───
2
+ // Invoked as a tool call from the Conversation Engine.
3
+ 'use strict';
4
+
5
+ const { getStmts, getSetting } = require('./db');
6
+
7
+ const CALENDAR_BASE = 'http://localhost:3847/api/integrations/google/calendars';
8
+
9
+ function log(msg, data) {
10
+ const ts = new Date().toISOString();
11
+ if (data) console.log(`[${ts}] [SCHEDULER] ${msg}`, JSON.stringify(data));
12
+ else console.log(`[${ts}] [SCHEDULER] ${msg}`);
13
+ }
14
+
15
+ /**
16
+ * Get scheduling rule for a contact category.
17
+ * @param {string} category
18
+ * @returns {Object}
19
+ */
20
+ function getSchedulingRule(category) {
21
+ const rule = getStmts().getSchedulingRule.get(category);
22
+ return rule || { category, rule_preset: 'flexible', custom_instructions: '', structured_overrides: '{}' };
23
+ }
24
+
25
+ /**
26
+ * Check calendar availability for a given time range.
27
+ * @param {string} timeMin - ISO datetime start
28
+ * @param {string} timeMax - ISO datetime end
29
+ * @param {string} [calendarId='primary'] - Calendar ID
30
+ * @param {string} [accountId] - Google account ID
31
+ * @returns {Promise<Array>} List of existing events (conflicts)
32
+ */
33
+ async function checkAvailability(timeMin, timeMax, calendarId = 'primary', accountId = null) {
34
+ try {
35
+ let url = `${CALENDAR_BASE}/${encodeURIComponent(calendarId)}/events?timeMin=${encodeURIComponent(timeMin)}&timeMax=${encodeURIComponent(timeMax)}`;
36
+ if (accountId) url += `&accountId=${accountId}`;
37
+
38
+ const resp = await fetch(url, { signal: AbortSignal.timeout(15000) });
39
+ if (!resp.ok) {
40
+ log('Calendar API error', { status: resp.status });
41
+ return { error: `calendar_api_${resp.status}`, events: null };
42
+ }
43
+
44
+ const data = await resp.json();
45
+ return { error: null, events: data.events || data.items || data || [] };
46
+ } catch (err) {
47
+ log('Calendar check failed', { error: err.message });
48
+ return { error: err.message, events: null };
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Book an appointment on the calendar.
54
+ * @param {Object} event
55
+ * @param {string} event.summary - Event title
56
+ * @param {string} event.startTime - ISO datetime
57
+ * @param {string} event.endTime - ISO datetime
58
+ * @param {string} [event.description] - Event description
59
+ * @param {string} [event.attendeeEmail] - Attendee email
60
+ * @param {string} [event.calendarId='primary']
61
+ * @param {string} [event.accountId]
62
+ * @returns {Promise<{success: boolean, event?: Object, error?: string}>}
63
+ */
64
+ async function bookAppointment(event) {
65
+ const calendarId = event.calendarId || 'primary';
66
+ const accountId = event.accountId || null;
67
+
68
+ // Check for conflicts first
69
+ const availCheck = await checkAvailability(event.startTime, event.endTime, calendarId, accountId);
70
+ if (availCheck.error) {
71
+ log('Cannot verify availability', { error: availCheck.error });
72
+ return { success: false, error: `Cannot verify calendar availability: ${availCheck.error}. Please try again.` };
73
+ }
74
+ if (availCheck.events.length > 0) {
75
+ const conflictNames = availCheck.events.map(c => c.summary || 'Busy').join(', ');
76
+ log('Booking conflict', { time: event.startTime, conflicts: conflictNames });
77
+ return {
78
+ success: false,
79
+ error: `Time slot has conflicts: ${conflictNames}`,
80
+ conflicts: availCheck.events,
81
+ };
82
+ }
83
+
84
+ try {
85
+ let url = `${CALENDAR_BASE}/${encodeURIComponent(calendarId)}/events`;
86
+ if (accountId) url += `?accountId=${accountId}`;
87
+
88
+ const body = {
89
+ summary: event.summary,
90
+ start: { dateTime: event.startTime },
91
+ end: { dateTime: event.endTime },
92
+ };
93
+ if (event.description) body.description = event.description;
94
+ if (event.attendeeEmail) body.attendees = [{ email: event.attendeeEmail }];
95
+
96
+ const resp = await fetch(url, {
97
+ method: 'POST',
98
+ headers: { 'Content-Type': 'application/json' },
99
+ body: JSON.stringify(body),
100
+ signal: AbortSignal.timeout(15000),
101
+ });
102
+
103
+ if (!resp.ok) {
104
+ const errText = await resp.text().catch(() => 'unknown');
105
+ log('Booking failed', { status: resp.status, error: errText.substring(0, 200) });
106
+ return { success: false, error: `Calendar API error: ${resp.status}` };
107
+ }
108
+
109
+ const created = await resp.json();
110
+ log('Booking success', { summary: event.summary, start: event.startTime });
111
+ return { success: true, event: created };
112
+ } catch (err) {
113
+ log('Booking error', { error: err.message });
114
+ return { success: false, error: err.message };
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Find available slots in a date range.
120
+ * @param {string} dateStr - Date string (e.g. "2026-02-27")
121
+ * @param {number} [durationMin=30] - Meeting duration in minutes
122
+ * @param {string} [calendarId='primary']
123
+ * @param {string} [accountId]
124
+ * @returns {Promise<Array<{start: string, end: string}>>}
125
+ */
126
+ async function findAvailableSlots(dateStr, durationMin = 30, calendarId = 'primary', accountId = null) {
127
+ // Use Intl to get the correct PST/PDT offset for the given date
128
+ const formatter = new Intl.DateTimeFormat('en-US', { timeZone: 'America/Los_Angeles', timeZoneName: 'shortOffset' });
129
+ const refDate = new Date(dateStr + 'T12:00:00Z');
130
+ const parts = formatter.formatToParts(refDate);
131
+ const tzPart = parts.find(p => p.type === 'timeZoneName')?.value || 'GMT-8';
132
+ const offsetMatch = tzPart.match(/GMT([+-]\d+)/);
133
+ const offsetHours = offsetMatch ? parseInt(offsetMatch[1]) : -8;
134
+ const offsetStr = `${offsetHours >= 0 ? '+' : ''}${String(Math.abs(offsetHours)).padStart(2, '0')}:00`;
135
+
136
+ const dayStart = new Date(`${dateStr}T09:00:00${offsetStr}`);
137
+ const dayEnd = new Date(`${dateStr}T17:00:00${offsetStr}`);
138
+
139
+ const availCheck = await checkAvailability(dayStart.toISOString(), dayEnd.toISOString(), calendarId, accountId);
140
+ if (availCheck.error) return [];
141
+ const events = availCheck.events;
142
+
143
+ // Build busy blocks
144
+ const busy = events.map(e => ({
145
+ start: new Date(e.start?.dateTime || e.start).getTime(),
146
+ end: new Date(e.end?.dateTime || e.end).getTime(),
147
+ })).sort((a, b) => a.start - b.start);
148
+
149
+ // Find gaps
150
+ const slots = [];
151
+ const durationMs = durationMin * 60000;
152
+ let cursor = dayStart.getTime();
153
+
154
+ for (const block of busy) {
155
+ if (block.start - cursor >= durationMs) {
156
+ slots.push({
157
+ start: new Date(cursor).toISOString(),
158
+ end: new Date(cursor + durationMs).toISOString(),
159
+ });
160
+ }
161
+ cursor = Math.max(cursor, block.end);
162
+ }
163
+
164
+ // Check remaining time after last event
165
+ if (dayEnd.getTime() - cursor >= durationMs) {
166
+ slots.push({
167
+ start: new Date(cursor).toISOString(),
168
+ end: new Date(cursor + durationMs).toISOString(),
169
+ });
170
+ }
171
+
172
+ return slots;
173
+ }
174
+
175
+ /**
176
+ * Reschedule an existing event.
177
+ * @param {string} eventId - Event ID to reschedule
178
+ * @param {string} newStart - New ISO start time
179
+ * @param {string} newEnd - New ISO end time
180
+ * @param {string} [calendarId='primary']
181
+ * @param {string} [accountId]
182
+ * @returns {Promise<{success: boolean, error?: string}>}
183
+ */
184
+ async function rescheduleAppointment(eventId, newStart, newEnd, calendarId = 'primary', accountId = null) {
185
+ // Check for conflicts at new time
186
+ const availCheck = await checkAvailability(newStart, newEnd, calendarId, accountId);
187
+ if (availCheck.error) {
188
+ return { success: false, error: `Cannot verify availability: ${availCheck.error}` };
189
+ }
190
+ if (availCheck.events.length > 0) {
191
+ return { success: false, error: 'New time slot has conflicts', conflicts: availCheck.events };
192
+ }
193
+
194
+ try {
195
+ let url = `${CALENDAR_BASE}/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}`;
196
+ if (accountId) url += `?accountId=${accountId}`;
197
+
198
+ const resp = await fetch(url, {
199
+ method: 'PUT',
200
+ headers: { 'Content-Type': 'application/json' },
201
+ body: JSON.stringify({
202
+ start: { dateTime: newStart },
203
+ end: { dateTime: newEnd },
204
+ }),
205
+ signal: AbortSignal.timeout(15000),
206
+ });
207
+
208
+ if (!resp.ok) return { success: false, error: `Calendar API error: ${resp.status}` };
209
+ log('Rescheduled', { eventId, newStart });
210
+ return { success: true };
211
+ } catch (err) {
212
+ return { success: false, error: err.message };
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Cancel an existing event.
218
+ * @param {string} eventId
219
+ * @param {string} [calendarId='primary']
220
+ * @param {string} [accountId]
221
+ * @returns {Promise<{success: boolean, error?: string}>}
222
+ */
223
+ async function cancelAppointment(eventId, calendarId = 'primary', accountId = null) {
224
+ try {
225
+ let url = `${CALENDAR_BASE}/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}`;
226
+ if (accountId) url += `?accountId=${accountId}`;
227
+
228
+ const resp = await fetch(url, {
229
+ method: 'DELETE',
230
+ signal: AbortSignal.timeout(15000),
231
+ });
232
+
233
+ if (!resp.ok) return { success: false, error: `Calendar API error: ${resp.status}` };
234
+ log('Cancelled', { eventId });
235
+ return { success: true };
236
+ } catch (err) {
237
+ return { success: false, error: err.message };
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Handle a schedule_appointment tool call from the Conversation Engine.
243
+ * @param {Object} args - Tool call arguments from Sonnet
244
+ * @param {Object} contact - Contact record
245
+ * @returns {Promise<Object>} Result to feed back into conversation
246
+ */
247
+ async function handleToolCall(args, contact) {
248
+ const action = args.action || 'book';
249
+ const accountId = args.accountId || getSetting('calendarAccountId') || null;
250
+ const calendarId = args.calendarId || 'primary';
251
+
252
+ switch (action) {
253
+ case 'check_availability': {
254
+ const slots = await findAvailableSlots(args.date, args.duration || 30, calendarId, accountId);
255
+ if (slots.length === 0) {
256
+ return { success: false, message: `No available ${args.duration || 30}-minute slots on ${args.date}.` };
257
+ }
258
+ return {
259
+ success: true,
260
+ message: `Found ${slots.length} available slot(s) on ${args.date}.`,
261
+ slots: slots.slice(0, 3),
262
+ };
263
+ }
264
+
265
+ case 'book': {
266
+ const result = await bookAppointment({
267
+ summary: args.summary || `Meeting with ${contact.name}`,
268
+ startTime: args.startTime,
269
+ endTime: args.endTime,
270
+ description: args.description || `Booked by AIVA for ${contact.name} (${contact.phone})`,
271
+ attendeeEmail: args.attendeeEmail || null,
272
+ calendarId,
273
+ accountId,
274
+ });
275
+ return result;
276
+ }
277
+
278
+ case 'reschedule': {
279
+ const result = await rescheduleAppointment(args.eventId, args.newStart, args.newEnd, calendarId, accountId);
280
+ return result;
281
+ }
282
+
283
+ case 'cancel': {
284
+ const result = await cancelAppointment(args.eventId, calendarId, accountId);
285
+ return result;
286
+ }
287
+
288
+ default:
289
+ return { success: false, error: `Unknown scheduling action: ${action}` };
290
+ }
291
+ }
292
+
293
+ module.exports = {
294
+ getSchedulingRule,
295
+ checkAvailability,
296
+ bookAppointment,
297
+ findAvailableSlots,
298
+ rescheduleAppointment,
299
+ cancelAppointment,
300
+ handleToolCall,
301
+ };
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ // ── Migrate v1 Database to v2 ────────────────────────────
3
+ // Reads from v1 (data/aiva.db or message-router/router.db), writes to v2.
4
+ // Safe to run multiple times (idempotent). Does NOT modify v1 database.
5
+ 'use strict';
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const Database = require('better-sqlite3');
10
+
11
+ const V1_PATHS = [
12
+ path.join(process.env.HOME, '.openclaw', 'workspace', 'message-router', 'router.db'),
13
+ path.join(__dirname, '..', '..', 'data', 'aiva.db'),
14
+ ];
15
+
16
+ const V2_DIR = path.join(__dirname, '..', 'data');
17
+ const V2_PATH = path.join(V2_DIR, 'router-v2.db');
18
+
19
+ function log(msg) {
20
+ console.log(`[MIGRATE] ${msg}`);
21
+ }
22
+
23
+ function findV1Db() {
24
+ for (const p of V1_PATHS) {
25
+ if (fs.existsSync(p)) {
26
+ log(`Found v1 DB at: ${p}`);
27
+ return p;
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+
33
+ function run() {
34
+ const v1Path = findV1Db();
35
+ if (!v1Path) {
36
+ log('No v1 database found. Nothing to migrate.');
37
+ return;
38
+ }
39
+
40
+ // Initialize v2 DB (this creates tables)
41
+ const { initDatabase } = require('../db');
42
+ const { db: v2 } = initDatabase();
43
+
44
+ const v1 = new Database(v1Path, { readonly: true });
45
+ v1.pragma('journal_mode = WAL');
46
+
47
+ let migrated = { contacts: 0, context: 0, scopes: 0, messages: 0, settings: 0, followUps: 0, schedulingRules: 0 };
48
+
49
+ // ── Migrate contacts (contact_rules -> contacts) ──
50
+ try {
51
+ const contacts = v1.prepare('SELECT * FROM contact_rules').all();
52
+ const upsert = v2.prepare(`
53
+ INSERT INTO contacts (phone, name, category, response_mode, style, instructions, source, introduced, qualification_score, pipeline_stage, created_at, updated_at)
54
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 'none', ?, ?)
55
+ ON CONFLICT(phone) DO NOTHING
56
+ `);
57
+
58
+ for (const c of contacts) {
59
+ if (!c.phone) continue;
60
+ upsert.run(
61
+ c.phone, c.name || 'Unknown', c.category || 'unknown',
62
+ c.response_mode || 'auto', c.style || 'casual',
63
+ c.instructions || '', c.source || 'unknown',
64
+ c.aiva_introduced || 0,
65
+ c.created_at || new Date().toISOString(),
66
+ c.updated_at || new Date().toISOString(),
67
+ );
68
+ migrated.contacts++;
69
+ }
70
+ log(`Migrated ${migrated.contacts} contacts`);
71
+ } catch (e) {
72
+ log(`Contacts migration error: ${e.message}`);
73
+ }
74
+
75
+ // ── Migrate contact_context ──
76
+ try {
77
+ const contexts = v1.prepare('SELECT * FROM contact_context').all();
78
+ const upsert = v2.prepare(`
79
+ INSERT INTO contact_context (phone, relationship, last_topic, pending_items, conversation_summary, preferences, last_interaction)
80
+ VALUES (?, ?, ?, ?, ?, ?, ?)
81
+ ON CONFLICT(phone) DO NOTHING
82
+ `);
83
+
84
+ for (const ctx of contexts) {
85
+ if (!ctx.phone) continue;
86
+ upsert.run(
87
+ ctx.phone, ctx.relationship || '', ctx.last_topic || '',
88
+ ctx.pending_items || '[]', ctx.conversation_summary || '',
89
+ ctx.preferences_learned || '{}', ctx.last_interaction || null,
90
+ );
91
+ migrated.context++;
92
+ }
93
+ log(`Migrated ${migrated.context} contact contexts`);
94
+ } catch (e) {
95
+ log(`Context migration error: ${e.message}`);
96
+ }
97
+
98
+ // ── Migrate contact_scopes ──
99
+ try {
100
+ const scopes = v1.prepare('SELECT * FROM contact_scopes').all();
101
+ const upsert = v2.prepare(`
102
+ INSERT INTO contact_scopes (phone, scope, granted, granted_by, granted_at)
103
+ VALUES (?, ?, ?, ?, ?)
104
+ ON CONFLICT(phone, scope) DO NOTHING
105
+ `);
106
+
107
+ for (const s of scopes) {
108
+ upsert.run(s.phone, s.scope, s.granted || 0, s.granted_by || 'migrated', s.granted_at || null);
109
+ migrated.scopes++;
110
+ }
111
+ log(`Migrated ${migrated.scopes} scopes`);
112
+ } catch (e) {
113
+ log(`Scopes migration error: ${e.message}`);
114
+ }
115
+
116
+ // ── Migrate message_log ──
117
+ try {
118
+ const messages = v1.prepare('SELECT * FROM message_log ORDER BY id DESC LIMIT 5000').all();
119
+ const insert = v2.prepare(`
120
+ INSERT INTO message_log (phone, channel, direction, text, attachments, sent_by, state_at_time, created_at)
121
+ VALUES (?, ?, ?, ?, '[]', ?, ?, ?)
122
+ `);
123
+
124
+ // Check if we already have messages to avoid re-importing
125
+ const existingCount = v2.prepare('SELECT COUNT(*) as c FROM message_log').get().c;
126
+ if (existingCount === 0) {
127
+ for (const m of messages.reverse()) {
128
+ const channel = (m.source || '').includes('whatsapp') ? 'whatsapp' : 'imessage';
129
+ const sentBy = m.sent_by || (m.direction === 'outbound' ? 'aiva' : 'contact');
130
+ insert.run(
131
+ m.phone, channel, m.direction || 'inbound',
132
+ m.message_preview || '', sentBy,
133
+ JSON.stringify(m.rules_applied || '{}'),
134
+ m.timestamp || new Date().toISOString(),
135
+ );
136
+ migrated.messages++;
137
+ }
138
+ log(`Migrated ${migrated.messages} messages`);
139
+ } else {
140
+ log(`Skipping message migration - v2 already has ${existingCount} messages`);
141
+ }
142
+ } catch (e) {
143
+ log(`Message log migration error: ${e.message}`);
144
+ }
145
+
146
+ // ── Migrate settings ──
147
+ try {
148
+ const settings = v1.prepare('SELECT * FROM settings').all();
149
+ const upsert = v2.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)');
150
+
151
+ for (const s of settings) {
152
+ // Skip v1-only settings
153
+ if (['scopes_migrated'].includes(s.key)) continue;
154
+ upsert.run(s.key, s.value);
155
+ migrated.settings++;
156
+ }
157
+ log(`Migrated ${migrated.settings} settings`);
158
+ } catch (e) {
159
+ log(`Settings migration error: ${e.message}`);
160
+ }
161
+
162
+ // ── Migrate follow_up_tracker ──
163
+ try {
164
+ const followUps = v1.prepare('SELECT * FROM follow_up_tracker').all();
165
+ const upsert = v2.prepare(`
166
+ INSERT INTO follow_up_tracker (phone, channel, contact_name, last_our_message, last_our_message_at, status, follow_up_count, max_follow_ups, next_follow_up_at, last_follow_up_at, last_follow_up_topic, opted_out, created_at, updated_at)
167
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
168
+ ON CONFLICT(phone) DO NOTHING
169
+ `);
170
+
171
+ for (const fu of followUps) {
172
+ upsert.run(
173
+ fu.phone, fu.channel || 'imessage', fu.contact_name || 'Unknown',
174
+ fu.last_our_message || '', fu.last_our_message_at,
175
+ fu.status || 'active', fu.follow_up_count || 0,
176
+ fu.max_follow_ups || 3, fu.next_follow_up_at,
177
+ fu.last_follow_up_at, fu.last_follow_up_topic || '',
178
+ fu.opted_out || 0, fu.created_at, fu.updated_at,
179
+ );
180
+ migrated.followUps++;
181
+ }
182
+ log(`Migrated ${migrated.followUps} follow-up trackers`);
183
+ } catch (e) {
184
+ log(`Follow-up migration error: ${e.message}`);
185
+ }
186
+
187
+ // ── Migrate scheduling_rules ──
188
+ try {
189
+ const rules = v1.prepare('SELECT * FROM scheduling_rules').all();
190
+ const upsert = v2.prepare(`
191
+ INSERT OR IGNORE INTO scheduling_rules (category, rule_preset, custom_instructions, structured_overrides)
192
+ VALUES (?, ?, ?, ?)
193
+ `);
194
+
195
+ for (const r of rules) {
196
+ upsert.run(r.category, r.rule_preset || 'flexible', r.custom_instructions || '', r.structured_overrides || '{}');
197
+ migrated.schedulingRules++;
198
+ }
199
+ log(`Migrated ${migrated.schedulingRules} scheduling rules`);
200
+ } catch (e) {
201
+ log(`Scheduling rules migration error: ${e.message}`);
202
+ }
203
+
204
+ v1.close();
205
+
206
+ log('Migration complete!');
207
+ log(`Summary: ${JSON.stringify(migrated)}`);
208
+ }
209
+
210
+ // Run if called directly
211
+ if (require.main === module) {
212
+ run();
213
+ }
214
+
215
+ module.exports = { run };
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ // ── Seed Initial FAQ Entries ─────────────────────────────
3
+ 'use strict';
4
+
5
+ function run() {
6
+ const { initDatabase } = require('../db');
7
+ const { createFaq } = require('../knowledge-base');
8
+
9
+ initDatabase();
10
+
11
+ const entries = [
12
+ {
13
+ question: 'What services does Conversion Marketing Pros offer?',
14
+ answer: 'We offer website design and development, paid advertising management (Google Ads, Meta Ads, LinkedIn), SEO, and marketing strategy consulting.',
15
+ category: 'services',
16
+ keywords: 'services, offer, do, help, work',
17
+ },
18
+ {
19
+ question: 'What are your business hours?',
20
+ answer: 'Our team is available Monday through Friday, 9 AM to 5 PM Pacific Time. We also respond to messages on Saturdays.',
21
+ category: 'hours',
22
+ keywords: 'hours, open, available, time, when',
23
+ },
24
+ {
25
+ question: 'How much does a website cost?',
26
+ answer: 'Website projects vary based on scope and complexity. We\'d love to understand your needs first - can we hop on a quick call to discuss?',
27
+ category: 'pricing',
28
+ keywords: 'cost, price, pricing, expensive, budget, website',
29
+ },
30
+ {
31
+ question: 'How long does a website take to build?',
32
+ answer: 'A typical website redesign takes 6-8 weeks from kickoff to launch. We can expedite for an additional fee if you have a tight deadline.',
33
+ category: 'general',
34
+ keywords: 'timeline, how long, weeks, time, build, launch',
35
+ },
36
+ {
37
+ question: 'Where are you located?',
38
+ answer: 'We\'re based in Spokane, Washington, but we work with clients across the US.',
39
+ category: 'general',
40
+ keywords: 'location, where, based, office, city',
41
+ },
42
+ {
43
+ question: 'Do you offer free consultations?',
44
+ answer: 'Yes! We offer a free 30-minute discovery call to learn about your goals and see if we\'re a good fit.',
45
+ category: 'general',
46
+ keywords: 'free, consultation, discovery, call, meeting',
47
+ },
48
+ ];
49
+
50
+ let created = 0;
51
+ for (const entry of entries) {
52
+ try {
53
+ createFaq(entry);
54
+ created++;
55
+ } catch (e) {
56
+ console.log(`Skipped: ${entry.question.substring(0, 50)} - ${e.message}`);
57
+ }
58
+ }
59
+
60
+ console.log(`[SEED-FAQ] Created ${created} FAQ entries`);
61
+ }
62
+
63
+ if (require.main === module) {
64
+ run();
65
+ }
66
+
67
+ module.exports = { run };
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Seed the FAQ entries from knowledge-base.json into the SQLite database.
6
+ * Usage: node seed-knowledge-base.js [--clear]
7
+ * --clear: Remove all existing FAQ entries before seeding
8
+ */
9
+
10
+ const path = require('path');
11
+ const { listFaq, createFaq, deleteFaq } = require('./knowledge-base');
12
+ const kbPath = path.join(__dirname, 'data', 'knowledge-base.json');
13
+
14
+ // Force DB init
15
+ const { initDatabase } = require('./db');
16
+ initDatabase();
17
+
18
+ const entries = require(kbPath);
19
+ const doClear = process.argv.includes('--clear');
20
+
21
+ if (doClear) {
22
+ const existing = listFaq();
23
+ console.log(`Clearing ${existing.length} existing FAQ entries...`);
24
+ for (const e of existing) deleteFaq(e.id);
25
+ }
26
+
27
+ let created = 0;
28
+ for (const entry of entries) {
29
+ createFaq({
30
+ device_id: null,
31
+ question: entry.question,
32
+ answer: entry.answer,
33
+ category: entry.category,
34
+ keywords: entry.keywords || '',
35
+ });
36
+ created++;
37
+ }
38
+
39
+ console.log(`Seeded ${created} FAQ entries from knowledge-base.json`);