@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,310 @@
1
+ /**
2
+ * Google OAuth 2.0 Integration - Phase 1
3
+ * Handles OAuth flow, token management, and account status.
4
+ *
5
+ * Endpoints:
6
+ * GET /api/integrations/google/auth-url - Generate OAuth URL
7
+ * GET /api/integrations/google/callback - Handle OAuth callback
8
+ * GET /api/integrations/google/status - List connected accounts
9
+ * DELETE /api/integrations/google/disconnect - Disconnect an account
10
+ */
11
+
12
+ const express = require('express');
13
+ const crypto = require('crypto');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+ const { v4: uuidv4 } = require('uuid');
18
+
19
+ const router = express.Router();
20
+
21
+ // ─── Config ───────────────────────────────────────────────────────────────────
22
+
23
+ const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
24
+ const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
25
+ const GOOGLE_REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3847/api/integrations/google/callback';
26
+ const TOKENS_FILE = path.join(__dirname, 'data', 'oauth-tokens.json');
27
+
28
+ const SCOPES = [
29
+ 'https://www.googleapis.com/auth/gmail.modify',
30
+ 'https://www.googleapis.com/auth/calendar',
31
+ 'https://www.googleapis.com/auth/contacts.readonly',
32
+ 'https://www.googleapis.com/auth/userinfo.email',
33
+ 'https://www.googleapis.com/auth/userinfo.profile',
34
+ ];
35
+
36
+ // In-memory CSRF state store (5 min TTL)
37
+ const pendingStates = new Map();
38
+
39
+ // ─── Encryption (matches existing tokens.json pattern) ────────────────────────
40
+
41
+ function getEncryptionKey() {
42
+ return crypto.createHash('sha256').update('aiva-tokens-' + os.hostname()).digest();
43
+ }
44
+
45
+ function encrypt(text) {
46
+ const key = getEncryptionKey();
47
+ const iv = crypto.randomBytes(12);
48
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
49
+ let enc = cipher.update(text, 'utf8', 'hex');
50
+ enc += cipher.final('hex');
51
+ const tag = cipher.getAuthTag().toString('hex');
52
+ return `${iv.toString('hex')}:${tag}:${enc}`;
53
+ }
54
+
55
+ function decrypt(stored) {
56
+ const key = getEncryptionKey();
57
+ const [ivHex, tagHex, enc] = stored.split(':');
58
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(ivHex, 'hex'));
59
+ decipher.setAuthTag(Buffer.from(tagHex, 'hex'));
60
+ let dec = decipher.update(enc, 'hex', 'utf8');
61
+ dec += decipher.final('utf8');
62
+ return dec;
63
+ }
64
+
65
+ // ─── Token Storage ────────────────────────────────────────────────────────────
66
+
67
+ function loadTokens() {
68
+ try {
69
+ if (fs.existsSync(TOKENS_FILE)) {
70
+ return JSON.parse(fs.readFileSync(TOKENS_FILE, 'utf8'));
71
+ }
72
+ } catch (e) {
73
+ console.error('[google-oauth] Failed to load tokens:', e.message);
74
+ }
75
+ return { google: { accounts: [] } };
76
+ }
77
+
78
+ function saveTokens(data) {
79
+ const dir = path.dirname(TOKENS_FILE);
80
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
81
+ fs.writeFileSync(TOKENS_FILE, JSON.stringify(data, null, 2));
82
+ }
83
+
84
+ // ─── PKCE Helpers ─────────────────────────────────────────────────────────────
85
+
86
+ function generateCodeVerifier() {
87
+ return crypto.randomBytes(64).toString('base64url');
88
+ }
89
+
90
+ function generateCodeChallenge(verifier) {
91
+ return crypto.createHash('sha256').update(verifier).digest('base64url');
92
+ }
93
+
94
+ // ─── Token Refresh ────────────────────────────────────────────────────────────
95
+
96
+ async function refreshAccessToken(account) {
97
+ const refreshToken = decrypt(account.refresh_token);
98
+ const resp = await fetch('https://oauth2.googleapis.com/token', {
99
+ method: 'POST',
100
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
101
+ body: new URLSearchParams({
102
+ client_id: GOOGLE_CLIENT_ID,
103
+ client_secret: GOOGLE_CLIENT_SECRET,
104
+ refresh_token: refreshToken,
105
+ grant_type: 'refresh_token',
106
+ }),
107
+ });
108
+
109
+ if (!resp.ok) {
110
+ const err = await resp.text();
111
+ console.error('[google-oauth] Refresh failed:', err);
112
+ return null;
113
+ }
114
+
115
+ const data = await resp.json();
116
+ account.access_token = encrypt(data.access_token);
117
+ account.token_expiry = new Date(Date.now() + data.expires_in * 1000).toISOString();
118
+ if (data.refresh_token) {
119
+ account.refresh_token = encrypt(data.refresh_token);
120
+ }
121
+ return account;
122
+ }
123
+
124
+ /**
125
+ * Get a valid access token for an account, refreshing if needed.
126
+ * Exported so email/calendar modules can use it.
127
+ */
128
+ async function getValidAccessToken(accountId) {
129
+ const store = loadTokens();
130
+ const account = store.google.accounts.find(a => a.id === accountId);
131
+ if (!account) throw new Error('Account not found');
132
+
133
+ const expiry = new Date(account.token_expiry);
134
+ const fiveMinFromNow = new Date(Date.now() + 5 * 60 * 1000);
135
+
136
+ if (expiry > fiveMinFromNow) {
137
+ return { token: decrypt(account.access_token), email: account.email };
138
+ }
139
+
140
+ // Refresh needed
141
+ const refreshed = await refreshAccessToken(account);
142
+ if (!refreshed) {
143
+ account.status = 'expired';
144
+ saveTokens(store);
145
+ throw new Error('GOOGLE_AUTH_EXPIRED');
146
+ }
147
+ account.status = 'connected';
148
+ saveTokens(store);
149
+ return { token: decrypt(account.access_token), email: account.email };
150
+ }
151
+
152
+ // ─── Routes ───────────────────────────────────────────────────────────────────
153
+
154
+ // Generate OAuth authorization URL
155
+ router.get('/auth-url', (req, res) => {
156
+ if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) {
157
+ return res.json({ success: false, error: 'Google OAuth not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.' });
158
+ }
159
+
160
+ const state = crypto.randomBytes(32).toString('hex');
161
+ const codeVerifier = generateCodeVerifier();
162
+ const codeChallenge = generateCodeChallenge(codeVerifier);
163
+
164
+ // Store state with 5 min TTL
165
+ pendingStates.set(state, { codeVerifier, createdAt: Date.now() });
166
+ setTimeout(() => pendingStates.delete(state), 5 * 60 * 1000);
167
+
168
+ const params = new URLSearchParams({
169
+ client_id: GOOGLE_CLIENT_ID,
170
+ redirect_uri: GOOGLE_REDIRECT_URI,
171
+ response_type: 'code',
172
+ scope: SCOPES.join(' '),
173
+ state,
174
+ code_challenge: codeChallenge,
175
+ code_challenge_method: 'S256',
176
+ access_type: 'offline',
177
+ prompt: 'consent',
178
+ });
179
+
180
+ const url = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
181
+ res.json({ success: true, data: { url } });
182
+ });
183
+
184
+ // OAuth callback
185
+ router.get('/callback', async (req, res) => {
186
+ const { code, state, error } = req.query;
187
+
188
+ if (error) {
189
+ return res.redirect('/#settings?oauth=error&message=' + encodeURIComponent(error));
190
+ }
191
+
192
+ if (!state || !pendingStates.has(state)) {
193
+ return res.redirect('/#settings?oauth=error&message=Invalid+state+token');
194
+ }
195
+
196
+ const { codeVerifier } = pendingStates.get(state);
197
+ pendingStates.delete(state);
198
+
199
+ try {
200
+ // Exchange code for tokens
201
+ const tokenResp = await fetch('https://oauth2.googleapis.com/token', {
202
+ method: 'POST',
203
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
204
+ body: new URLSearchParams({
205
+ client_id: GOOGLE_CLIENT_ID,
206
+ client_secret: GOOGLE_CLIENT_SECRET,
207
+ code,
208
+ code_verifier: codeVerifier,
209
+ grant_type: 'authorization_code',
210
+ redirect_uri: GOOGLE_REDIRECT_URI,
211
+ }),
212
+ });
213
+
214
+ if (!tokenResp.ok) {
215
+ const err = await tokenResp.text();
216
+ console.error('[google-oauth] Token exchange failed:', err);
217
+ return res.redirect('/#settings?oauth=error&message=Token+exchange+failed');
218
+ }
219
+
220
+ const tokens = await tokenResp.json();
221
+
222
+ // Get user profile
223
+ const profileResp = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
224
+ headers: { Authorization: `Bearer ${tokens.access_token}` },
225
+ });
226
+ const profile = await profileResp.json();
227
+
228
+ // Store account
229
+ const store = loadTokens();
230
+
231
+ // Check if this email is already connected — update if so
232
+ const existingIdx = store.google.accounts.findIndex(a => a.email === profile.email);
233
+ const account = {
234
+ id: existingIdx >= 0 ? store.google.accounts[existingIdx].id : uuidv4(),
235
+ email: profile.email,
236
+ name: profile.name || profile.email,
237
+ picture: profile.picture || null,
238
+ type: profile.hd ? 'workspace' : 'personal',
239
+ access_token: encrypt(tokens.access_token),
240
+ refresh_token: encrypt(tokens.refresh_token),
241
+ token_expiry: new Date(Date.now() + tokens.expires_in * 1000).toISOString(),
242
+ scopes: SCOPES,
243
+ status: 'connected',
244
+ connected_at: new Date().toISOString(),
245
+ };
246
+
247
+ if (existingIdx >= 0) {
248
+ store.google.accounts[existingIdx] = account;
249
+ } else {
250
+ store.google.accounts.push(account);
251
+ }
252
+ saveTokens(store);
253
+
254
+ console.log(`[google-oauth] Connected: ${profile.email} (${account.type})`);
255
+ return res.redirect('/#settings?oauth=success&email=' + encodeURIComponent(profile.email));
256
+
257
+ } catch (e) {
258
+ console.error('[google-oauth] Callback error:', e);
259
+ return res.redirect('/#settings?oauth=error&message=' + encodeURIComponent(e.message));
260
+ }
261
+ });
262
+
263
+ // Account status
264
+ router.get('/status', (req, res) => {
265
+ const store = loadTokens();
266
+ const accounts = store.google.accounts.map(a => ({
267
+ id: a.id,
268
+ email: a.email,
269
+ name: a.name,
270
+ picture: a.picture,
271
+ type: a.type,
272
+ status: a.status || 'connected',
273
+ token_expiry: a.token_expiry,
274
+ connected_at: a.connected_at,
275
+ scopes: a.scopes,
276
+ }));
277
+ res.json({ success: true, data: { accounts, configured: !!(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET) } });
278
+ });
279
+
280
+ // Disconnect account
281
+ router.delete('/disconnect', async (req, res) => {
282
+ const { accountId } = req.body || {};
283
+ if (!accountId) return res.json({ success: false, error: 'accountId required' });
284
+
285
+ const store = loadTokens();
286
+ const idx = store.google.accounts.findIndex(a => a.id === accountId);
287
+ if (idx < 0) return res.json({ success: false, error: 'Account not found' });
288
+
289
+ const account = store.google.accounts[idx];
290
+
291
+ // Attempt to revoke with Google
292
+ try {
293
+ const token = decrypt(account.refresh_token);
294
+ await fetch(`https://oauth2.googleapis.com/revoke?token=${encodeURIComponent(token)}`, {
295
+ method: 'POST',
296
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
297
+ });
298
+ } catch (e) {
299
+ console.warn('[google-oauth] Revocation failed (deleting locally anyway):', e.message);
300
+ }
301
+
302
+ // Delete locally regardless
303
+ store.google.accounts.splice(idx, 1);
304
+ saveTokens(store);
305
+
306
+ console.log(`[google-oauth] Disconnected: ${account.email}`);
307
+ res.json({ success: true, data: { email: account.email } });
308
+ });
309
+
310
+ module.exports = { router, getValidAccessToken, loadTokens, decrypt, encrypt };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Grok Imagine — Image & Video Generation via xAI API
3
+ */
4
+
5
+ const XAI_API_KEY = process.env.XAI_API_KEY || 'xai-Gn37fuJg5ty4gvWFG2rbth34AxNORUKH8r4vTXQDtjwMGUqKZ7nYy8u2YStosGUCVBEg7VMHSqQZcKS4';
6
+ const IMAGE_API = 'https://api.x.ai/v1/images/generations';
7
+ const VIDEO_API = 'https://api.x.ai/v1/videos/generations';
8
+ const VIDEO_POLL_BASE = 'https://api.x.ai/v1/videos';
9
+
10
+ const headers = () => ({
11
+ 'Authorization': `Bearer ${XAI_API_KEY}`,
12
+ 'Content-Type': 'application/json',
13
+ });
14
+
15
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
16
+
17
+ /**
18
+ * Generate an image from a text prompt (or edit with imageUrl).
19
+ */
20
+ async function generateImage(prompt, options = {}) {
21
+ const body = {
22
+ model: 'grok-imagine-image',
23
+ prompt,
24
+ n: options.n || 1,
25
+ response_format: 'url',
26
+ };
27
+ if (options.imageUrl) body.image_url = options.imageUrl;
28
+
29
+ console.log('[grok-imagine] Generating image:', prompt);
30
+ const res = await fetch(IMAGE_API, { method: 'POST', headers: headers(), body: JSON.stringify(body) });
31
+ if (!res.ok) {
32
+ const err = await res.text();
33
+ throw new Error(`Image generation failed (${res.status}): ${err}`);
34
+ }
35
+ const data = await res.json();
36
+ const url = data.data?.[0]?.url;
37
+ if (!url) throw new Error('No image URL in response');
38
+ console.log('[grok-imagine] Image generated:', url.substring(0, 80));
39
+ return { url };
40
+ }
41
+
42
+ /**
43
+ * Generate a video (async — polls until done).
44
+ */
45
+ async function generateVideo(prompt, options = {}) {
46
+ const body = {
47
+ model: 'grok-imagine-video',
48
+ prompt,
49
+ };
50
+ if (options.duration) body.duration = options.duration;
51
+ if (options.aspectRatio || options.aspect_ratio) body.aspect_ratio = options.aspectRatio || options.aspect_ratio;
52
+ if (options.resolution) body.resolution = options.resolution;
53
+ if (options.imageUrl) body.image_url = options.imageUrl;
54
+ if (options.videoUrl) body.video_url = options.videoUrl;
55
+
56
+ console.log('[grok-imagine] Starting video generation:', prompt);
57
+ const res = await fetch(VIDEO_API, { method: 'POST', headers: headers(), body: JSON.stringify(body) });
58
+ if (!res.ok) {
59
+ const err = await res.text();
60
+ throw new Error(`Video generation failed (${res.status}): ${err}`);
61
+ }
62
+ const { request_id } = await res.json();
63
+ if (!request_id) throw new Error('No request_id in video response');
64
+ console.log('[grok-imagine] Video request_id:', request_id);
65
+
66
+ // Poll every 5 sec, timeout 10 min
67
+ const timeout = Date.now() + 10 * 60 * 1000;
68
+ while (Date.now() < timeout) {
69
+ await sleep(5000);
70
+ const poll = await fetch(`${VIDEO_POLL_BASE}/${request_id}`, { headers: headers() });
71
+ if (!poll.ok) {
72
+ console.warn('[grok-imagine] Poll error:', poll.status);
73
+ continue;
74
+ }
75
+ const status = await poll.json();
76
+ console.log('[grok-imagine] Video status:', status.status);
77
+ if (status.status === 'done') {
78
+ const url = status.video?.url;
79
+ if (!url) throw new Error('Video done but no URL');
80
+ return { url, duration: status.video?.duration };
81
+ }
82
+ if (status.status === 'expired') throw new Error('Video generation expired');
83
+ }
84
+ throw new Error('Video generation timed out (10 min)');
85
+ }
86
+
87
+ /** Convenience: edit an image */
88
+ async function editImage(prompt, sourceImageUrl) {
89
+ return generateImage(prompt, { imageUrl: sourceImageUrl });
90
+ }
91
+
92
+ /** Convenience: edit a video */
93
+ async function editVideo(prompt, sourceVideoUrl) {
94
+ return generateVideo(prompt, { videoUrl: sourceVideoUrl });
95
+ }
96
+
97
+ module.exports = { generateImage, generateVideo, editImage, editVideo };
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AIVA Health Reporter
4
+ * Runs on each device, collects health data every 5 minutes,
5
+ * and POSTs it to the dashboard.
6
+ */
7
+
8
+ const http = require('http');
9
+ const https = require('https');
10
+ const { execSync } = require('child_process');
11
+ const os = require('os');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // Config from env
16
+ const DASHBOARD_URL = process.env.DASHBOARD_URL || 'http://localhost:3456';
17
+ const DEVICE_ID = process.env.DEVICE_ID || os.hostname();
18
+ const HEARTBEAT_TOKEN = process.env.HEARTBEAT_TOKEN || '';
19
+ const INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
20
+ const LOCAL_PORT = 3853;
21
+ const LOG_FILE = path.join(__dirname, 'data', 'health-reporter.log');
22
+
23
+ // Ensure data dir exists
24
+ try { fs.mkdirSync(path.join(__dirname, 'data'), { recursive: true }); } catch {}
25
+
26
+ function log(msg) {
27
+ const line = `[${new Date().toISOString()}] ${msg}`;
28
+ console.log(line);
29
+ try { fs.appendFileSync(LOG_FILE, line + '\n'); } catch {}
30
+ }
31
+
32
+ function run(cmd, fallback = null) {
33
+ try { return execSync(cmd, { encoding: 'utf8', timeout: 10000 }).trim(); } catch { return fallback; }
34
+ }
35
+
36
+ function getGitHash() {
37
+ return run(`git -C "${__dirname}" rev-parse HEAD`, 'unknown');
38
+ }
39
+
40
+ function getDiskSpace() {
41
+ try {
42
+ const raw = run('df -k / | tail -1');
43
+ if (!raw) return null;
44
+ const parts = raw.split(/\s+/);
45
+ const total = parseInt(parts[1]) * 1024;
46
+ const used = parseInt(parts[2]) * 1024;
47
+ const available = parseInt(parts[3]) * 1024;
48
+ return { total, used, available, percentUsed: Math.round((used / total) * 100) };
49
+ } catch { return null; }
50
+ }
51
+
52
+ function getMemoryUsage() {
53
+ const total = os.totalmem();
54
+ const free = os.freemem();
55
+ return { total, free, used: total - free, percentUsed: Math.round(((total - free) / total) * 100) };
56
+ }
57
+
58
+ function getProcessStatus() {
59
+ // Check PM2 first
60
+ const pm2List = run('pm2 jlist 2>/dev/null');
61
+ if (pm2List) {
62
+ try {
63
+ const procs = JSON.parse(pm2List);
64
+ const aivaProc = procs.find(p => p.name === 'aiva-app');
65
+ if (aivaProc) {
66
+ return {
67
+ manager: 'pm2',
68
+ running: aivaProc.pm2_env?.status === 'online',
69
+ status: aivaProc.pm2_env?.status || 'unknown',
70
+ uptime: aivaProc.pm2_env?.pm_uptime ? Date.now() - aivaProc.pm2_env.pm_uptime : null,
71
+ restarts: aivaProc.pm2_env?.restart_time || 0,
72
+ pid: aivaProc.pid
73
+ };
74
+ }
75
+ } catch {}
76
+ }
77
+ // Check LaunchAgent
78
+ const launchCheck = run('launchctl list 2>/dev/null | grep aiva');
79
+ return {
80
+ manager: launchCheck ? 'launchagent' : 'unknown',
81
+ running: !!launchCheck,
82
+ status: launchCheck ? 'running' : 'not found'
83
+ };
84
+ }
85
+
86
+ function getAutoDeployStatus() {
87
+ // Check if auto-deploy process is running
88
+ const pm2List = run('pm2 jlist 2>/dev/null');
89
+ let running = false;
90
+ if (pm2List) {
91
+ try {
92
+ const procs = JSON.parse(pm2List);
93
+ running = procs.some(p => p.name === 'auto-deploy' && p.pm2_env?.status === 'online');
94
+ } catch {}
95
+ }
96
+ // Check auto-deploy log for last activity
97
+ const logPath = path.join(__dirname, 'data', 'auto-deploy.log');
98
+ let lastCheck = null, lastUpdate = null;
99
+ try {
100
+ const lines = fs.readFileSync(logPath, 'utf8').split('\n').filter(Boolean).slice(-50);
101
+ for (const line of lines.reverse()) {
102
+ if (!lastCheck && line.includes('Checking')) lastCheck = line.match(/\[(.*?)\]/)?.[1];
103
+ if (!lastUpdate && (line.includes('Updated') || line.includes('Deployed'))) lastUpdate = line.match(/\[(.*?)\]/)?.[1];
104
+ if (lastCheck && lastUpdate) break;
105
+ }
106
+ } catch {}
107
+ return { running, lastCheck, lastUpdate };
108
+ }
109
+
110
+ function getErrorCount() {
111
+ // Parse recent PM2 logs for errors in last hour
112
+ const oneHourAgo = Date.now() - 3600000;
113
+ let count = 0;
114
+ try {
115
+ const logDir = path.join(os.homedir(), '.pm2', 'logs');
116
+ const files = fs.readdirSync(logDir).filter(f => f.includes('error') || f.includes('aiva'));
117
+ for (const file of files) {
118
+ const stat = fs.statSync(path.join(logDir, file));
119
+ if (stat.mtimeMs < oneHourAgo) continue;
120
+ const content = fs.readFileSync(path.join(logDir, file), 'utf8');
121
+ const lines = content.split('\n');
122
+ for (const line of lines) {
123
+ if (line.toLowerCase().includes('error') || line.toLowerCase().includes('exception')) {
124
+ // Try to extract timestamp
125
+ const ts = line.match(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/);
126
+ if (ts && new Date(ts[0]).getTime() > oneHourAgo) count++;
127
+ else if (!ts) count++; // Count if no timestamp
128
+ }
129
+ }
130
+ }
131
+ } catch {}
132
+ return count;
133
+ }
134
+
135
+ function getMemorySearchStatus() {
136
+ try {
137
+ const raw = run('export PATH="/opt/homebrew/bin:$PATH"; openclaw memory status --deep 2>&1');
138
+ if (!raw) return { status: 'unknown', error: 'no output' };
139
+
140
+ const providerMatch = raw.match(/Provider:\s*(.+)/);
141
+ const embeddingsMatch = raw.match(/Embeddings:\s*(.+)/);
142
+ const indexedMatch = raw.match(/Indexed:\s*(.+)/);
143
+ const chunksMatch = raw.match(/(\d+)\s*chunks/);
144
+
145
+ const embeddingsReady = embeddingsMatch && embeddingsMatch[1].trim().toLowerCase() === 'ready';
146
+
147
+ return {
148
+ status: embeddingsReady ? 'healthy' : 'broken',
149
+ provider: providerMatch ? providerMatch[1].trim() : 'unknown',
150
+ embeddings: embeddingsMatch ? embeddingsMatch[1].trim() : 'unknown',
151
+ indexed: indexedMatch ? indexedMatch[1].trim() : 'unknown',
152
+ chunks: chunksMatch ? parseInt(chunksMatch[1]) : 0
153
+ };
154
+ } catch (e) {
155
+ return { status: 'error', error: e.message };
156
+ }
157
+ }
158
+
159
+ function getTokenStatus() {
160
+ try {
161
+ const tokensPath = path.join(__dirname, 'data', 'tokens.json');
162
+ const tokens = JSON.parse(fs.readFileSync(tokensPath, 'utf8'));
163
+ return Object.keys(tokens).map(name => ({
164
+ name,
165
+ configured: !!tokens[name] && tokens[name] !== ''
166
+ }));
167
+ } catch { return []; }
168
+ }
169
+
170
+ async function checkConnectivity(url) {
171
+ return new Promise(resolve => {
172
+ const mod = url.startsWith('https') ? https : http;
173
+ const req = mod.get(url, { timeout: 5000 }, (res) => {
174
+ resolve(true);
175
+ res.resume();
176
+ });
177
+ req.on('error', () => resolve(false));
178
+ req.on('timeout', () => { req.destroy(); resolve(false); });
179
+ });
180
+ }
181
+
182
+ async function collectHealthData() {
183
+ const [canReachGithub, canReachDashboard] = await Promise.all([
184
+ checkConnectivity('https://github.com'),
185
+ checkConnectivity(DASHBOARD_URL)
186
+ ]);
187
+
188
+ return {
189
+ deviceId: DEVICE_ID,
190
+ timestamp: new Date().toISOString(),
191
+ gitHash: getGitHash(),
192
+ uptime: {
193
+ process: Math.floor(process.uptime()),
194
+ system: Math.floor(os.uptime())
195
+ },
196
+ disk: getDiskSpace(),
197
+ memory: getMemoryUsage(),
198
+ appProcess: getProcessStatus(),
199
+ autoDeploy: getAutoDeployStatus(),
200
+ nodeVersion: process.version,
201
+ macosVersion: run('sw_vers -productVersion', os.release()),
202
+ hostname: os.hostname(),
203
+ errorCount: getErrorCount(),
204
+ tokens: getTokenStatus(),
205
+ memorySearch: getMemorySearchStatus(),
206
+ network: {
207
+ github: canReachGithub,
208
+ dashboard: canReachDashboard
209
+ }
210
+ };
211
+ }
212
+
213
+ function postHeartbeat(data) {
214
+ return new Promise((resolve, reject) => {
215
+ const url = new URL('/api/devices/heartbeat', DASHBOARD_URL);
216
+ const payload = JSON.stringify(data);
217
+ const mod = url.protocol === 'https:' ? https : http;
218
+
219
+ const req = mod.request(url, {
220
+ method: 'POST',
221
+ headers: {
222
+ 'Content-Type': 'application/json',
223
+ 'Content-Length': Buffer.byteLength(payload),
224
+ 'X-Device-Id': DEVICE_ID,
225
+ 'X-Heartbeat-Token': HEARTBEAT_TOKEN
226
+ },
227
+ timeout: 10000
228
+ }, (res) => {
229
+ let body = '';
230
+ res.on('data', c => body += c);
231
+ res.on('end', () => resolve({ status: res.statusCode, body }));
232
+ });
233
+
234
+ req.on('error', reject);
235
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
236
+ req.write(payload);
237
+ req.end();
238
+ });
239
+ }
240
+
241
+ let lastHealthData = null;
242
+
243
+ async function heartbeatCycle() {
244
+ try {
245
+ log('Collecting health data...');
246
+ const data = await collectHealthData();
247
+ lastHealthData = data;
248
+
249
+ log(`Sending heartbeat to ${DASHBOARD_URL}...`);
250
+ const result = await postHeartbeat(data);
251
+ log(`Heartbeat sent: ${result.status}`);
252
+ } catch (err) {
253
+ log(`Heartbeat error: ${err.message}`);
254
+ }
255
+ }
256
+
257
+ // Local /health endpoint for diagnostics
258
+ const server = http.createServer(async (req, res) => {
259
+ if (req.url === '/health' && req.method === 'GET') {
260
+ const data = lastHealthData || await collectHealthData();
261
+ res.writeHead(200, { 'Content-Type': 'application/json' });
262
+ res.end(JSON.stringify(data, null, 2));
263
+ } else {
264
+ res.writeHead(404);
265
+ res.end('Not found');
266
+ }
267
+ });
268
+
269
+ server.listen(LOCAL_PORT, () => {
270
+ log(`Health reporter started. Device: ${DEVICE_ID}`);
271
+ log(`Local diagnostics: http://localhost:${LOCAL_PORT}/health`);
272
+ log(`Dashboard: ${DASHBOARD_URL}`);
273
+
274
+ // Initial heartbeat
275
+ heartbeatCycle();
276
+
277
+ // Schedule every 5 minutes
278
+ setInterval(heartbeatCycle, INTERVAL_MS);
279
+ });
280
+
281
+ server.on('error', (err) => {
282
+ if (err.code === 'EADDRINUSE') {
283
+ log(`Port ${LOCAL_PORT} in use, running without local endpoint`);
284
+ heartbeatCycle();
285
+ setInterval(heartbeatCycle, INTERVAL_MS);
286
+ }
287
+ });