@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,160 @@
1
+ // ── Outbound Sender - Unified Send Across Channels ───────
2
+ 'use strict';
3
+
4
+ const { getStmts, getSetting } = require('./db');
5
+ const { sanitizeOutbound } = require('./utils/sanitize');
6
+ const { normalizePhone } = require('./utils/phone');
7
+ const { needsReintro } = require('./contact-manager');
8
+
9
+ const REINTRO_PHRASES = [
10
+ "Hey, Aiva here - ",
11
+ "Hey, it's Aiva - ",
12
+ "Hey, Aiva here. ",
13
+ "It's Aiva - ",
14
+ ];
15
+
16
+ // Channel adapters - lazily loaded
17
+ let _imessageAdapter = null;
18
+ let _whatsappAdapter = null;
19
+ let _quoAdapter = null;
20
+
21
+ function getAdapter(channel) {
22
+ switch (channel) {
23
+ case 'imessage':
24
+ if (!_imessageAdapter) _imessageAdapter = require('./adapters/imessage');
25
+ return _imessageAdapter;
26
+ case 'whatsapp':
27
+ if (!_whatsappAdapter) _whatsappAdapter = require('./adapters/whatsapp');
28
+ return _whatsappAdapter;
29
+ case 'quo':
30
+ if (!_quoAdapter) _quoAdapter = require('./adapters/quo');
31
+ return _quoAdapter;
32
+ default:
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function log(msg, data) {
38
+ const ts = new Date().toISOString();
39
+ if (data) console.log(`[${ts}] [SENDER] ${msg}`, JSON.stringify(data));
40
+ else console.log(`[${ts}] [SENDER] ${msg}`);
41
+ }
42
+
43
+ /**
44
+ * Send a message to a contact through the appropriate channel.
45
+ * Handles sanitization, reintroduction, rate limiting, and logging.
46
+ * @param {Object} options
47
+ * @param {string} options.phone - E.164 phone
48
+ * @param {string} options.text - Message text
49
+ * @param {string} [options.channel='imessage'] - Channel to send through
50
+ * @param {string} [options.sentBy='aiva'] - Who is sending (aiva, brandon, follow-up)
51
+ * @param {string} [options.stateAtTime=''] - Conversation state label
52
+ * @param {boolean} [options.skipSanitize=false] - Skip sanitization (for pre-sanitized messages)
53
+ * @returns {Promise<{sent: boolean, blocked: boolean, reason: string|null}>}
54
+ */
55
+ async function sendMessage({ phone, text, channel = 'imessage', sentBy = 'aiva', stateAtTime = '', skipSanitize = false }) {
56
+ const normalized = normalizePhone(phone);
57
+ const masterPhone = getSetting('masterPhone');
58
+ const isMaster = normalized === masterPhone;
59
+
60
+ // Rate limiting (skip for master phone)
61
+ if (!isMaster && sentBy === 'aiva') {
62
+ const stmts = getStmts();
63
+ const recentCount = stmts.countRecentOutbound.get(normalized, '1');
64
+ if (recentCount && recentCount.count >= 10) {
65
+ log('RATE LIMITED', { phone: normalized, count: recentCount.count });
66
+ return { sent: false, blocked: true, reason: 'rate_limit' };
67
+ }
68
+ }
69
+
70
+ // Sanitize
71
+ let finalText = text;
72
+ if (!skipSanitize) {
73
+ const sanitized = sanitizeOutbound(text, isMaster);
74
+ if (sanitized.blocked) {
75
+ log(`BLOCKED: ${sanitized.reason}`, { phone: normalized, preview: text.substring(0, 100) });
76
+ return { sent: false, blocked: true, reason: sanitized.reason };
77
+ }
78
+ finalText = sanitized.text;
79
+ }
80
+
81
+ if (!finalText) {
82
+ return { sent: false, blocked: true, reason: 'empty_after_sanitize' };
83
+ }
84
+
85
+ // Away message prepend
86
+ const awayMessage = getSetting('awayMessage');
87
+ if (awayMessage && sentBy === 'aiva') {
88
+ finalText = `${awayMessage}\n\n${finalText}`;
89
+ }
90
+
91
+ // Reintroduction logic
92
+ if (sentBy === 'aiva' && needsReintro(normalized)) {
93
+ const phrase = REINTRO_PHRASES[Math.floor(Math.random() * REINTRO_PHRASES.length)];
94
+ finalText = phrase + finalText;
95
+ }
96
+
97
+ // DRY RUN MODE: Log intended response instead of sending (Option A - parallel testing)
98
+ const dryRun = getSetting('v2DryRun') !== 'false'; // Default: dry run ON
99
+ if (dryRun) {
100
+ log('DRY RUN - WOULD SEND', { phone: normalized, channel, sentBy, len: finalText.length, text: finalText.substring(0, 300) });
101
+ // Still log to message_log so we can compare v1 vs v2 responses
102
+ const stmts = getStmts();
103
+ stmts.insertMessage.run(
104
+ normalized, channel, 'outbound', `[V2-DRY-RUN] ${finalText}`,
105
+ '[]', sentBy, stateAtTime,
106
+ );
107
+ return { sent: true, blocked: false, reason: 'dry_run' };
108
+ }
109
+
110
+ // Send via adapter
111
+ const adapter = getAdapter(channel);
112
+ if (!adapter) {
113
+ log('No adapter for channel', { channel, phone: normalized });
114
+ return { sent: false, blocked: false, reason: `no_adapter_${channel}` };
115
+ }
116
+
117
+ try {
118
+ const result = await adapter.send(normalized, finalText);
119
+ if (result.success) {
120
+ // Log to message_log
121
+ const stmts = getStmts();
122
+ stmts.insertMessage.run(
123
+ normalized, channel, 'outbound', finalText,
124
+ '[]', sentBy, stateAtTime,
125
+ );
126
+ log('Sent', { phone: normalized, channel, len: finalText.length });
127
+ return { sent: true, blocked: false, reason: null };
128
+ } else {
129
+ log('Send failed', { phone: normalized, channel, error: result.error });
130
+ return { sent: false, blocked: false, reason: result.error || 'send_failed' };
131
+ }
132
+ } catch (err) {
133
+ log('Send error', { phone: normalized, channel, error: err.message });
134
+ return { sent: false, blocked: false, reason: err.message };
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Send an escalation timeout notification to Brandon via iMessage.
140
+ * @param {string} contactName - Who the escalation was about
141
+ * @param {string} contactPhone - Contact's phone
142
+ * @param {string} question - What the contact asked
143
+ * @returns {Promise<boolean>}
144
+ */
145
+ async function sendEscalationTimeout(contactName, contactPhone, question) {
146
+ const notifyPhone = getSetting('escalationNotifyPhone') || '+15099794110';
147
+ const text = `Escalation timed out (2 strikes)\nFrom: ${contactName} (${contactPhone})\nAsked: ${question.substring(0, 200)}`;
148
+
149
+ const adapter = getAdapter('imessage');
150
+ if (!adapter) return false;
151
+
152
+ try {
153
+ const result = await adapter.send(notifyPhone, text);
154
+ return result.success;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ module.exports = { sendMessage, sendEscalationTimeout, REINTRO_PHRASES };
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "aiva-router-v2",
3
+ "version": "2.0.0",
4
+ "description": "AIVA Message Router v2 - autonomous contact messaging with escalation, learning, and scheduling",
5
+ "private": true,
6
+ "main": "index.js",
7
+ "dependencies": {
8
+ "better-sqlite3": "^11.0.0"
9
+ },
10
+ "engines": {
11
+ "node": ">=20.0.0"
12
+ }
13
+ }
@@ -0,0 +1,86 @@
1
+ // ── Permission Gate - Scope Checking ─────────────────────
2
+ // Gates actions, not speech. If a contact lacks scope for an action,
3
+ // the router escalates silently - never tells the contact they lack permission.
4
+ 'use strict';
5
+
6
+ const { hasScope } = require('./contact-manager');
7
+
8
+ // Map tool calls to required scopes
9
+ const TOOL_SCOPE_MAP = {
10
+ schedule_appointment: 'calendar.book',
11
+ check_availability: 'calendar.view',
12
+ reschedule_appointment: 'calendar.modify',
13
+ cancel_appointment: 'calendar.modify',
14
+ search_knowledge_base: null, // no scope needed
15
+ escalate: null, // always allowed
16
+ send_message: 'messages.send',
17
+ create_task: 'tasks.create',
18
+ create_reminder: 'reminders.create',
19
+ send_file: 'files.send',
20
+ };
21
+
22
+ function log(msg, data) {
23
+ const ts = new Date().toISOString();
24
+ if (data) console.log(`[${ts}] [PERMISSION] ${msg}`, JSON.stringify(data));
25
+ else console.log(`[${ts}] [PERMISSION] ${msg}`);
26
+ }
27
+
28
+ /**
29
+ * Check if a contact has permission for a specific tool call.
30
+ * Returns { allowed: boolean, requiredScope: string|null }.
31
+ * @param {string} phone - E.164 phone
32
+ * @param {string} toolName - Name of the tool being called
33
+ * @returns {{ allowed: boolean, requiredScope: string|null, shouldEscalate: boolean }}
34
+ */
35
+ function checkToolPermission(phone, toolName) {
36
+ const requiredScope = TOOL_SCOPE_MAP[toolName];
37
+
38
+ // No scope required for this tool
39
+ if (requiredScope === null || requiredScope === undefined) {
40
+ return { allowed: true, requiredScope: null, shouldEscalate: false };
41
+ }
42
+
43
+ const allowed = hasScope(phone, requiredScope);
44
+
45
+ if (!allowed) {
46
+ log('Permission denied - escalating', { phone, toolName, requiredScope });
47
+ }
48
+
49
+ return {
50
+ allowed,
51
+ requiredScope,
52
+ // When permission denied, we escalate silently - never tell the contact
53
+ shouldEscalate: !allowed,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Check if a contact has auto-reply enabled.
59
+ * Two ways to qualify:
60
+ * 1. Has 'messages.auto-reply' scope granted explicitly
61
+ * 2. Has response_mode set to 'auto' (three-tier hierarchy handles the rest)
62
+ * @param {string} phone
63
+ * @returns {boolean}
64
+ */
65
+ function canAutoReply(phone) {
66
+ if (hasScope(phone, 'messages.auto-reply')) return true;
67
+ // Also allow if contact's response_mode is 'auto'
68
+ const { getContact } = require('./contact-manager');
69
+ const contact = getContact(phone);
70
+ if (contact && contact.response_mode === 'auto') return true;
71
+ return false;
72
+ }
73
+
74
+ /**
75
+ * Check if a contact is blocked (no scopes at all / category = blocked).
76
+ * @param {string} phone
77
+ * @param {Object} contact - Contact record
78
+ * @returns {boolean}
79
+ */
80
+ function isBlocked(phone, contact) {
81
+ if (contact?.category === 'blocked') return true;
82
+ if (contact?.response_mode === 'block') return true;
83
+ return false;
84
+ }
85
+
86
+ module.exports = { checkToolPermission, canAutoReply, isBlocked, TOOL_SCOPE_MAP };
@@ -0,0 +1,177 @@
1
+ // ── Business Playbook - Loading + Composable Prompt Sections ──
2
+ 'use strict';
3
+
4
+ const { getStmts } = require('./db');
5
+
6
+ function log(msg, data) {
7
+ const ts = new Date().toISOString();
8
+ if (data) console.log(`[${ts}] [PLAYBOOK] ${msg}`, JSON.stringify(data));
9
+ else console.log(`[${ts}] [PLAYBOOK] ${msg}`);
10
+ }
11
+
12
+ /**
13
+ * Get brand voice section for a device. Always injected.
14
+ * @param {string|null} deviceId
15
+ * @returns {string} Brand voice text
16
+ */
17
+ function getBrandVoice(deviceId = null) {
18
+ const stmts = getStmts();
19
+ const sections = stmts.getPlaybookByType.all('brand_voice', deviceId);
20
+ if (sections.length === 0) return '';
21
+ return sections.map(s => s.content).join('\n');
22
+ }
23
+
24
+ /**
25
+ * Get guardrail sections. Always injected as background rules.
26
+ * @param {string|null} deviceId
27
+ * @returns {string}
28
+ */
29
+ function getGuardrails(deviceId = null) {
30
+ const stmts = getStmts();
31
+ const sections = stmts.getPlaybookByType.all('guardrail', deviceId);
32
+ if (sections.length === 0) return '';
33
+ return sections.map(s => `- ${s.content}`).join('\n');
34
+ }
35
+
36
+ /**
37
+ * Get relevant playbook sections based on conversation context.
38
+ * Only returns sections matching the detected topic.
39
+ * @param {string|null} deviceId
40
+ * @param {Object} context - { topic, isLead, isPricing, isObjection }
41
+ * @returns {string} Combined relevant playbook text
42
+ */
43
+ function getRelevantSections(deviceId = null, context = {}) {
44
+ const stmts = getStmts();
45
+ const allSections = stmts.getPlaybookSections.all(deviceId);
46
+ const relevant = [];
47
+
48
+ for (const section of allSections) {
49
+ // Brand voice and guardrails handled separately
50
+ if (section.section_type === 'brand_voice' || section.section_type === 'guardrail') continue;
51
+
52
+ // Service sections - match by topic keyword in title
53
+ if (section.section_type === 'service' && context.topic) {
54
+ const titleLower = (section.title || '').toLowerCase();
55
+ const topicLower = context.topic.toLowerCase();
56
+ if (titleLower.includes(topicLower) || topicLower.includes(titleLower)) {
57
+ relevant.push(section);
58
+ }
59
+ }
60
+
61
+ // Pricing - only when pricing is being discussed
62
+ if (section.section_type === 'pricing' && context.isPricing) {
63
+ relevant.push(section);
64
+ }
65
+
66
+ // Qualifier - only for leads
67
+ if (section.section_type === 'qualifier' && context.isLead) {
68
+ relevant.push(section);
69
+ }
70
+
71
+ // Objection handler - when contact pushes back
72
+ if (section.section_type === 'objection_handler' && context.isObjection) {
73
+ relevant.push(section);
74
+ }
75
+ }
76
+
77
+ if (relevant.length === 0) return '';
78
+ return relevant.map(s => {
79
+ const header = s.title ? `[${s.section_type}: ${s.title}]` : `[${s.section_type}]`;
80
+ return `${header}\n${s.content}`;
81
+ }).join('\n\n');
82
+ }
83
+
84
+ /**
85
+ * Build the full playbook prompt injection for a message.
86
+ * Keeps it lean - only injects what's relevant.
87
+ * @param {string|null} deviceId
88
+ * @param {Object} context - Conversation context signals
89
+ * @returns {string} Combined playbook text for prompt injection
90
+ */
91
+ function buildPlaybookPrompt(deviceId = null, context = {}) {
92
+ const parts = [];
93
+
94
+ const voice = getBrandVoice(deviceId);
95
+ if (voice) parts.push(`[Brand Voice]\n${voice}`);
96
+
97
+ const guardrails = getGuardrails(deviceId);
98
+ if (guardrails) parts.push(`[Guardrails]\n${guardrails}`);
99
+
100
+ const relevant = getRelevantSections(deviceId, context);
101
+ if (relevant) parts.push(relevant);
102
+
103
+ return parts.join('\n\n');
104
+ }
105
+
106
+ // ── CRUD for playbook management API ──
107
+
108
+ /**
109
+ * List all playbook sections for a device.
110
+ * @param {string|null} deviceId
111
+ * @returns {Array}
112
+ */
113
+ function listSections(deviceId = null) {
114
+ return getStmts().getPlaybookSections.all(deviceId);
115
+ }
116
+
117
+ /**
118
+ * Create a new playbook section.
119
+ * @param {Object} section
120
+ * @returns {Object} Created section with id
121
+ */
122
+ function createSection(section) {
123
+ const stmts = getStmts();
124
+ const info = stmts.insertPlaybook.run(
125
+ section.device_id || null,
126
+ section.section_type,
127
+ section.title || '',
128
+ section.content,
129
+ section.priority || 10,
130
+ );
131
+ log('Created playbook section', { id: info.lastInsertRowid, type: section.section_type });
132
+ return stmts.getPlaybookById.get(info.lastInsertRowid);
133
+ }
134
+
135
+ /**
136
+ * Update a playbook section.
137
+ * @param {number} id
138
+ * @param {Object} updates
139
+ * @returns {Object|null}
140
+ */
141
+ function updateSection(id, updates) {
142
+ const stmts = getStmts();
143
+ const existing = stmts.getPlaybookById.get(id);
144
+ if (!existing) return null;
145
+
146
+ stmts.updatePlaybook.run(
147
+ updates.section_type ?? existing.section_type,
148
+ updates.title ?? existing.title,
149
+ updates.content ?? existing.content,
150
+ updates.priority ?? existing.priority,
151
+ updates.enabled ?? existing.enabled,
152
+ id,
153
+ );
154
+ log('Updated playbook section', { id });
155
+ return stmts.getPlaybookById.get(id);
156
+ }
157
+
158
+ /**
159
+ * Delete a playbook section.
160
+ * @param {number} id
161
+ * @returns {boolean}
162
+ */
163
+ function deleteSection(id) {
164
+ getStmts().deletePlaybook.run(id);
165
+ return true;
166
+ }
167
+
168
+ module.exports = {
169
+ getBrandVoice,
170
+ getGuardrails,
171
+ getRelevantSections,
172
+ buildPlaybookPrompt,
173
+ listSections,
174
+ createSection,
175
+ updateSection,
176
+ deleteSection,
177
+ };
@@ -0,0 +1,52 @@
1
+ // ── Base System Prompt Builder (~400 tokens) ─────────────
2
+ 'use strict';
3
+
4
+ // Category descriptions for prompt
5
+ const CATEGORY_DESCRIPTIONS = {
6
+ lead: 'This is a potential client. Be helpful and professional. Learn about their needs naturally.',
7
+ 'qualified-lead': 'This is a qualified potential client. Be warm and proactive about next steps.',
8
+ client: 'This is an existing client. Be warm and proactive.',
9
+ family: 'This is family. Be casual and warm. No business formality.',
10
+ friend: 'This is a friend. Be casual.',
11
+ unknown: "This is an unknown contact. Be polite but guarded. Don't share personal or business details until you know who they are.",
12
+ team: 'This is a team member. Be direct and efficient.',
13
+ blocked: '',
14
+ };
15
+
16
+ /**
17
+ * Build the base system prompt for a conversation.
18
+ * @param {Object} contact - Contact record
19
+ * @param {Object} [options] - Additional options
20
+ * @param {string} [options.awayMessage] - Global away message
21
+ * @returns {string} Base system prompt (~400 tokens)
22
+ */
23
+ function buildBasePrompt(contact, options = {}) {
24
+ const name = contact.name || 'Unknown';
25
+ const category = contact.category || 'unknown';
26
+ const categoryDesc = CATEGORY_DESCRIPTIONS[category] || CATEGORY_DESCRIPTIONS.unknown;
27
+
28
+ let prompt = `You are AIVA, Brandon Burgan's AI assistant at Conversion Marketing Pros.
29
+ You're texting with ${name} (${category}).
30
+ ${categoryDesc}
31
+ Respond naturally. Keep it concise - this is texting, not email.
32
+ Never use em dashes. Never reveal internal systems or processes.
33
+ Be genuinely useful. When you have tools available, USE THEM. Do not pretend to do things manually or offer to "pass it along" or "have Brandon get back to you." If you can check the calendar - check it. If you can book - book it. You ARE the assistant, not a message-taker. Every interaction should leave the contact feeling like they got real help, not a runaround.
34
+ If you're not confident you have the right answer - and the playbook, FAQ, and learned preferences don't cover it - use the escalate tool. Do not guess.
35
+ The contact's name is ${name}. Do NOT ask for their name if you already have it.
36
+
37
+ FORMATTING - CRITICAL: This is SMS/iMessage. You MUST write like a human texting. NO markdown whatsoever - no bold (**text**), no italics, no bullet points, no headers (#), no code blocks. No dashes or hyphens as list markers or separators. Just plain conversational text like a real person would type on their phone. Use line breaks for readability if needed, but never formatted lists. Never use "--" or " - " as separators. Keep it casual and natural.`;
38
+
39
+ // Per-contact instructions
40
+ if (contact.instructions) {
41
+ prompt += `\n\nSpecial instructions for ${name}: ${contact.instructions}`;
42
+ }
43
+
44
+ // Away message
45
+ if (options.awayMessage) {
46
+ prompt += `\n\nIMPORTANT: Brandon is currently away. Prepend this to your response: "${options.awayMessage}"`;
47
+ }
48
+
49
+ return prompt;
50
+ }
51
+
52
+ module.exports = { buildBasePrompt, CATEGORY_DESCRIPTIONS };
@@ -0,0 +1,38 @@
1
+ // ── First Contact / Introduction Prompt Module ───────────
2
+ // Injected when contact has not been introduced to AIVA yet
3
+ 'use strict';
4
+
5
+ /**
6
+ * Build first contact introduction prompt.
7
+ * @param {Object} contact - Contact record
8
+ * @returns {string}
9
+ */
10
+ function buildFirstContactPrompt(contact) {
11
+ const category = contact.category || 'unknown';
12
+
13
+ if (category === 'unknown') {
14
+ return `[First Contact]
15
+ This contact hasn't interacted with AIVA before. Introduce yourself naturally:
16
+ "Hey! This is Aiva, Brandon's assistant. How can I help?"
17
+ Keep it brief. Don't over-explain what you are or what you can do.
18
+ Learn who they are and what they need before saying more.`;
19
+ }
20
+
21
+ if (category === 'lead' || category === 'qualified-lead') {
22
+ return `[First Contact - Lead]
23
+ This is a new lead. Introduce yourself warmly:
24
+ "Hey! This is Aiva, Brandon's assistant at Conversion Marketing Pros. Thanks for reaching out - how can I help?"
25
+ Be welcoming but not pushy. Learn about their needs.`;
26
+ }
27
+
28
+ if (category === 'client') {
29
+ return `[First Contact - Client]
30
+ This is an existing client but AIVA hasn't introduced itself yet. Keep it simple:
31
+ "Hey! This is Aiva, Brandon's assistant. I'll be helping with scheduling and quick questions. What can I do for you?"`;
32
+ }
33
+
34
+ return `[First Contact]
35
+ Introduce yourself briefly as Aiva, Brandon's assistant. Keep it natural.`;
36
+ }
37
+
38
+ module.exports = { buildFirstContactPrompt };
@@ -0,0 +1,37 @@
1
+ // ── Lead Qualification Prompt Module ──────────────────────
2
+ // Only injected when category = lead or qualified-lead
3
+ 'use strict';
4
+
5
+ /**
6
+ * Build lead qualification prompt.
7
+ * @param {Object} contact - Contact record with qualification data
8
+ * @returns {string}
9
+ */
10
+ function buildLeadQualPrompt(contact) {
11
+ const score = contact.qualification_score || 0;
12
+ const stage = contact.pipeline_stage || 'cold';
13
+
14
+ let prompt = `[Lead Qualification - Internal Only]
15
+ This person might be a potential client. As you chat, naturally learn:
16
+ - What they need help with (their problem/goal)
17
+ - Their timeline (urgent vs. exploring)
18
+ - Their budget range (if it comes up naturally)
19
+ - Their business type and size
20
+ Don't interrogate. Just be helpful and curious. If they mention a need,
21
+ ask a natural follow-up. If they're just chatting, chat back.`;
22
+
23
+ // Stage-specific behavior
24
+ const stageBehavior = {
25
+ cold: 'Be friendly, answer questions, gently explore their needs.',
26
+ warm: 'Be more proactive about understanding their specific needs and timeline.',
27
+ hot: 'Suggest a call or meeting with Brandon. Share relevant case studies or examples.',
28
+ qualified: 'This lead is highly qualified. Prioritize scheduling a discovery call with Brandon.',
29
+ };
30
+
31
+ prompt += `\nCurrent stage: ${stage} (score: ${score}/100)`;
32
+ prompt += `\nBehavior: ${stageBehavior[stage] || stageBehavior.cold}`;
33
+
34
+ return prompt;
35
+ }
36
+
37
+ module.exports = { buildLeadQualPrompt };
@@ -0,0 +1,72 @@
1
+ // ── Scheduling Context Prompt Module ──────────────────────
2
+ // Only injected when conversation state = scheduling or scheduling tool available
3
+ 'use strict';
4
+
5
+ /**
6
+ * Build scheduling context prompt.
7
+ * @param {Object} schedulingRule - Category scheduling rule
8
+ * @param {Object} [stateData] - Current scheduling state data
9
+ * @returns {string}
10
+ */
11
+ function buildSchedulingPrompt(schedulingRule, stateData = {}) {
12
+ const preset = schedulingRule?.rule_preset || 'flexible';
13
+ const custom = schedulingRule?.custom_instructions || '';
14
+
15
+ // Provide current date/time and day-of-week mapping so the model doesn't hallucinate dates
16
+ const now = new Date();
17
+ const pst = new Intl.DateTimeFormat('en-US', {
18
+ timeZone: 'America/Los_Angeles',
19
+ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
20
+ hour: 'numeric', minute: 'numeric', hour12: true,
21
+ }).format(now);
22
+
23
+ // Generate next 7 days with day-of-week for reference
24
+ const days = [];
25
+ for (let i = 1; i <= 7; i++) {
26
+ const d = new Date(now.getTime() + i * 86400000);
27
+ const fmt = new Intl.DateTimeFormat('en-US', {
28
+ timeZone: 'America/Los_Angeles',
29
+ weekday: 'long', month: 'long', day: 'numeric',
30
+ }).format(d);
31
+ const iso = d.toISOString().split('T')[0];
32
+ days.push(`${fmt} (${iso})`);
33
+ }
34
+
35
+ let prompt = `[Scheduling Context]
36
+ CURRENT DATE/TIME: ${pst} (Pacific Time)
37
+ UPCOMING DAYS: ${days.join(', ')}
38
+
39
+ MANDATORY: You MUST use the schedule_appointment tool for ALL scheduling actions. NEVER pretend to book, check availability, or confirm appointments without calling the tool. If you say "booked" or "calendar invite sent" without having called the tool, you are lying to the contact.
40
+
41
+ Step 1: When contact wants to meet, use schedule_appointment with action "check_availability" and the requested date to find open slots.
42
+ Step 2: Present available times to the contact.
43
+ Step 3: When they confirm a time, ask for meeting topic. If you need their email for the calendar invite, ask for it - but NEVER ask for their name if you already know it from the conversation context.
44
+ Step 4: Use schedule_appointment with action "book" to ACTUALLY create the event. Include attendeeEmail if provided.
45
+ Step 5: Only confirm the booking AFTER the tool returns success.
46
+
47
+ IMPORTANT: You already know the contact's name from the system context. Do NOT ask "who am I speaking with" or "what's your name" if the contact name is already provided above. Only ask for their EMAIL if you need it for the calendar invite.
48
+
49
+ Use ISO datetime format for all times (e.g., 2026-03-05T14:00:00-08:00 for 2 PM PST).
50
+ ALWAYS use the day-of-week reference above to map "next Wednesday" etc. to the correct date. Do NOT guess dates.`;
51
+
52
+ const presetRules = {
53
+ flexible: 'Be flexible with timing. Mornings and afternoons are fine.',
54
+ 'work-hours': 'Only schedule during business hours (9 AM - 5 PM PST, Mon-Fri).',
55
+ professional: 'Schedule during business hours. Allow 30-minute buffer between meetings. Suggest 2-3 options.',
56
+ gatekeeper: 'Before scheduling, understand why they want to meet. Suggest a brief 15-minute intro call first.',
57
+ };
58
+
59
+ prompt += `\nScheduling rule: ${presetRules[preset] || presetRules.flexible}`;
60
+
61
+ if (custom) {
62
+ prompt += `\nAdditional: ${custom}`;
63
+ }
64
+
65
+ if (stateData.suggestedSlots) {
66
+ prompt += `\nPreviously suggested slots: ${JSON.stringify(stateData.suggestedSlots)}`;
67
+ }
68
+
69
+ return prompt;
70
+ }
71
+
72
+ module.exports = { buildSchedulingPrompt };
@@ -0,0 +1,22 @@
1
+ // ── Per-Contact Style Injection ───────────────────────────
2
+ 'use strict';
3
+
4
+ const STYLE_GUIDES = {
5
+ casual: 'Keep it casual. Short messages. Emoji ok but don\'t overdo it.',
6
+ professional: 'Keep it professional. Complete sentences. No emoji.',
7
+ friendly: 'Be warm and friendly. Light humor is fine. Emoji welcome.',
8
+ };
9
+
10
+ /**
11
+ * Build style override prompt for a contact.
12
+ * @param {Object} contact - Contact record
13
+ * @returns {string} Style override text, or empty string if default
14
+ */
15
+ function buildStylePrompt(contact) {
16
+ const style = contact.style || 'casual';
17
+ const guide = STYLE_GUIDES[style];
18
+ if (!guide) return '';
19
+ return `[Style: ${style}]\n${guide}`;
20
+ }
21
+
22
+ module.exports = { buildStylePrompt, STYLE_GUIDES };