@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,129 @@
1
+ // ── AI Call Wrapper (Sonnet via OpenClaw proxy) ───────────
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const PROXY_URL = 'http://127.0.0.1:18789/v1/chat/completions';
8
+ const CONFIG_PATH = path.join(process.env.HOME || '/Users/brandonburgan', '.openclaw', 'openclaw.json');
9
+ const DEFAULT_MODEL = 'claude-sonnet-4-20250514';
10
+
11
+ let _cachedPassword = null;
12
+
13
+ function getOpenClawPassword() {
14
+ if (_cachedPassword) return _cachedPassword;
15
+ try {
16
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
17
+ _cachedPassword = config?.gateway?.auth?.password || '';
18
+ return _cachedPassword;
19
+ } catch {
20
+ return '';
21
+ }
22
+ }
23
+
24
+ function log(msg, data) {
25
+ const ts = new Date().toISOString();
26
+ if (data) console.log(`[${ts}] [AI] ${msg}`, JSON.stringify(data));
27
+ else console.log(`[${ts}] [AI] ${msg}`);
28
+ }
29
+
30
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
31
+
32
+ /**
33
+ * Call Sonnet via the OpenClaw proxy with retry and backoff.
34
+ * @param {Object} options
35
+ * @param {Array<{role: string, content: string}>} options.messages - Chat messages
36
+ * @param {Array<Object>} [options.tools] - Tool definitions for function calling
37
+ * @param {string} [options.model] - Model override (default: latest Sonnet)
38
+ * @param {number} [options.maxTokens=1000] - Max tokens
39
+ * @param {number} [options.temperature=0.7] - Temperature
40
+ * @param {number} [options.timeoutMs=60000] - Request timeout
41
+ * @returns {Promise<{content: string|null, toolCalls: Array|null, usage: Object|null}>}
42
+ */
43
+ async function callSonnet({ messages, tools, model, maxTokens = 1000, temperature = 0.7, timeoutMs = 60000 }) {
44
+ const password = getOpenClawPassword();
45
+ if (!password) {
46
+ log('No OpenClaw password found');
47
+ return { content: null, toolCalls: null, usage: null };
48
+ }
49
+
50
+ const body = {
51
+ model: model || DEFAULT_MODEL,
52
+ max_tokens: maxTokens,
53
+ temperature,
54
+ messages,
55
+ };
56
+ if (tools && tools.length > 0) {
57
+ body.tools = tools;
58
+ }
59
+
60
+ // Retry with exponential backoff
61
+ const delays = [0, 5000, 15000, 30000, 60000];
62
+ for (let attempt = 0; attempt < delays.length; attempt++) {
63
+ if (attempt > 0) {
64
+ log(`Retry ${attempt}/${delays.length - 1}, waiting ${delays[attempt] / 1000}s`);
65
+ await sleep(delays[attempt]);
66
+ }
67
+
68
+ try {
69
+ const resp = await fetch(PROXY_URL, {
70
+ method: 'POST',
71
+ headers: {
72
+ 'Content-Type': 'application/json',
73
+ 'Authorization': `Bearer ${password}`,
74
+ },
75
+ body: JSON.stringify(body),
76
+ signal: AbortSignal.timeout(timeoutMs),
77
+ });
78
+
79
+ if (!resp.ok) {
80
+ const errText = await resp.text().catch(() => 'unknown');
81
+ log('Proxy HTTP error', { status: resp.status, body: errText.substring(0, 200), attempt });
82
+ continue;
83
+ }
84
+
85
+ const data = await resp.json();
86
+ const choice = data.choices?.[0];
87
+ if (!choice) {
88
+ log('No choices in response', { attempt });
89
+ continue;
90
+ }
91
+
92
+ const message = choice.message;
93
+ const content = message?.content?.trim() || null;
94
+ const toolCalls = message?.tool_calls || null;
95
+
96
+ return {
97
+ content,
98
+ toolCalls,
99
+ usage: data.usage || null,
100
+ };
101
+ } catch (e) {
102
+ log('Proxy request error', { error: e.message, attempt });
103
+ continue;
104
+ }
105
+ }
106
+
107
+ log('All retries exhausted');
108
+ return { content: null, toolCalls: null, usage: null };
109
+ }
110
+
111
+ /**
112
+ * Simple text-in/text-out AI call.
113
+ * @param {string} systemPrompt - System prompt
114
+ * @param {string} userMessage - User message
115
+ * @param {Object} [opts] - Additional options (model, maxTokens, temperature)
116
+ * @returns {Promise<string|null>} Response text or null on failure
117
+ */
118
+ async function callAI(systemPrompt, userMessage, opts = {}) {
119
+ const result = await callSonnet({
120
+ messages: [
121
+ { role: 'system', content: systemPrompt },
122
+ { role: 'user', content: userMessage },
123
+ ],
124
+ ...opts,
125
+ });
126
+ return result.content;
127
+ }
128
+
129
+ module.exports = { callSonnet, callAI, DEFAULT_MODEL };
@@ -0,0 +1,52 @@
1
+ // ── Phone Number Normalization Utilities ──────────────────
2
+ 'use strict';
3
+
4
+ /**
5
+ * Normalize a phone number to E.164 format.
6
+ * Strips all non-digit characters (except leading +), ensures +1 prefix for US numbers.
7
+ * @param {string} raw - Raw phone number input
8
+ * @returns {string} E.164 normalized phone number (e.g. +15551234567)
9
+ */
10
+ function normalizePhone(raw) {
11
+ if (!raw) return '';
12
+ let cleaned = String(raw).replace(/[^\d+]/g, '');
13
+ // Remove leading + to work with digits only
14
+ const hasPlus = cleaned.startsWith('+');
15
+ cleaned = cleaned.replace(/^\+/, '');
16
+ // Remove leading 1 if 11 digits (US format)
17
+ if (cleaned.length === 11 && cleaned.startsWith('1')) {
18
+ return '+' + cleaned;
19
+ }
20
+ // 10 digits - assume US, add +1
21
+ if (cleaned.length === 10) {
22
+ return '+1' + cleaned;
23
+ }
24
+ // Already has country code or international
25
+ if (hasPlus || cleaned.length > 10) {
26
+ return '+' + cleaned;
27
+ }
28
+ // Fallback - return what we have with +
29
+ return '+' + cleaned;
30
+ }
31
+
32
+ /**
33
+ * Check if a phone number is valid (basic check - has enough digits).
34
+ * @param {string} phone - Phone number (should be E.164)
35
+ * @returns {boolean}
36
+ */
37
+ function isValidPhone(phone) {
38
+ if (!phone) return false;
39
+ const digits = phone.replace(/\D/g, '');
40
+ return digits.length >= 10 && digits.length <= 15;
41
+ }
42
+
43
+ /**
44
+ * Strip +1 prefix for display purposes.
45
+ * @param {string} phone - E.164 phone number
46
+ * @returns {string} Phone without country code
47
+ */
48
+ function stripCountryCode(phone) {
49
+ return (phone || '').replace(/^\+1/, '');
50
+ }
51
+
52
+ module.exports = { normalizePhone, isValidPhone, stripCountryCode };
@@ -0,0 +1,98 @@
1
+ // ── Response Validator - Haiku post-validation layer ──
2
+ // Checks if Sonnet's response overpromises capabilities the contact doesn't have.
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ const PROXY_URL = 'http://127.0.0.1:18789/v1/chat/completions';
9
+ const MODEL = 'claude-haiku-4-20250514';
10
+ const CONFIG_PATH = path.join(process.env.HOME || '/Users/brandonburgan', '.openclaw', 'openclaw.json');
11
+
12
+ let _cachedPassword = null;
13
+ function getPassword() {
14
+ if (_cachedPassword) return _cachedPassword;
15
+ try {
16
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
17
+ _cachedPassword = config?.gateway?.auth?.password || '';
18
+ return _cachedPassword;
19
+ } catch { return ''; }
20
+ }
21
+
22
+ /**
23
+ * Validate a Sonnet response against actual contact permissions.
24
+ * Uses Haiku via the OpenClaw proxy to check if the response overpromises.
25
+ * Fails open (returns valid) on any error.
26
+ *
27
+ * @param {string} responseText - Sonnet's generated response
28
+ * @param {Object} permissions - What the contact CAN do
29
+ * @param {boolean} permissions.canSchedule
30
+ * @param {boolean} permissions.canCreateTask
31
+ * @param {boolean} permissions.canCreateReminder
32
+ * @param {boolean} permissions.canSendFile
33
+ * @param {boolean} permissions.canSendMessage
34
+ * @returns {Promise<{valid: boolean, reason: string|null}>}
35
+ */
36
+ async function validateResponse(responseText, permissions) {
37
+ try {
38
+ // Build list of denied capabilities
39
+ const denied = [];
40
+ if (!permissions.canSchedule) denied.push('scheduling/booking appointments/checking calendar availability');
41
+ if (!permissions.canCreateTask) denied.push('creating tasks');
42
+ if (!permissions.canCreateReminder) denied.push('setting reminders');
43
+ if (!permissions.canSendFile) denied.push('sending files');
44
+ if (!permissions.canSendMessage) denied.push('sending messages to others');
45
+
46
+ // If everything is allowed, no need to validate
47
+ if (denied.length === 0) return { valid: true, reason: null };
48
+
49
+ const prompt = `You are a strict compliance validator. The assistant has these PROHIBITED capabilities: ${denied.join(', ')}.
50
+
51
+ The assistant MUST NOT:
52
+ - Promise, imply, or suggest it will do any prohibited action
53
+ - Say it will "pass along", "let someone know", "check on that", "get back to you", or relay any request related to a prohibited capability
54
+ - Mention Brandon or any human by name in relation to a prohibited capability
55
+ - Acknowledge the request in any way that implies it will be handled
56
+
57
+ If the response does ANY of the above, it is overpromising.
58
+
59
+ Response to validate: "${responseText}"
60
+
61
+ JSON only: {"overpromises": true/false, "capability": "which one or null"}`;
62
+
63
+ const password = getPassword();
64
+ const resp = await fetch(PROXY_URL, {
65
+ method: 'POST',
66
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${password}` },
67
+ body: JSON.stringify({
68
+ model: MODEL,
69
+ messages: [{ role: 'user', content: prompt }],
70
+ max_tokens: 100,
71
+ temperature: 0,
72
+ }),
73
+ signal: AbortSignal.timeout(10000),
74
+ });
75
+
76
+ if (!resp.ok) {
77
+ console.log(`[${new Date().toISOString()}] [VALIDATOR] Proxy error ${resp.status} (fail-open)`);
78
+ return { valid: true, reason: null };
79
+ }
80
+
81
+ const data = await resp.json();
82
+ const text = data.choices?.[0]?.message?.content || '';
83
+ const match = text.match(/\{[^}]+\}/);
84
+ if (!match) return { valid: true, reason: null };
85
+
86
+ const parsed = JSON.parse(match[0]);
87
+ if (parsed.overpromises) {
88
+ return { valid: false, reason: parsed.capability || 'unknown' };
89
+ }
90
+ return { valid: true, reason: null };
91
+ } catch (err) {
92
+ // Fail open
93
+ console.log(`[${new Date().toISOString()}] [VALIDATOR] Error (fail-open): ${err.message}`);
94
+ return { valid: true, reason: null };
95
+ }
96
+ }
97
+
98
+ module.exports = { validateResponse };
@@ -0,0 +1,222 @@
1
+ // ── Outbound Message Sanitization ─────────────────────────
2
+ // Ported from v1 router.js - every regex, every block pattern.
3
+ // This is the last line of defense before a message reaches a contact.
4
+ 'use strict';
5
+
6
+ // ── Block patterns - messages matching 2+ of these (or 1 strong) are blocked ──
7
+ const OUTBOUND_BLOCK_PATTERNS = [
8
+ // Command/tool output
9
+ /gog\s+calendar/i,
10
+ /GOG_KEYRING_PASSWORD/i,
11
+ /openclaw\s+(cron|gateway|message|session)/i,
12
+ /imsg\s+send/i,
13
+ /execSync|exec\(|spawn\(/i,
14
+ /curl\s+/i,
15
+ /POST\s+\/api\//i,
16
+ /GET\s+\/api\//i,
17
+ /localhost:\d+/i,
18
+ /127\.0\.0\.1/i,
19
+ /\$ \w+/, // shell prompts
20
+ // Error output
21
+ /Error:|ERR!|ECONNREFUSED|ETIMEDOUT|ENOENT|stack trace/i,
22
+ /at\s+\w+\s+\(.*:\d+:\d+\)/, // stack trace lines
23
+ /Command failed|exit code|stderr/i,
24
+ // Internal reasoning / chain-of-thought
25
+ /<thinking>[\s\S]*?<\/thinking>/i,
26
+ /\[ROUTER[\-:]|ROUTER-ALERT/i,
27
+ /\[Actionable:|CALENDAR_REQUEST|REMINDER_REQUEST|TASK_REQUEST/,
28
+ /escalat(?:ion|e|ing)\s+(?:to|code|request)/i,
29
+ /pending.action|pending_items|pending_request/i,
30
+ /system\s*prompt|chain.of.thought/i,
31
+ /HEARTBEAT_OK|HEARTBEAT\.md/i,
32
+ /AGENTS\.md|IDENTITY\.md|MEMORY\.md|TOOLS\.md/i,
33
+ /sub-?agent|subagent|spawned.*agent/i,
34
+ /webhook.*openclaw|openclaw.*webhook/i,
35
+ /callback.*url|callbackUrl/i,
36
+ // Schedule/calendar internals (not human-facing)
37
+ /available\s+slots?\s*:/i,
38
+ /Brandon'?s\s+(?:full\s+)?schedule/i,
39
+ // API keys and tokens
40
+ /sk-ant-|Bearer\s+\w{20,}|eyJ[A-Za-z0-9_-]{20,}/i,
41
+ /x-aiva-internal/i,
42
+ // JSON blobs (likely internal data)
43
+ /\{"(?:status|error|requestId|phone|pending|action)":/,
44
+ ];
45
+
46
+ // Strong patterns - a single match is enough to block
47
+ const STRONG_BLOCK_PATTERNS = [
48
+ /GOG_KEYRING_PASSWORD/i,
49
+ /sk-ant-|Bearer\s+\w{20,}/i,
50
+ /<thinking>[\s\S]*?<\/thinking>/i,
51
+ /x-aiva-internal/i,
52
+ /HEARTBEAT_OK|HEARTBEAT\.md/i,
53
+ /AGENTS\.md|IDENTITY\.md|TOOLS\.md/i,
54
+ /at\s+\w+\s+\(.*:\d+:\d+\)/,
55
+ /\{"(?:status|error|requestId|phone|pending|action)":/,
56
+ /openclaw/i,
57
+ ];
58
+
59
+ // Error message patterns
60
+ const ERROR_PATTERNS = [
61
+ /no response from openclaw/i,
62
+ /openclaw.*error/i,
63
+ /internal server error/i,
64
+ /ECONNREFUSED/i,
65
+ /agent.*timeout/i,
66
+ /session.*failed/i,
67
+ /tool.*error/i,
68
+ ];
69
+
70
+ // Chain-of-thought patterns to strip from responses
71
+ const COT_PATTERNS = [
72
+ /(?:Wait,?\s+(?:I need to|let me|I should|actually))[^\n]*/gi,
73
+ /(?:Let me (?:re-?read|reconsider|re-?think|re-?evaluate|look at))[^\n]*/gi,
74
+ /(?:Actually,?\s+(?:I think|let me|I should|on second thought))[^\n]*/gi,
75
+ /(?:Hmm,?\s+(?:I think|let me|I should|looking at))[^\n]*/gi,
76
+ /(?:On second thought)[^\n]*/gi,
77
+ /(?:I need to (?:reconsider|re-?think|re-?evaluate|check|be careful))[^\n]*/gi,
78
+ /(?:I'm responding as (?:Brandon's|an?) (?:assistant|AI))[^\n]*/gi,
79
+ ];
80
+
81
+ // Internal directive patterns to strip
82
+ const INTERNAL_PATTERNS = [
83
+ /(?:I should\s)[^\n]*/gi,
84
+ /(?:I need to\s)[^\n]*/gi,
85
+ /(?:Looking at the conversation)[^\n]*/gi,
86
+ /(?:Analyzing (?:the |this ))[^\n]*/gi,
87
+ /(?:Based on (?:the |my )(?:instructions|rules|system prompt|context))[^\n]*/gi,
88
+ /(?:The (?:user|contact|sender) (?:is asking|wants|seems))[^\n]*/gi,
89
+ /(?:My (?:task|job|role|goal) (?:is|here is))[^\n]*/gi,
90
+ ];
91
+
92
+ // Sensitive line patterns for stripping from multi-part responses
93
+ const SENSITIVE_LINE_PATTERNS = [
94
+ /api[_-]?key/i, /password/i, /token/i, /secret/i, /openclaw/i,
95
+ /localhost/i, /127\.0\.0\.1/i, /\.openclaw\//i, /curl\s/i,
96
+ /sk-ant-/i, /sk-or-/i, /ghp_/i, /xai-/i, /r8_/i,
97
+ /supabase/i, /cloudflare/i, /twilio/i, /replicate/i,
98
+ /SID:/i, /Base64:/i, /\.json/i, /PM2/i, /ngrok/i,
99
+ /sessions_send/i, /sessions_spawn/i, /heartbeat/i,
100
+ /AGENTS\.md/i, /HEARTBEAT\.md/i, /MEMORY\.md/i, /TOOLS\.md/i,
101
+ /task.*board/i, /cron.*job/i, /gateway/i,
102
+ ];
103
+
104
+ /**
105
+ * Check if a message contains internal content that should NEVER reach a contact.
106
+ * @param {string} text - Message text to check
107
+ * @returns {boolean} True if the message contains internal content
108
+ */
109
+ function isInternalContent(text) {
110
+ if (!text) return false;
111
+ const matchCount = OUTBOUND_BLOCK_PATTERNS.reduce((count, p) => count + (p.test(text) ? 1 : 0), 0);
112
+ const strongMatch = STRONG_BLOCK_PATTERNS.some(p => p.test(text));
113
+ return strongMatch || matchCount >= 2;
114
+ }
115
+
116
+ /**
117
+ * Check if a message is an error message from internal systems.
118
+ * @param {string} text - Message text to check
119
+ * @returns {boolean}
120
+ */
121
+ function isErrorMessage(text) {
122
+ if (!text) return false;
123
+ return ERROR_PATTERNS.some(p => p.test(text));
124
+ }
125
+
126
+ /**
127
+ * Sanitize a response before sending to a contact.
128
+ * Strips chain-of-thought, internal directives, em dashes, and other artifacts.
129
+ * Returns null if the message should be suppressed entirely.
130
+ * @param {string} text - Raw response text
131
+ * @returns {string|null} Sanitized text, or null if message should be suppressed
132
+ */
133
+ function sanitizeResponse(text) {
134
+ if (!text) return null;
135
+ let result = text;
136
+
137
+ // 1. Strip thinking blocks
138
+ result = result.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
139
+
140
+ // 2. NO_REPLY anywhere in text - suppress entire message
141
+ if (/NO_REPLY/i.test(result)) return null;
142
+
143
+ // 3. Strip chain-of-thought patterns
144
+ for (const pat of COT_PATTERNS) {
145
+ result = result.replace(pat, '');
146
+ }
147
+
148
+ // 4. Strip internal directive lines
149
+ for (const pat of INTERNAL_PATTERNS) {
150
+ result = result.replace(pat, '');
151
+ }
152
+
153
+ // 5. Handle multiple draft concatenation - take last clean block
154
+ const draftSplitters = /(?:Wait|Actually|Let me re|Hmm|On second thought)[^\n]*\n/i;
155
+ if (draftSplitters.test(result)) {
156
+ const parts = result.split(draftSplitters).map(p => p.trim()).filter(Boolean);
157
+ if (parts.length > 1) {
158
+ result = parts[parts.length - 1];
159
+ }
160
+ }
161
+
162
+ // 6. Replace em dashes, en dashes, and horizontal bars with commas (AI artifact, looks unnatural in texts)
163
+ result = result.replace(/[\u2013\u2014\u2015]/g, ',').replace(/ , /g, ', ');
164
+
165
+ // 7. Final cleanup
166
+ result = result.replace(/\n{3,}/g, '\n\n').trim();
167
+ if (!result || result.length === 0) return null;
168
+
169
+ return result;
170
+ }
171
+
172
+ /**
173
+ * Strip sensitive lines from multi-part text (e.g. conversation history assembly).
174
+ * @param {string} text - Text that may contain sensitive lines
175
+ * @returns {string} Text with sensitive lines removed
176
+ */
177
+ function stripSensitiveLines(text) {
178
+ if (!text) return '';
179
+ const lines = text.split('\n');
180
+ return lines.filter(line => !SENSITIVE_LINE_PATTERNS.some(p => p.test(line))).join('\n');
181
+ }
182
+
183
+ /**
184
+ * Full sanitization pipeline for outbound messages.
185
+ * Combines sanitizeResponse + isInternalContent + isErrorMessage checks.
186
+ * @param {string} text - Raw outbound message
187
+ * @param {boolean} isMaster - Whether sending to the master phone (less restrictive)
188
+ * @returns {{ text: string|null, blocked: boolean, reason: string }}
189
+ */
190
+ function sanitizeOutbound(text, isMaster = false) {
191
+ if (!text) return { text: null, blocked: true, reason: 'empty' };
192
+
193
+ // Sanitize first
194
+ let sanitized = sanitizeResponse(text);
195
+ if (!sanitized) return { text: null, blocked: true, reason: 'sanitizer_null' };
196
+
197
+ // For non-master phones, check for internal content and errors
198
+ if (!isMaster) {
199
+ if (isInternalContent(sanitized)) {
200
+ return { text: null, blocked: true, reason: 'internal_content' };
201
+ }
202
+ if (isErrorMessage(sanitized)) {
203
+ return { text: null, blocked: true, reason: 'error_message' };
204
+ }
205
+ }
206
+
207
+ // Clean up escape sequences and extra whitespace for text messages
208
+ sanitized = sanitized.replace(/\\n/g, '\n').replace(/\\t/g, ' ');
209
+ sanitized = sanitized.replace(/\n{3,}/g, '\n\n').trim();
210
+
211
+ return { text: sanitized, blocked: false, reason: null };
212
+ }
213
+
214
+ module.exports = {
215
+ isInternalContent,
216
+ isErrorMessage,
217
+ sanitizeResponse,
218
+ stripSensitiveLines,
219
+ sanitizeOutbound,
220
+ OUTBOUND_BLOCK_PATTERNS,
221
+ STRONG_BLOCK_PATTERNS,
222
+ };