@conversionpros/aiva 1.0.0 → 2.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 (150) hide show
  1. package/bin/aiva.js +26 -14
  2. package/lib/bluebubbles.js +145 -0
  3. package/lib/config-gen.js +253 -0
  4. package/lib/config.js +1 -1
  5. package/lib/constants.js +72 -0
  6. package/lib/launch-agent.js +112 -0
  7. package/lib/prerequisites.js +236 -0
  8. package/lib/process.js +59 -145
  9. package/lib/setup.js +224 -194
  10. package/lib/validate.js +194 -0
  11. package/package.json +7 -32
  12. package/auto-deploy.js +0 -190
  13. package/cli-sync.js +0 -126
  14. package/d2a-prompt-template.txt +0 -106
  15. package/diagnostics-api.js +0 -304
  16. package/docs/ara-dedup-fix-scope.md +0 -112
  17. package/docs/ara-fix-round2-scope.md +0 -61
  18. package/docs/ara-greeting-fix-scope.md +0 -70
  19. package/docs/calendar-date-fix-scope.md +0 -28
  20. package/docs/getting-started.md +0 -115
  21. package/docs/network-architecture-rollout-scope.md +0 -43
  22. package/docs/scope-google-oauth-integration.md +0 -351
  23. package/docs/settings-page-scope.md +0 -50
  24. package/docs/xai-imagine-scope.md +0 -116
  25. package/docs/xai-voice-integration-scope.md +0 -115
  26. package/docs/xai-voice-tools-scope.md +0 -165
  27. package/email-router.js +0 -512
  28. package/follow-up-handler.js +0 -606
  29. package/gateway-monitor.js +0 -158
  30. package/google-email.js +0 -379
  31. package/google-oauth.js +0 -310
  32. package/grok-imagine.js +0 -97
  33. package/health-reporter.js +0 -287
  34. package/invisible-prefix-base.txt +0 -206
  35. package/invisible-prefix-owner.txt +0 -26
  36. package/invisible-prefix-slim.txt +0 -10
  37. package/invisible-prefix.txt +0 -43
  38. package/knowledge-base.js +0 -472
  39. package/lib/cli.js +0 -19
  40. package/lib/server.js +0 -42
  41. package/meta-capi.js +0 -206
  42. package/meta-leads.js +0 -411
  43. package/notion-oauth.js +0 -323
  44. package/public/agent-config.html +0 -241
  45. package/public/aiva-avatar-anime.png +0 -0
  46. package/public/css/docs.css.bak +0 -688
  47. package/public/css/onboarding.css +0 -543
  48. package/public/diagrams/claude-subscription-pool.html +0 -329
  49. package/public/diagrams/claude-subscription-pool.png +0 -0
  50. package/public/docs-icon.png +0 -0
  51. package/public/escalation.html +0 -237
  52. package/public/group-config.html +0 -300
  53. package/public/icon-192.png +0 -0
  54. package/public/icon-512.png +0 -0
  55. package/public/icons/agents.svg +0 -1
  56. package/public/icons/attach.svg +0 -1
  57. package/public/icons/characters.svg +0 -1
  58. package/public/icons/chat.svg +0 -1
  59. package/public/icons/docs.svg +0 -1
  60. package/public/icons/heartbeat.svg +0 -1
  61. package/public/icons/messages.svg +0 -1
  62. package/public/icons/mic.svg +0 -1
  63. package/public/icons/notes.svg +0 -1
  64. package/public/icons/settings.svg +0 -1
  65. package/public/icons/tasks.svg +0 -1
  66. package/public/images/onboarding/p0-communication-layer.png +0 -0
  67. package/public/images/onboarding/p0-infinite-surface.png +0 -0
  68. package/public/images/onboarding/p0-learning-model.png +0 -0
  69. package/public/images/onboarding/p0-meet-aiva.png +0 -0
  70. package/public/images/onboarding/p4-contact-intelligence.png +0 -0
  71. package/public/images/onboarding/p4-context-compounds.png +0 -0
  72. package/public/images/onboarding/p4-message-router.png +0 -0
  73. package/public/images/onboarding/p4-per-contact-rules.png +0 -0
  74. package/public/images/onboarding/p4-send-messages.png +0 -0
  75. package/public/images/onboarding/p6-be-precise.png +0 -0
  76. package/public/images/onboarding/p6-review-escalations.png +0 -0
  77. package/public/images/onboarding/p6-voice-input.png +0 -0
  78. package/public/images/onboarding/p7-completion.png +0 -0
  79. package/public/index.html +0 -11594
  80. package/public/js/onboarding.js +0 -699
  81. package/public/manifest.json +0 -24
  82. package/public/messages-v2.html +0 -2824
  83. package/public/permission-approve.html.bak +0 -107
  84. package/public/permissions.html +0 -150
  85. package/public/styles/design-system.css +0 -68
  86. package/router-db.js +0 -604
  87. package/router-utils.js +0 -28
  88. package/router-v2/adapters/imessage.js +0 -191
  89. package/router-v2/adapters/quo.js +0 -82
  90. package/router-v2/adapters/whatsapp.js +0 -192
  91. package/router-v2/contact-manager.js +0 -234
  92. package/router-v2/conversation-engine.js +0 -498
  93. package/router-v2/data/knowledge-base.json +0 -176
  94. package/router-v2/data/router-v2.db +0 -0
  95. package/router-v2/data/router-v2.db-shm +0 -0
  96. package/router-v2/data/router-v2.db-wal +0 -0
  97. package/router-v2/data/router.db +0 -0
  98. package/router-v2/db.js +0 -457
  99. package/router-v2/escalation-bridge.js +0 -540
  100. package/router-v2/follow-up-engine.js +0 -347
  101. package/router-v2/index.js +0 -441
  102. package/router-v2/ingestion.js +0 -213
  103. package/router-v2/knowledge-base.js +0 -231
  104. package/router-v2/lead-qualifier.js +0 -152
  105. package/router-v2/learning-loop.js +0 -202
  106. package/router-v2/outbound-sender.js +0 -160
  107. package/router-v2/package.json +0 -13
  108. package/router-v2/permission-gate.js +0 -86
  109. package/router-v2/playbook.js +0 -177
  110. package/router-v2/prompts/base.js +0 -52
  111. package/router-v2/prompts/first-contact.js +0 -38
  112. package/router-v2/prompts/lead-qualification.js +0 -37
  113. package/router-v2/prompts/scheduling.js +0 -72
  114. package/router-v2/prompts/style-overrides.js +0 -22
  115. package/router-v2/scheduler.js +0 -301
  116. package/router-v2/scripts/migrate-v1-to-v2.js +0 -215
  117. package/router-v2/scripts/seed-faq.js +0 -67
  118. package/router-v2/seed-knowledge-base.js +0 -39
  119. package/router-v2/utils/ai.js +0 -129
  120. package/router-v2/utils/phone.js +0 -52
  121. package/router-v2/utils/response-validator.js +0 -98
  122. package/router-v2/utils/sanitize.js +0 -222
  123. package/router.js +0 -5005
  124. package/routes/google-calendar.js +0 -186
  125. package/scripts/deploy.sh +0 -62
  126. package/scripts/macos-calendar.sh +0 -232
  127. package/scripts/onboard-device.sh +0 -466
  128. package/server.js +0 -5131
  129. package/start.sh +0 -24
  130. package/templates/AGENTS.md +0 -548
  131. package/templates/IDENTITY.md +0 -15
  132. package/templates/docs-agents.html +0 -132
  133. package/templates/docs-app.html +0 -130
  134. package/templates/docs-home.html +0 -83
  135. package/templates/docs-imessage.html +0 -121
  136. package/templates/docs-tasks.html +0 -123
  137. package/templates/docs-tips.html +0 -175
  138. package/templates/getting-started.html +0 -809
  139. package/templates/invisible-prefix-base.txt +0 -171
  140. package/templates/invisible-prefix-owner.txt +0 -282
  141. package/templates/invisible-prefix.txt +0 -338
  142. package/templates/manifest.json +0 -61
  143. package/templates/memory-org/clients.md +0 -7
  144. package/templates/memory-org/credentials.md +0 -9
  145. package/templates/memory-org/devices.md +0 -7
  146. package/templates/updates.html +0 -464
  147. package/tts-proxy.js +0 -96
  148. package/voice-call-local.js +0 -731
  149. package/voice-call.js +0 -732
  150. package/wa-listener.js +0 -354
package/notion-oauth.js DELETED
@@ -1,323 +0,0 @@
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 };
@@ -1,241 +0,0 @@
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