@conversionpros/aiva 1.0.1 → 2.0.1

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