@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
|
@@ -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`);
|