@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,472 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const WORKSPACE = path.join(process.env.HOME || '/Users/brandonburgan', '.openclaw', 'workspace');
5
+ const DB_PATH = path.join(__dirname, 'data', 'knowledge.db');
6
+ const EMBEDDING_DIM = 1536;
7
+
8
+ // Try to find an embedding endpoint - gateway chat completions doesn't support embeddings,
9
+ // so we fall back to BM25 text search which works great for this use case
10
+ let EMBEDDING_URL = null;
11
+ let EMBEDDING_TOKEN = null;
12
+ let EMBEDDING_MODEL = 'openai/text-embedding-3-small';
13
+
14
+ const WATCHED_PATHS = [
15
+ path.join(WORKSPACE, 'memory'),
16
+ path.join(WORKSPACE, 'MEMORY.md'),
17
+ path.join(WORKSPACE, 'TOOLS.md'),
18
+ path.join(WORKSPACE, 'AGENTS.md'),
19
+ path.join(WORKSPACE, 'SOUL.md'),
20
+ path.join(WORKSPACE, 'USER.md'),
21
+ ];
22
+
23
+ let db = null;
24
+ let lastIndexedAt = null;
25
+ let watchers = [];
26
+ let debounceTimers = {};
27
+
28
+ function initKnowledgeBase() {
29
+ const Database = require('better-sqlite3');
30
+ const dataDir = path.join(__dirname, 'data');
31
+ if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
32
+
33
+ db = new Database(DB_PATH);
34
+ db.pragma('journal_mode = WAL');
35
+
36
+ db.exec(`
37
+ CREATE TABLE IF NOT EXISTS chunks (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ file_path TEXT NOT NULL,
40
+ file_mtime REAL NOT NULL,
41
+ chunk_index INTEGER NOT NULL,
42
+ start_line INTEGER NOT NULL,
43
+ end_line INTEGER NOT NULL,
44
+ text TEXT NOT NULL,
45
+ embedding BLOB,
46
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
47
+ );
48
+ CREATE INDEX IF NOT EXISTS idx_file_path ON chunks(file_path);
49
+ `);
50
+
51
+ // Try to detect embedding API availability
52
+ tryDetectEmbeddingAPI();
53
+
54
+ console.log('[knowledge-base] Initialized SQLite at', DB_PATH);
55
+ return db;
56
+ }
57
+
58
+ async function tryDetectEmbeddingAPI() {
59
+ // TIER 1: Platform infrastructure — uses CMP's key from .env, NOT client token. This is intentional.
60
+ if (process.env.OPENAI_API_KEY) {
61
+ EMBEDDING_URL = 'https://api.openai.com/v1/embeddings';
62
+ EMBEDDING_TOKEN = process.env.OPENAI_API_KEY;
63
+ EMBEDDING_MODEL = 'text-embedding-3-small';
64
+ console.log('[knowledge-base] Using OpenAI embeddings API');
65
+ return;
66
+ }
67
+ // Check OpenClaw gateway (may support embeddings in future)
68
+ try {
69
+ const resp = await fetch('http://127.0.0.1:18789/v1/embeddings', {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.OPENCLAW_GATEWAY_TOKEN || 'Ttsrgr812!'}` },
72
+ body: JSON.stringify({ model: 'openai/text-embedding-3-small', input: 'test' })
73
+ });
74
+ if (resp.ok) {
75
+ EMBEDDING_URL = 'http://127.0.0.1:18789/v1/embeddings';
76
+ EMBEDDING_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || 'Ttsrgr812!';
77
+ console.log('[knowledge-base] Using OpenClaw gateway embeddings');
78
+ return;
79
+ }
80
+ } catch (e) {}
81
+ console.log('[knowledge-base] No embedding API available — using BM25 text search');
82
+ }
83
+
84
+ // ========== BM25 SEARCH ==========
85
+ // Simple but effective text search when no embedding API is available
86
+
87
+ function tokenize(text) {
88
+ return text.toLowerCase()
89
+ .replace(/[^a-z0-9\s-]/g, ' ')
90
+ .split(/\s+/)
91
+ .filter(t => t.length > 1);
92
+ }
93
+
94
+ function bm25Search(query, chunks, limit = 5) {
95
+ const queryTokens = tokenize(query);
96
+ if (queryTokens.length === 0) return [];
97
+
98
+ const N = chunks.length;
99
+ const avgDl = chunks.reduce((s, c) => s + tokenize(c.text).length, 0) / N;
100
+ const k1 = 1.5, b = 0.75;
101
+
102
+ // Compute IDF for query terms
103
+ const df = {};
104
+ for (const token of queryTokens) df[token] = 0;
105
+ for (const chunk of chunks) {
106
+ const tokens = new Set(tokenize(chunk.text));
107
+ for (const qt of queryTokens) {
108
+ if (tokens.has(qt)) df[qt]++;
109
+ }
110
+ }
111
+
112
+ const idf = {};
113
+ for (const [term, freq] of Object.entries(df)) {
114
+ idf[term] = Math.log((N - freq + 0.5) / (freq + 0.5) + 1);
115
+ }
116
+
117
+ // Score each chunk
118
+ const scored = chunks.map(chunk => {
119
+ const tokens = tokenize(chunk.text);
120
+ const dl = tokens.length;
121
+ const tf = {};
122
+ for (const t of tokens) {
123
+ tf[t] = (tf[t] || 0) + 1;
124
+ }
125
+
126
+ let score = 0;
127
+ for (const qt of queryTokens) {
128
+ const termTf = tf[qt] || 0;
129
+ if (termTf === 0) continue;
130
+ const tfNorm = (termTf * (k1 + 1)) / (termTf + k1 * (1 - b + b * (dl / avgDl)));
131
+ score += (idf[qt] || 0) * tfNorm;
132
+ }
133
+
134
+ return {
135
+ filePath: chunk.file_path,
136
+ startLine: chunk.start_line,
137
+ endLine: chunk.end_line,
138
+ text: chunk.text,
139
+ score
140
+ };
141
+ });
142
+
143
+ scored.sort((a, b) => b.score - a.score);
144
+ return scored.slice(0, limit).filter(r => r.score > 0);
145
+ }
146
+
147
+ // ========== EMBEDDING SEARCH ==========
148
+
149
+ async function getEmbedding(text) {
150
+ if (!EMBEDDING_URL) return null;
151
+ const resp = await fetch(EMBEDDING_URL, {
152
+ method: 'POST',
153
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${EMBEDDING_TOKEN}` },
154
+ body: JSON.stringify({ model: EMBEDDING_MODEL, input: text })
155
+ });
156
+ if (!resp.ok) throw new Error(`Embedding API error: ${resp.status}`);
157
+ const data = await resp.json();
158
+ return new Float32Array(data.data[0].embedding);
159
+ }
160
+
161
+ async function getEmbeddingsBatch(texts) {
162
+ if (!EMBEDDING_URL) return texts.map(() => null);
163
+ const results = [];
164
+ for (let i = 0; i < texts.length; i += 20) {
165
+ const batch = texts.slice(i, i + 20);
166
+ const resp = await fetch(EMBEDDING_URL, {
167
+ method: 'POST',
168
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${EMBEDDING_TOKEN}` },
169
+ body: JSON.stringify({ model: EMBEDDING_MODEL, input: batch })
170
+ });
171
+ if (!resp.ok) throw new Error(`Embedding API error: ${resp.status}`);
172
+ const data = await resp.json();
173
+ for (const item of data.data) results.push(new Float32Array(item.embedding));
174
+ }
175
+ return results;
176
+ }
177
+
178
+ function cosineSimilarity(a, b) {
179
+ let dot = 0;
180
+ for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
181
+ return dot;
182
+ }
183
+
184
+ // ========== CHUNKING ==========
185
+
186
+ function chunkMarkdown(text) {
187
+ const lines = text.split('\n');
188
+ const chunks = [];
189
+ const MAX_CHARS = 2000;
190
+
191
+ // Build paragraphs
192
+ let paragraphs = [];
193
+ let currentPara = [];
194
+ let paraStart = 0;
195
+
196
+ for (let i = 0; i < lines.length; i++) {
197
+ if (lines[i].trim() === '' && currentPara.length > 0) {
198
+ paragraphs.push({ text: currentPara.join('\n'), startLine: paraStart + 1, endLine: i });
199
+ currentPara = [];
200
+ paraStart = i + 1;
201
+ } else {
202
+ if (currentPara.length === 0) paraStart = i;
203
+ currentPara.push(lines[i]);
204
+ }
205
+ }
206
+ if (currentPara.length > 0) {
207
+ paragraphs.push({ text: currentPara.join('\n'), startLine: paraStart + 1, endLine: lines.length });
208
+ }
209
+
210
+ // Group into chunks
211
+ let chunk = { texts: [], startLine: 0, endLine: 0, charCount: 0 };
212
+
213
+ for (const para of paragraphs) {
214
+ if (para.text.length > MAX_CHARS) {
215
+ if (chunk.texts.length > 0) {
216
+ chunks.push({ text: chunk.texts.join('\n\n'), startLine: chunk.startLine, endLine: chunk.endLine });
217
+ chunk = { texts: [], startLine: 0, endLine: 0, charCount: 0 };
218
+ }
219
+ const sentences = para.text.match(/[^.!?\n]+[.!?\n]+|[^.!?\n]+$/g) || [para.text];
220
+ let sentChunk = [], sentCharCount = 0;
221
+ for (const sent of sentences) {
222
+ if (sentCharCount + sent.length > MAX_CHARS && sentChunk.length > 0) {
223
+ chunks.push({ text: sentChunk.join(' '), startLine: para.startLine, endLine: para.endLine });
224
+ sentChunk = sentChunk.slice(-2);
225
+ sentCharCount = sentChunk.join(' ').length;
226
+ }
227
+ sentChunk.push(sent.trim());
228
+ sentCharCount += sent.length;
229
+ }
230
+ if (sentChunk.length > 0) {
231
+ chunks.push({ text: sentChunk.join(' '), startLine: para.startLine, endLine: para.endLine });
232
+ }
233
+ } else {
234
+ if (chunk.charCount + para.text.length > MAX_CHARS && chunk.texts.length > 0) {
235
+ chunks.push({ text: chunk.texts.join('\n\n'), startLine: chunk.startLine, endLine: chunk.endLine });
236
+ const lastText = chunk.texts[chunk.texts.length - 1];
237
+ chunk = { texts: [lastText], startLine: chunk.startLine, endLine: chunk.endLine, charCount: lastText.length };
238
+ }
239
+ if (chunk.texts.length === 0) chunk.startLine = para.startLine;
240
+ chunk.texts.push(para.text);
241
+ chunk.endLine = para.endLine;
242
+ chunk.charCount += para.text.length;
243
+ }
244
+ }
245
+ if (chunk.texts.length > 0) {
246
+ chunks.push({ text: chunk.texts.join('\n\n'), startLine: chunk.startLine, endLine: chunk.endLine });
247
+ }
248
+
249
+ return chunks;
250
+ }
251
+
252
+ // ========== INDEXING ==========
253
+
254
+ async function indexFile(filePath) {
255
+ if (!db) return 0;
256
+ try {
257
+ if (!fs.existsSync(filePath)) return 0;
258
+ const stat = fs.statSync(filePath);
259
+ const mtime = stat.mtimeMs;
260
+
261
+ const existing = db.prepare('SELECT file_mtime FROM chunks WHERE file_path = ? LIMIT 1').get(filePath);
262
+ if (existing && existing.file_mtime === mtime) return 0;
263
+
264
+ const text = fs.readFileSync(filePath, 'utf8');
265
+ if (!text.trim()) return 0;
266
+
267
+ const chunks = chunkMarkdown(text);
268
+ if (chunks.length === 0) return 0;
269
+
270
+ // Try embeddings, fall back to null
271
+ let embeddings;
272
+ try {
273
+ embeddings = await getEmbeddingsBatch(chunks.map(c => c.text));
274
+ } catch (e) {
275
+ embeddings = chunks.map(() => null);
276
+ }
277
+
278
+ db.prepare('DELETE FROM chunks WHERE file_path = ?').run(filePath);
279
+
280
+ const insert = db.prepare('INSERT INTO chunks (file_path, file_mtime, chunk_index, start_line, end_line, text, embedding) VALUES (?, ?, ?, ?, ?, ?, ?)');
281
+ const insertMany = db.transaction((items) => { for (const item of items) insert.run(...item); });
282
+
283
+ insertMany(chunks.map((c, i) => [
284
+ filePath, mtime, i, c.startLine, c.endLine, c.text,
285
+ embeddings[i] ? Buffer.from(embeddings[i].buffer) : null
286
+ ]));
287
+
288
+ return chunks.length;
289
+ } catch (e) {
290
+ console.error(`[knowledge-base] Error indexing ${filePath}:`, e.message);
291
+ return 0;
292
+ }
293
+ }
294
+
295
+ function findMdFiles(dir) {
296
+ const files = [];
297
+ if (!fs.existsSync(dir)) return files;
298
+ const stat = fs.statSync(dir);
299
+ if (stat.isFile() && dir.endsWith('.md')) return [dir];
300
+ if (!stat.isDirectory()) return files;
301
+ for (const entry of fs.readdirSync(dir)) {
302
+ if (entry.startsWith('.') || entry === 'node_modules') continue;
303
+ const full = path.join(dir, entry);
304
+ const s = fs.statSync(full);
305
+ if (s.isDirectory()) files.push(...findMdFiles(full));
306
+ else if (entry.endsWith('.md')) files.push(full);
307
+ }
308
+ return files;
309
+ }
310
+
311
+ async function indexAllFiles() {
312
+ if (!db) return { filesIndexed: 0, chunksCreated: 0 };
313
+ let filesIndexed = 0, chunksCreated = 0;
314
+ const allFiles = [];
315
+ for (const wp of WATCHED_PATHS) allFiles.push(...findMdFiles(wp));
316
+ for (const f of allFiles) {
317
+ const n = await indexFile(f);
318
+ if (n > 0) { filesIndexed++; chunksCreated += n; }
319
+ }
320
+ lastIndexedAt = new Date().toISOString();
321
+ console.log(`[knowledge-base] Indexed ${filesIndexed} files, ${chunksCreated} chunks`);
322
+ return { filesIndexed, chunksCreated };
323
+ }
324
+
325
+ // ========== SEARCH ==========
326
+
327
+ async function search(query, limit = 5) {
328
+ if (!db) return [];
329
+ const allChunks = db.prepare('SELECT id, file_path, start_line, end_line, text, embedding FROM chunks').all();
330
+ if (allChunks.length === 0) return [];
331
+
332
+ // Check if we have embeddings and an embedding API
333
+ const hasEmbeddings = allChunks.some(c => c.embedding != null);
334
+
335
+ if (hasEmbeddings && EMBEDDING_URL) {
336
+ try {
337
+ const queryEmb = await getEmbedding(query);
338
+ if (queryEmb) {
339
+ const scored = allChunks.filter(r => r.embedding).map(row => {
340
+ const emb = new Float32Array(new Uint8Array(row.embedding).buffer);
341
+ return { filePath: row.file_path, startLine: row.start_line, endLine: row.end_line, text: row.text, score: cosineSimilarity(queryEmb, emb) };
342
+ });
343
+ scored.sort((a, b) => b.score - a.score);
344
+ return scored.slice(0, limit);
345
+ }
346
+ } catch (e) {
347
+ console.error('[knowledge-base] Embedding search failed, falling back to BM25:', e.message);
348
+ }
349
+ }
350
+
351
+ // BM25 fallback
352
+ return bm25Search(query, allChunks, limit);
353
+ }
354
+
355
+ // ========== STATUS ==========
356
+
357
+ function getStatus() {
358
+ if (!db) return { totalFiles: 0, totalChunks: 0, lastIndexedAt: null, watchedPaths: WATCHED_PATHS, searchMode: 'offline' };
359
+ const totalChunks = db.prepare('SELECT COUNT(*) as c FROM chunks').get().c;
360
+ const totalFiles = db.prepare('SELECT COUNT(DISTINCT file_path) as c FROM chunks').get().c;
361
+ const hasEmbeddings = db.prepare('SELECT COUNT(*) as c FROM chunks WHERE embedding IS NOT NULL').get().c > 0;
362
+ return {
363
+ totalFiles, totalChunks, lastIndexedAt, watchedPaths: WATCHED_PATHS,
364
+ searchMode: hasEmbeddings && EMBEDDING_URL ? 'vector' : 'bm25'
365
+ };
366
+ }
367
+
368
+ // ========== FILE WATCHER ==========
369
+
370
+ function startWatcher() {
371
+ for (const wp of WATCHED_PATHS) {
372
+ if (!fs.existsSync(wp)) continue;
373
+ try {
374
+ const isDir = fs.statSync(wp).isDirectory();
375
+ const watcher = fs.watch(wp, { recursive: isDir }, (eventType, filename) => {
376
+ if (!filename) return;
377
+ if (!filename.endsWith('.md')) return;
378
+ const fullPath = isDir ? path.join(wp, filename) : wp;
379
+
380
+ if (debounceTimers[fullPath]) clearTimeout(debounceTimers[fullPath]);
381
+ debounceTimers[fullPath] = setTimeout(async () => {
382
+ delete debounceTimers[fullPath];
383
+ if (fs.existsSync(fullPath)) {
384
+ const n = await indexFile(fullPath);
385
+ if (n > 0) console.log(`[knowledge-base] Re-indexed ${path.basename(fullPath)} (${n} chunks)`);
386
+ }
387
+ }, 2000);
388
+ });
389
+ watchers.push(watcher);
390
+ } catch (e) {
391
+ console.error(`[knowledge-base] Could not watch ${wp}:`, e.message);
392
+ }
393
+ }
394
+ console.log(`[knowledge-base] Watching ${watchers.length} paths`);
395
+ }
396
+
397
+ // ========== ROUTES ==========
398
+
399
+ function setupRoutes(app) {
400
+ app.get('/api/knowledge/search', async (req, res) => {
401
+ try {
402
+ const q = req.query.q;
403
+ if (!q) return res.status(400).json({ error: 'Missing q parameter' });
404
+ const limit = parseInt(req.query.limit) || 5;
405
+ const results = await search(q, limit);
406
+ res.json({ results });
407
+ } catch (e) {
408
+ res.status(500).json({ error: e.message });
409
+ }
410
+ });
411
+
412
+ app.post('/api/knowledge/reindex', async (req, res) => {
413
+ try {
414
+ db.prepare('DELETE FROM chunks').run();
415
+ const result = await indexAllFiles();
416
+ res.json({ success: true, ...result });
417
+ } catch (e) {
418
+ res.status(500).json({ error: e.message });
419
+ }
420
+ });
421
+
422
+ app.get('/api/knowledge/status', (req, res) => {
423
+ res.json(getStatus());
424
+ });
425
+ }
426
+
427
+ // Index arbitrary text (e.g., voice note transcripts) into the knowledge base
428
+ function indexText(id, text, metadata = {}) {
429
+ if (!db) initKnowledgeBase();
430
+ const filePath = id; // e.g. "voice-note:uuid"
431
+ const mtime = Date.now();
432
+
433
+ // Delete existing chunks for this id
434
+ db.prepare('DELETE FROM chunks WHERE file_path = ?').run(filePath);
435
+
436
+ // Split text into chunks
437
+ const lines = text.split('\n');
438
+ const chunkSize = 500; // chars per chunk
439
+ let chunks = [];
440
+ let current = '';
441
+ let startLine = 1;
442
+ let lineNum = 1;
443
+
444
+ for (const line of lines) {
445
+ if (current.length + line.length > chunkSize && current.length > 0) {
446
+ chunks.push({ text: current.trim(), startLine, endLine: lineNum - 1 });
447
+ current = '';
448
+ startLine = lineNum;
449
+ }
450
+ current += line + '\n';
451
+ lineNum++;
452
+ }
453
+ if (current.trim()) {
454
+ chunks.push({ text: current.trim(), startLine, endLine: lineNum - 1 });
455
+ }
456
+
457
+ // Prepend metadata to first chunk for context
458
+ if (chunks.length > 0 && metadata.title) {
459
+ chunks[0].text = `[Voice Note: ${metadata.title}] ${metadata.tags?.length ? 'Tags: ' + metadata.tags.join(', ') + '. ' : ''}${chunks[0].text}`;
460
+ }
461
+
462
+ const insert = db.prepare('INSERT INTO chunks (file_path, file_mtime, chunk_index, start_line, end_line, text, embedding) VALUES (?, ?, ?, ?, ?, ?, ?)');
463
+ const emptyEmbedding = Buffer.alloc(0);
464
+ chunks.forEach((chunk, i) => {
465
+ insert.run(filePath, mtime, i, chunk.startLine, chunk.endLine, chunk.text, emptyEmbedding);
466
+ });
467
+
468
+ console.log(`[knowledge-base] Indexed text "${id}": ${chunks.length} chunks`);
469
+ return { chunks: chunks.length };
470
+ }
471
+
472
+ module.exports = { initKnowledgeBase, indexAllFiles, search, getStatus, startWatcher, setupRoutes, indexText };
package/lib/cli.js ADDED
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const pc = require('picocolors');
5
+
6
+ function update() {
7
+ console.log(pc.cyan('Checking for updates...'));
8
+ try {
9
+ execSync('npm update -g @conversionpros/aiva', { stdio: 'inherit' });
10
+ console.log(pc.green('\nUpdate complete. Restarting AIVA...'));
11
+ const { restartServer } = require('./process');
12
+ restartServer();
13
+ } catch (e) {
14
+ console.error(pc.red('Update failed:'), e.message);
15
+ process.exit(1);
16
+ }
17
+ }
18
+
19
+ module.exports = { update };
package/lib/config.js ADDED
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const pc = require('picocolors');
6
+
7
+ const AIVA_HOME = path.join(process.env.HOME || require('os').homedir(), '.aiva');
8
+
9
+ const DEFAULT_CONFIG = {
10
+ name: 'AIVA',
11
+ owner: '',
12
+ port: 3847,
13
+ gateway: {
14
+ url: 'http://localhost:3033',
15
+ password: ''
16
+ },
17
+ autoStart: true,
18
+ installedAt: new Date().toISOString(),
19
+ client: {
20
+ fullName: '',
21
+ nickname: '',
22
+ pronouns: 'they/them',
23
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
24
+ email: '',
25
+ phone: '',
26
+ business: '',
27
+ businessDescription: ''
28
+ },
29
+ personality: {
30
+ name: 'AIVA',
31
+ style: 'Direct & efficient',
32
+ emoji: 'lightning bolt'
33
+ }
34
+ };
35
+
36
+ function getConfigPath() {
37
+ return path.join(AIVA_HOME, 'config.json');
38
+ }
39
+
40
+ function getDataDir() {
41
+ return path.join(AIVA_HOME, 'data');
42
+ }
43
+
44
+ function getUploadsDir() {
45
+ return path.join(AIVA_HOME, 'uploads');
46
+ }
47
+
48
+ function getLogsDir() {
49
+ return path.join(AIVA_HOME, 'logs');
50
+ }
51
+
52
+ function getWorkspaceDir() {
53
+ return path.join(AIVA_HOME, 'workspace');
54
+ }
55
+
56
+ function getAivaHome() {
57
+ return AIVA_HOME;
58
+ }
59
+
60
+ function loadConfig() {
61
+ const configPath = getConfigPath();
62
+ if (!fs.existsSync(configPath)) {
63
+ return { ...DEFAULT_CONFIG };
64
+ }
65
+ try {
66
+ const raw = fs.readFileSync(configPath, 'utf8');
67
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
68
+ } catch (e) {
69
+ console.error(pc.yellow('Warning: Could not parse config file, using defaults'));
70
+ return { ...DEFAULT_CONFIG };
71
+ }
72
+ }
73
+
74
+ function saveConfig(config) {
75
+ const configPath = getConfigPath();
76
+ const dir = path.dirname(configPath);
77
+ if (!fs.existsSync(dir)) {
78
+ fs.mkdirSync(dir, { recursive: true });
79
+ }
80
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
81
+ }
82
+
83
+ function ensureDirectories() {
84
+ const dirs = [AIVA_HOME, getDataDir(), getUploadsDir(), getLogsDir(), getWorkspaceDir()];
85
+ for (const dir of dirs) {
86
+ if (!fs.existsSync(dir)) {
87
+ fs.mkdirSync(dir, { recursive: true });
88
+ }
89
+ }
90
+ }
91
+
92
+ function printConfig() {
93
+ const config = loadConfig();
94
+ console.log(pc.bold('\nAIVA Configuration'));
95
+ console.log(pc.dim('─'.repeat(40)));
96
+ console.log(` Name: ${pc.cyan(config.name)}`);
97
+ console.log(` Owner: ${config.client?.nickname || config.owner || 'Not set'}`);
98
+ console.log(` Port: ${config.port}`);
99
+ console.log(` Gateway: ${config.gateway?.url || 'Not configured'}`);
100
+ console.log(` Auto-start: ${config.autoStart ? pc.green('Yes') : pc.red('No')}`);
101
+ console.log(` Installed: ${config.installedAt || 'Unknown'}`);
102
+ console.log(pc.dim('─'.repeat(40)));
103
+ console.log(` Config: ${getConfigPath()}`);
104
+ console.log(` Data: ${getDataDir()}`);
105
+ console.log(` Uploads: ${getUploadsDir()}`);
106
+ console.log(` Logs: ${getLogsDir()}`);
107
+ console.log(` Workspace: ${getWorkspaceDir()}`);
108
+ console.log();
109
+ }
110
+
111
+ module.exports = {
112
+ loadConfig,
113
+ saveConfig,
114
+ getConfigPath,
115
+ getDataDir,
116
+ getUploadsDir,
117
+ getLogsDir,
118
+ getWorkspaceDir,
119
+ getAivaHome,
120
+ ensureDirectories,
121
+ printConfig,
122
+ DEFAULT_CONFIG,
123
+ AIVA_HOME
124
+ };
package/lib/health.js ADDED
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const pc = require('picocolors');
5
+ const { loadConfig } = require('./config');
6
+
7
+ function checkHealth(port, timeout = 5000) {
8
+ return new Promise((resolve) => {
9
+ const req = http.get(`http://localhost:${port}/`, (res) => {
10
+ resolve({ ok: res.statusCode < 400, status: res.statusCode });
11
+ });
12
+ req.on('error', () => resolve({ ok: false, status: null }));
13
+ req.setTimeout(timeout, () => {
14
+ req.destroy();
15
+ resolve({ ok: false, status: 'timeout' });
16
+ });
17
+ });
18
+ }
19
+
20
+ async function checkGateway(url, timeout = 5000) {
21
+ return new Promise((resolve) => {
22
+ const mod = url.startsWith('https') ? require('https') : http;
23
+ const req = mod.get(url, (res) => {
24
+ resolve({ ok: res.statusCode < 400, status: res.statusCode });
25
+ });
26
+ req.on('error', () => resolve({ ok: false, status: null }));
27
+ req.setTimeout(timeout, () => {
28
+ req.destroy();
29
+ resolve({ ok: false, status: 'timeout' });
30
+ });
31
+ });
32
+ }
33
+
34
+ async function runHealthCheck() {
35
+ const config = loadConfig();
36
+ const port = config.port || 3847;
37
+
38
+ console.log(pc.bold('\nHealth Check'));
39
+ console.log(pc.dim('─'.repeat(40)));
40
+
41
+ // Server check
42
+ const server = await checkHealth(port);
43
+ console.log(` Server (port ${port}): ${server.ok ? pc.green('OK') : pc.red('UNREACHABLE')}`);
44
+
45
+ // Gateway check
46
+ if (config.gateway?.url) {
47
+ const gw = await checkGateway(config.gateway.url);
48
+ console.log(` Gateway: ${gw.ok ? pc.green('OK') : pc.red('UNREACHABLE')}`);
49
+ } else {
50
+ console.log(` Gateway: ${pc.yellow('Not configured')}`);
51
+ }
52
+
53
+ console.log(pc.dim('─'.repeat(40)));
54
+ return server.ok;
55
+ }
56
+
57
+ module.exports = { checkHealth, checkGateway, runHealthCheck };