@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,323 @@
1
+ /**
2
+ * Notion OAuth Integration
3
+ * Handles OAuth flow, token management, and Notion API proxy.
4
+ *
5
+ * Endpoints:
6
+ * GET /api/integrations/notion/auth-url - Generate OAuth URL
7
+ * GET /api/integrations/notion/callback - Handle OAuth callback
8
+ * GET /api/integrations/notion/status - List connected workspaces
9
+ * DELETE /api/integrations/notion/disconnect - Disconnect a workspace
10
+ * GET /api/integrations/notion/search - Proxy search
11
+ * GET /api/integrations/notion/pages/:pageId - Get page content
12
+ * POST /api/integrations/notion/pages - Create page
13
+ */
14
+
15
+ const express = require('express');
16
+ const crypto = require('crypto');
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const { v4: uuidv4 } = require('uuid');
21
+
22
+ const router = express.Router();
23
+
24
+ // ─── Config ───────────────────────────────────────────────────────────────────
25
+
26
+ const NOTION_CLIENT_ID = process.env.NOTION_CLIENT_ID || '315d872b-594c-8176-9040-00378fe5ec21';
27
+ const NOTION_CLIENT_SECRET = process.env.NOTION_CLIENT_SECRET || 'secret_tmzIhJEbTFsM6EmeXQ6oX5IFFPLICS5XPYTx19ZqwVM';
28
+ const NOTION_REDIRECT_URI = process.env.NOTION_REDIRECT_URI || 'https://app.aivahelpme.com/api/integrations/notion/callback';
29
+ const TOKENS_FILE = path.join(__dirname, 'data', 'oauth-tokens.json');
30
+ const NOTION_API_VERSION = '2022-06-28';
31
+
32
+ // In-memory CSRF state store (5 min TTL)
33
+ const pendingStates = new Map();
34
+
35
+ // ─── Encryption (matches google-oauth.js) ─────────────────────────────────────
36
+
37
+ function getEncryptionKey() {
38
+ return crypto.createHash('sha256').update('aiva-tokens-' + os.hostname()).digest();
39
+ }
40
+
41
+ function encrypt(text) {
42
+ const key = getEncryptionKey();
43
+ const iv = crypto.randomBytes(12);
44
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
45
+ let enc = cipher.update(text, 'utf8', 'hex');
46
+ enc += cipher.final('hex');
47
+ const tag = cipher.getAuthTag().toString('hex');
48
+ return `${iv.toString('hex')}:${tag}:${enc}`;
49
+ }
50
+
51
+ function decrypt(stored) {
52
+ const key = getEncryptionKey();
53
+ const [ivHex, tagHex, enc] = stored.split(':');
54
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(ivHex, 'hex'));
55
+ decipher.setAuthTag(Buffer.from(tagHex, 'hex'));
56
+ let dec = decipher.update(enc, 'hex', 'utf8');
57
+ dec += decipher.final('utf8');
58
+ return dec;
59
+ }
60
+
61
+ // ─── Token Storage ────────────────────────────────────────────────────────────
62
+
63
+ function loadTokens() {
64
+ try {
65
+ if (fs.existsSync(TOKENS_FILE)) {
66
+ return JSON.parse(fs.readFileSync(TOKENS_FILE, 'utf8'));
67
+ }
68
+ } catch (e) {
69
+ console.error('[notion-oauth] Failed to load tokens:', e.message);
70
+ }
71
+ return { google: { accounts: [] }, notion: { accounts: [] } };
72
+ }
73
+
74
+ function saveTokens(data) {
75
+ const dir = path.dirname(TOKENS_FILE);
76
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
77
+ fs.writeFileSync(TOKENS_FILE, JSON.stringify(data, null, 2));
78
+ }
79
+
80
+ // ─── Helper: get token for an account ─────────────────────────────────────────
81
+
82
+ function getNotionToken(accountId) {
83
+ const store = loadTokens();
84
+ if (!store.notion) return null;
85
+ const account = accountId
86
+ ? store.notion.accounts.find(a => a.id === accountId)
87
+ : store.notion.accounts[0];
88
+ if (!account) return null;
89
+ return {
90
+ token: decrypt(account.access_token),
91
+ workspace_name: account.workspace_name,
92
+ workspace_id: account.workspace_id,
93
+ bot_id: account.bot_id,
94
+ };
95
+ }
96
+
97
+ // ─── Routes ───────────────────────────────────────────────────────────────────
98
+
99
+ // Generate OAuth authorization URL
100
+ router.get('/auth-url', (req, res) => {
101
+ if (!NOTION_CLIENT_ID || !NOTION_CLIENT_SECRET) {
102
+ return res.json({ success: false, error: 'Notion OAuth not configured. Set NOTION_CLIENT_ID and NOTION_CLIENT_SECRET.' });
103
+ }
104
+
105
+ const state = crypto.randomBytes(32).toString('hex');
106
+ pendingStates.set(state, { createdAt: Date.now() });
107
+ setTimeout(() => pendingStates.delete(state), 5 * 60 * 1000);
108
+
109
+ const params = new URLSearchParams({
110
+ client_id: NOTION_CLIENT_ID,
111
+ response_type: 'code',
112
+ owner: 'user',
113
+ redirect_uri: NOTION_REDIRECT_URI,
114
+ state,
115
+ });
116
+
117
+ const url = `https://api.notion.com/v1/oauth/authorize?${params}`;
118
+ res.json({ success: true, data: { url } });
119
+ });
120
+
121
+ // OAuth callback
122
+ router.get('/callback', async (req, res) => {
123
+ const { code, state, error } = req.query;
124
+
125
+ if (error) {
126
+ return res.redirect('/#settings?notion_oauth=error&message=' + encodeURIComponent(error));
127
+ }
128
+
129
+ if (!state || !pendingStates.has(state)) {
130
+ return res.redirect('/#settings?notion_oauth=error&message=Invalid+state+token');
131
+ }
132
+
133
+ pendingStates.delete(state);
134
+
135
+ try {
136
+ // Exchange code for token (Basic auth with client_id:client_secret)
137
+ const basicAuth = Buffer.from(`${NOTION_CLIENT_ID}:${NOTION_CLIENT_SECRET}`).toString('base64');
138
+ const tokenResp = await fetch('https://api.notion.com/v1/oauth/token', {
139
+ method: 'POST',
140
+ headers: {
141
+ 'Content-Type': 'application/json',
142
+ 'Authorization': `Basic ${basicAuth}`,
143
+ },
144
+ body: JSON.stringify({
145
+ grant_type: 'authorization_code',
146
+ code,
147
+ redirect_uri: NOTION_REDIRECT_URI,
148
+ }),
149
+ });
150
+
151
+ if (!tokenResp.ok) {
152
+ const err = await tokenResp.text();
153
+ console.error('[notion-oauth] Token exchange failed:', err);
154
+ return res.redirect('/#settings?notion_oauth=error&message=Token+exchange+failed');
155
+ }
156
+
157
+ const tokens = await tokenResp.json();
158
+
159
+ // Store workspace
160
+ const store = loadTokens();
161
+ if (!store.notion) store.notion = { accounts: [] };
162
+
163
+ // Check if this workspace is already connected - update if so
164
+ const existingIdx = store.notion.accounts.findIndex(a => a.workspace_id === tokens.workspace_id);
165
+ const account = {
166
+ id: existingIdx >= 0 ? store.notion.accounts[existingIdx].id : uuidv4(),
167
+ workspace_name: tokens.workspace_name || 'Unnamed Workspace',
168
+ workspace_id: tokens.workspace_id,
169
+ bot_id: tokens.bot_id,
170
+ access_token: encrypt(tokens.access_token),
171
+ status: 'connected',
172
+ connected_at: new Date().toISOString(),
173
+ };
174
+
175
+ if (existingIdx >= 0) {
176
+ store.notion.accounts[existingIdx] = account;
177
+ } else {
178
+ store.notion.accounts.push(account);
179
+ }
180
+ saveTokens(store);
181
+
182
+ console.log(`[notion-oauth] Connected workspace: ${tokens.workspace_name} (${tokens.workspace_id})`);
183
+ return res.redirect('/#settings?notion_oauth=success&workspace=' + encodeURIComponent(tokens.workspace_name || ''));
184
+
185
+ } catch (e) {
186
+ console.error('[notion-oauth] Callback error:', e);
187
+ return res.redirect('/#settings?notion_oauth=error&message=' + encodeURIComponent(e.message));
188
+ }
189
+ });
190
+
191
+ // Workspace status
192
+ router.get('/status', (req, res) => {
193
+ const store = loadTokens();
194
+ const accounts = (store.notion?.accounts || []).map(a => ({
195
+ id: a.id,
196
+ workspace_name: a.workspace_name,
197
+ workspace_id: a.workspace_id,
198
+ bot_id: a.bot_id,
199
+ status: a.status || 'connected',
200
+ connected_at: a.connected_at,
201
+ }));
202
+ res.json({ success: true, data: { accounts, configured: !!(NOTION_CLIENT_ID && NOTION_CLIENT_SECRET) } });
203
+ });
204
+
205
+ // Disconnect workspace
206
+ router.delete('/disconnect', (req, res) => {
207
+ const { accountId } = req.body || {};
208
+ if (!accountId) return res.json({ success: false, error: 'accountId required' });
209
+
210
+ const store = loadTokens();
211
+ if (!store.notion) return res.json({ success: false, error: 'No Notion workspaces found' });
212
+
213
+ const idx = store.notion.accounts.findIndex(a => a.id === accountId);
214
+ if (idx < 0) return res.json({ success: false, error: 'Workspace not found' });
215
+
216
+ const account = store.notion.accounts[idx];
217
+ store.notion.accounts.splice(idx, 1);
218
+ saveTokens(store);
219
+
220
+ console.log(`[notion-oauth] Disconnected workspace: ${account.workspace_name}`);
221
+ res.json({ success: true, data: { workspace_name: account.workspace_name } });
222
+ });
223
+
224
+ // Search Notion
225
+ router.get('/search', async (req, res) => {
226
+ const { q, accountId } = req.query;
227
+ const tokenInfo = getNotionToken(accountId);
228
+ if (!tokenInfo) return res.json({ success: false, error: 'No Notion workspace connected' });
229
+
230
+ try {
231
+ const body = {};
232
+ if (q) body.query = q;
233
+
234
+ const resp = await fetch('https://api.notion.com/v1/search', {
235
+ method: 'POST',
236
+ headers: {
237
+ 'Authorization': `Bearer ${tokenInfo.token}`,
238
+ 'Notion-Version': NOTION_API_VERSION,
239
+ 'Content-Type': 'application/json',
240
+ },
241
+ body: JSON.stringify(body),
242
+ });
243
+
244
+ if (!resp.ok) {
245
+ const err = await resp.text();
246
+ console.error('[notion-oauth] Search failed:', err);
247
+ return res.json({ success: false, error: 'Notion search failed' });
248
+ }
249
+
250
+ const data = await resp.json();
251
+ res.json({ success: true, data });
252
+ } catch (e) {
253
+ res.json({ success: false, error: e.message });
254
+ }
255
+ });
256
+
257
+ // Get page content (blocks)
258
+ router.get('/pages/:pageId', async (req, res) => {
259
+ const { accountId } = req.query;
260
+ const tokenInfo = getNotionToken(accountId);
261
+ if (!tokenInfo) return res.json({ success: false, error: 'No Notion workspace connected' });
262
+
263
+ try {
264
+ // Get page properties
265
+ const pageResp = await fetch(`https://api.notion.com/v1/pages/${req.params.pageId}`, {
266
+ headers: {
267
+ 'Authorization': `Bearer ${tokenInfo.token}`,
268
+ 'Notion-Version': NOTION_API_VERSION,
269
+ },
270
+ });
271
+
272
+ // Get page blocks (content)
273
+ const blocksResp = await fetch(`https://api.notion.com/v1/blocks/${req.params.pageId}/children?page_size=100`, {
274
+ headers: {
275
+ 'Authorization': `Bearer ${tokenInfo.token}`,
276
+ 'Notion-Version': NOTION_API_VERSION,
277
+ },
278
+ });
279
+
280
+ if (!pageResp.ok || !blocksResp.ok) {
281
+ return res.json({ success: false, error: 'Failed to fetch page' });
282
+ }
283
+
284
+ const page = await pageResp.json();
285
+ const blocks = await blocksResp.json();
286
+
287
+ res.json({ success: true, data: { page, blocks: blocks.results } });
288
+ } catch (e) {
289
+ res.json({ success: false, error: e.message });
290
+ }
291
+ });
292
+
293
+ // Create page
294
+ router.post('/pages', async (req, res) => {
295
+ const { accountId } = req.query;
296
+ const tokenInfo = getNotionToken(accountId);
297
+ if (!tokenInfo) return res.json({ success: false, error: 'No Notion workspace connected' });
298
+
299
+ try {
300
+ const resp = await fetch('https://api.notion.com/v1/pages', {
301
+ method: 'POST',
302
+ headers: {
303
+ 'Authorization': `Bearer ${tokenInfo.token}`,
304
+ 'Notion-Version': NOTION_API_VERSION,
305
+ 'Content-Type': 'application/json',
306
+ },
307
+ body: JSON.stringify(req.body),
308
+ });
309
+
310
+ if (!resp.ok) {
311
+ const err = await resp.text();
312
+ console.error('[notion-oauth] Create page failed:', err);
313
+ return res.json({ success: false, error: 'Failed to create page' });
314
+ }
315
+
316
+ const data = await resp.json();
317
+ res.json({ success: true, data });
318
+ } catch (e) {
319
+ res.json({ success: false, error: e.message });
320
+ }
321
+ });
322
+
323
+ module.exports = { router, getNotionToken };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@conversionpros/aiva",
3
+ "version": "1.0.0",
4
+ "description": "AIVA - AI Virtual Assistant. Personal AI assistant powered by OpenClaw.",
5
+ "main": "lib/server.js",
6
+ "bin": {
7
+ "aiva": "./bin/aiva.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18.0.0"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "lib/",
15
+ "public/",
16
+ "routes/",
17
+ "templates/",
18
+ "docs/",
19
+ "message-router/",
20
+ "router-v2/",
21
+ "scripts/",
22
+ "*.js",
23
+ "*.txt",
24
+ "*.sh",
25
+ "README.md"
26
+ ],
27
+ "dependencies": {
28
+ "@anthropic-ai/sdk": "^0.74.0",
29
+ "@deepgram/sdk": "^4.11.3",
30
+ "bcryptjs": "^3.0.3",
31
+ "better-sqlite3": "^12.6.2",
32
+ "commander": "^13.1.0",
33
+ "cors": "^2.8.5",
34
+ "dotenv": "^17.3.1",
35
+ "express": "^4.18.2",
36
+ "express-session": "^1.19.0",
37
+ "form-data": "^4.0.5",
38
+ "jsonwebtoken": "^9.0.3",
39
+ "multer": "^2.0.2",
40
+ "node-fetch": "^2.7.0",
41
+ "ora": "^5.4.1",
42
+ "picocolors": "^1.1.1",
43
+ "prompts": "^2.4.2",
44
+ "socket.io": "^4.8.3",
45
+ "ssh2": "^1.17.0",
46
+ "uuid": "^13.0.0",
47
+ "ws": "^8.19.0"
48
+ },
49
+ "keywords": [
50
+ "aiva",
51
+ "ai-assistant",
52
+ "openclaw",
53
+ "virtual-assistant"
54
+ ],
55
+ "author": "Conversion Marketing Pros",
56
+ "license": "UNLICENSED",
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "https://github.com/conversionpros/aiva-cli"
60
+ }
61
+ }
@@ -0,0 +1,241 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <link rel="stylesheet" href="/styles/design-system.css">
7
+ <title>AIVA - Agent Configuration</title>
8
+ <style>
9
+ body{font-family:var(--font-sans);background:var(--bg-secondary);color:var(--text);min-height:100vh;display:flex;flex-direction:column;align-items:center;padding:20px}
10
+ .container{max-width:520px;width:100%}
11
+ .header{text-align:center;margin-bottom:24px;padding:16px 0;border-bottom:1px solid var(--border-dim)}
12
+ .header h1{font-size:18px;color:var(--text-dim);font-weight:500;letter-spacing:1px}
13
+ .header .subtitle{font-size:14px;color:var(--text-dim);margin-top:6px}
14
+ .header .contact-name{font-size:20px;font-weight:600;color:var(--text);margin-top:8px}
15
+ .header .contact-phone{font-size:14px;color:var(--blue);margin-top:2px}
16
+
17
+ .section{background:var(--surface-glass);border:1px solid var(--border-dim);border-radius:var(--radius-lg);padding:var(--space-lg);margin-bottom:var(--space-lg)}
18
+ .section-title{font-size:16px;font-weight:600;color:var(--text);margin-bottom:var(--space-xs)}
19
+ .section-desc{font-size:13px;color:var(--text-dim);margin-bottom:var(--space-lg);line-height:1.4}
20
+
21
+ .presets{display:flex;gap:var(--space-sm);margin-bottom:var(--space-lg)}
22
+ .preset-btn{flex:1;padding:8px 4px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--text-dim);font-size:12px;font-weight:600;cursor:pointer;transition:all .2s;font-family:inherit}
23
+ .preset-btn:hover{border-color:var(--blue);color:var(--blue)}
24
+ .preset-btn.active{border-color:var(--blue);background:#1c2333;color:var(--blue)}
25
+
26
+ .skill-item{display:flex;align-items:flex-start;justify-content:space-between;padding:12px 0;border-bottom:1px solid var(--border-dim)}
27
+ .skill-item:last-child{border-bottom:none}
28
+ .skill-info{flex:1;margin-right:12px}
29
+ .skill-name{font-size:14px;font-weight:600;color:var(--text-secondary)}
30
+ .skill-desc{font-size:12px;color:var(--text-dim);margin-top:2px;line-height:1.4}
31
+
32
+ .toggle{position:relative;width:44px;height:24px;flex-shrink:0;margin-top:2px}
33
+ .toggle input{opacity:0;width:0;height:0}
34
+ .toggle .slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:var(--border);border-radius:24px;transition:.2s}
35
+ .toggle .slider:before{content:'';position:absolute;height:18px;width:18px;left:3px;bottom:3px;background:var(--text-dim);border-radius:50%;transition:.2s}
36
+ .toggle input:checked+.slider{background:var(--green-border)}
37
+ .toggle input:checked+.slider:before{transform:translateX(20px);background:#fff}
38
+
39
+ .warning-banner{background:var(--yellow-bg);border:1px solid var(--yellow-border);border-radius:var(--radius-md);padding:10px 12px;margin-bottom:var(--space-lg);font-size:12px;color:var(--yellow-border);line-height:1.4}
40
+
41
+ .collapsible-header{display:flex;align-items:center;justify-content:space-between;cursor:pointer;user-select:none}
42
+ .collapsible-header .chevron{color:var(--text-dim);font-size:14px;transition:transform .2s}
43
+ .collapsible-header.open .chevron{transform:rotate(180deg)}
44
+ .collapsible-body{display:none;margin-top:var(--space-lg)}
45
+ .collapsible-body.open{display:block}
46
+
47
+ textarea{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-md);padding:12px;font-size:14px;color:var(--text);resize:vertical;font-family:inherit;min-height:100px;transition:border-color .2s}
48
+ textarea:focus{outline:none;border-color:var(--blue)}
49
+ .char-count{text-align:right;font-size:11px;color:var(--text-muted);margin-top:var(--space-xs)}
50
+
51
+ .btn{width:100%;padding:14px;background:var(--green-border);color:#fff;border:none;border-radius:var(--radius-lg);font-size:16px;font-weight:600;cursor:pointer;margin-top:var(--space-sm);transition:background .2s}
52
+ .btn:hover{background:#2ea043}
53
+ .btn:disabled{background:var(--border-dim);color:var(--text-muted);cursor:not-allowed}
54
+
55
+ .status{text-align:center;padding:12px;border-radius:var(--radius-lg);margin-top:var(--space-md);font-size:14px;display:none}
56
+ .status.success{display:block;background:var(--green-bg);border:1px solid var(--green-border);color:var(--green)}
57
+ .status.error{display:block;background:var(--red-bg);border:1px solid var(--red-border);color:var(--red)}
58
+
59
+ .loading{text-align:center;padding:60px;color:var(--text-dim)}
60
+ .loading .spinner{display:inline-block;width:24px;height:24px;border:3px solid var(--border);border-top-color:var(--blue);border-radius:50%;animation:spin .8s linear infinite;margin-bottom:12px}
61
+ @keyframes spin{to{transform:rotate(360deg)}}
62
+
63
+ .error-page{text-align:center;padding:40px 20px}
64
+ .error-page h2{color:var(--red);margin-bottom:12px}
65
+ .error-page p{color:var(--text-dim)}
66
+ </style>
67
+ </head>
68
+ <body>
69
+ <div class="container" id="app">
70
+ <div class="loading" id="loading"><div class="spinner"></div><br>Loading configuration...</div>
71
+ </div>
72
+ <script>
73
+ const phone = new URLSearchParams(window.location.search).get('phone');
74
+ const app = document.getElementById('app');
75
+
76
+ const BASIC_SKILLS = ['search', 'calendar', 'knowledge', 'contacts'];
77
+
78
+ function esc(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
79
+
80
+ if (!phone) {
81
+ app.innerHTML = '<div class="error-page"><h2>Invalid Link</h2><p>Missing phone number parameter.</p></div>';
82
+ } else {
83
+ loadConfig();
84
+ }
85
+
86
+ async function loadConfig() {
87
+ try {
88
+ const res = await fetch(`/api/agent-sessions/${encodeURIComponent(phone)}/config`);
89
+ if (!res.ok) throw new Error(res.status === 404 ? 'Session not found for this contact.' : 'Failed to load configuration.');
90
+ const data = await res.json();
91
+ render(data);
92
+ } catch (e) {
93
+ app.innerHTML = `<div class="error-page"><h2>Error</h2><p>${esc(e.message)}</p></div>`;
94
+ }
95
+ }
96
+
97
+ function render(data) {
98
+ const contactName = data.contactName || 'this contact';
99
+ const skills = data.skills || [];
100
+ const toolPolicies = data.toolPolicies || [];
101
+ const instructions = data.instructions || '';
102
+ const enabledSkills = new Set(data.enabledSkills || []);
103
+ const enabledPolicies = data.enabledPolicies || {};
104
+
105
+ app.innerHTML = `
106
+ <div class="header">
107
+ <h1>AGENT CONFIGURATION</h1>
108
+ <div class="contact-name">${esc(contactName)}</div>
109
+ <div class="contact-phone">${esc(phone)}</div>
110
+ </div>
111
+
112
+ <div class="section">
113
+ <div class="section-title">Agent Skills</div>
114
+ <div class="section-desc">Choose what this agent can do for ${esc(contactName)}</div>
115
+ <div class="presets">
116
+ <button class="preset-btn" onclick="applyPreset('demo')">Demo (none)</button>
117
+ <button class="preset-btn" onclick="applyPreset('basic')">Basic</button>
118
+ <button class="preset-btn" onclick="applyPreset('full')">Full Access</button>
119
+ </div>
120
+ <div id="skills-list">
121
+ ${skills.map(s => `
122
+ <div class="skill-item">
123
+ <div class="skill-info">
124
+ <div class="skill-name">${esc(s.name)}</div>
125
+ <div class="skill-desc">${esc(s.description)}</div>
126
+ </div>
127
+ <label class="toggle">
128
+ <input type="checkbox" data-skill-id="${esc(s.id)}" ${enabledSkills.has(s.id) ? 'checked' : ''} onchange="updatePresetHighlight()">
129
+ <span class="slider"></span>
130
+ </label>
131
+ </div>
132
+ `).join('')}
133
+ </div>
134
+ </div>
135
+
136
+ <div class="section">
137
+ <div class="collapsible-header" onclick="togglePolicies()">
138
+ <div>
139
+ <div class="section-title">Tool Policies</div>
140
+ <div class="section-desc" style="margin-bottom:0">Advanced system-level permissions</div>
141
+ </div>
142
+ <span class="chevron">▼</span>
143
+ </div>
144
+ <div class="collapsible-body" id="policies-body">
145
+ <div class="warning-banner">⚠️ These controls grant low-level system access. Most contacts should not need any of these enabled.</div>
146
+ <div id="policies-list">
147
+ ${toolPolicies.map(p => `
148
+ <div class="skill-item">
149
+ <div class="skill-info">
150
+ <div class="skill-name">${esc(p.name)}</div>
151
+ <div class="skill-desc">${esc(p.description)}</div>
152
+ </div>
153
+ <label class="toggle">
154
+ <input type="checkbox" data-policy-id="${esc(p.id)}" ${enabledPolicies[p.id] ? 'checked' : ''}>
155
+ <span class="slider"></span>
156
+ </label>
157
+ </div>
158
+ `).join('')}
159
+ </div>
160
+ </div>
161
+ </div>
162
+
163
+ <div class="section">
164
+ <div class="section-title">Custom Instructions</div>
165
+ <div class="section-desc">Tell the agent how to behave with this contact</div>
166
+ <textarea id="instructions" maxlength="2000" placeholder="e.g., 'Be friendly and casual. This is my mom, help her with tech questions. Never discuss business details.'" oninput="updateCharCount()">${esc(instructions)}</textarea>
167
+ <div class="char-count" id="char-count">${instructions.length}/2000</div>
168
+ </div>
169
+
170
+ <button class="btn" id="save-btn" onclick="saveConfig()">Save Configuration</button>
171
+ <div class="status" id="status"></div>
172
+ `;
173
+
174
+ window._skillIds = skills.map(s => s.id);
175
+ updatePresetHighlight();
176
+ }
177
+
178
+ window.applyPreset = function(preset) {
179
+ const checkboxes = document.querySelectorAll('[data-skill-id]');
180
+ checkboxes.forEach(cb => {
181
+ if (preset === 'demo') cb.checked = false;
182
+ else if (preset === 'basic') cb.checked = BASIC_SKILLS.includes(cb.dataset.skillId);
183
+ else if (preset === 'full') cb.checked = true;
184
+ });
185
+ updatePresetHighlight();
186
+ };
187
+
188
+ window.updatePresetHighlight = function() {
189
+ const checkboxes = [...document.querySelectorAll('[data-skill-id]')];
190
+ const allOff = checkboxes.every(c => !c.checked);
191
+ const allOn = checkboxes.every(c => c.checked);
192
+ const isBasic = checkboxes.every(c => c.checked === BASIC_SKILLS.includes(c.dataset.skillId));
193
+ document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
194
+ if (allOff) document.querySelectorAll('.preset-btn')[0]?.classList.add('active');
195
+ else if (isBasic) document.querySelectorAll('.preset-btn')[1]?.classList.add('active');
196
+ else if (allOn) document.querySelectorAll('.preset-btn')[2]?.classList.add('active');
197
+ };
198
+
199
+ window.togglePolicies = function() {
200
+ const header = document.querySelector('.collapsible-header');
201
+ const body = document.getElementById('policies-body');
202
+ header.classList.toggle('open');
203
+ body.classList.toggle('open');
204
+ };
205
+
206
+ window.updateCharCount = function() {
207
+ const ta = document.getElementById('instructions');
208
+ document.getElementById('char-count').textContent = `${ta.value.length}/2000`;
209
+ };
210
+
211
+ window.saveConfig = async function() {
212
+ const btn = document.getElementById('save-btn');
213
+ const status = document.getElementById('status');
214
+ status.className = 'status';
215
+ btn.disabled = true;
216
+ btn.textContent = 'Saving...';
217
+
218
+ const skills = [...document.querySelectorAll('[data-skill-id]:checked')].map(c => c.dataset.skillId);
219
+ const tool_policies = {};
220
+ document.querySelectorAll('[data-policy-id]').forEach(c => { tool_policies[c.dataset.policyId] = c.checked; });
221
+ const instructions = document.getElementById('instructions').value.trim();
222
+
223
+ try {
224
+ const res = await fetch(`/api/agent-sessions/${encodeURIComponent(phone)}/config`, {
225
+ method: 'POST',
226
+ headers: { 'Content-Type': 'application/json' },
227
+ body: JSON.stringify({ skills, tool_policies, instructions })
228
+ });
229
+ if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.error || 'Failed to save.'); }
230
+ status.className = 'status success';
231
+ status.textContent = '✓ Configuration saved successfully.';
232
+ } catch (e) {
233
+ status.className = 'status error';
234
+ status.textContent = e.message || 'Network error. Please try again.';
235
+ }
236
+ btn.disabled = false;
237
+ btn.textContent = 'Save Configuration';
238
+ };
239
+ </script>
240
+ </body>
241
+ </html>
Binary file